113 lines
2.9 KiB
Go
113 lines
2.9 KiB
Go
//go:build linux
|
|
|
|
// Package term lee contraseñas del terminal sin eco (§80).
|
|
//
|
|
// Se implementa con ioctl(TCGETS/TCSETS) de la biblioteca estándar en lugar
|
|
// de golang.org/x/term para no añadir dependencias; el proyecto solo
|
|
// soporta Linux.
|
|
package term
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"unsafe"
|
|
)
|
|
|
|
// ErrNotTerminal indica que la entrada no es un terminal interactivo.
|
|
var ErrNotTerminal = errors.New("input is not an interactive terminal")
|
|
|
|
func getTermios(fd uintptr) (*syscall.Termios, error) {
|
|
var t syscall.Termios
|
|
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, syscall.TCGETS, uintptr(unsafe.Pointer(&t))); errno != 0 {
|
|
return nil, errno
|
|
}
|
|
return &t, nil
|
|
}
|
|
|
|
func setTermios(fd uintptr, t *syscall.Termios) error {
|
|
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, syscall.TCSETS, uintptr(unsafe.Pointer(t))); errno != 0 {
|
|
return errno
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IsTerminal informa si f es un terminal.
|
|
func IsTerminal(f *os.File) bool {
|
|
_, err := getTermios(f.Fd())
|
|
return err == nil
|
|
}
|
|
|
|
// DisableEcho desactiva el eco del terminal f y devuelve la función que lo
|
|
// restaura. Lo usa el CLI, que ya tiene su propio lector de stdin: se
|
|
// desactiva el eco y la línea se recibe por ese mismo lector, evitando dos
|
|
// lectores compitiendo por la entrada.
|
|
func DisableEcho(f *os.File) (restore func(), err error) {
|
|
fd := f.Fd()
|
|
old, err := getTermios(fd)
|
|
if err != nil {
|
|
return nil, ErrNotTerminal
|
|
}
|
|
noEcho := *old
|
|
noEcho.Lflag &^= syscall.ECHO
|
|
noEcho.Lflag |= syscall.ICANON | syscall.ISIG
|
|
if err := setTermios(fd, &noEcho); err != nil {
|
|
return nil, fmt.Errorf("failed to disable echo: %w", err)
|
|
}
|
|
return func() { setTermios(fd, old) }, nil
|
|
}
|
|
|
|
// ReadPassword muestra prompt en out y lee una línea de in sin eco.
|
|
//
|
|
// Garantiza que el eco se restaura siempre, también si llega SIGINT/SIGTERM
|
|
// mientras se escribe (en ese caso devuelve error).
|
|
func ReadPassword(in *os.File, out io.Writer, prompt string) (string, error) {
|
|
fd := in.Fd()
|
|
old, err := getTermios(fd)
|
|
if err != nil {
|
|
return "", ErrNotTerminal
|
|
}
|
|
noEcho := *old
|
|
noEcho.Lflag &^= syscall.ECHO
|
|
noEcho.Lflag |= syscall.ICANON | syscall.ISIG
|
|
noEcho.Iflag |= syscall.ICRNL
|
|
if err := setTermios(fd, &noEcho); err != nil {
|
|
return "", fmt.Errorf("failed to disable echo: %w", err)
|
|
}
|
|
|
|
sig := make(chan os.Signal, 1)
|
|
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
|
|
restore := func() {
|
|
setTermios(fd, old)
|
|
signal.Stop(sig)
|
|
}
|
|
defer restore()
|
|
|
|
fmt.Fprint(out, prompt)
|
|
type result struct {
|
|
line string
|
|
err error
|
|
}
|
|
done := make(chan result, 1)
|
|
go func() {
|
|
line, err := bufio.NewReader(in).ReadString('\n')
|
|
done <- result{line, err}
|
|
}()
|
|
select {
|
|
case r := <-done:
|
|
fmt.Fprintln(out)
|
|
if r.err != nil && r.line == "" {
|
|
return "", fmt.Errorf("failed to read secret: %w", r.err)
|
|
}
|
|
return strings.TrimRight(r.line, "\r\n"), nil
|
|
case <-sig:
|
|
fmt.Fprintln(out)
|
|
return "", errors.New("secret input cancelled")
|
|
}
|
|
}
|