//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") } }