Skip to content

Instantly share code, notes, and snippets.

@jlinoff
Last active May 31, 2022 07:21
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save jlinoff/e8e26b4ffa38d379c7f1891fd174a6d0 to your computer and use it in GitHub Desktop.
Save jlinoff/e8e26b4ffa38d379c7f1891fd174a6d0 to your computer and use it in GitHub Desktop.
Go code to prompt for password using only standard packages by utilizing syscall.ForkExec() and syscall.Wait4(), recovers from ^C gracefully.
// License: MIT Open Source
// Copyright (c) Joe Linoff 2016
// Go code to prompt for password using only standard packages by utilizing syscall.ForkExec() and syscall.Wait4().
// Correctly resets terminal echo after ^C interrupts.
package main
import (
"bufio"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
)
func main() {
// Get password.
p := getPassword("password: ")
fmt.Println("value:", p)
}
// techEcho() - turns terminal echo on or off.
func termEcho(on bool) {
// Common settings and variables for both stty calls.
attrs := syscall.ProcAttr{
Dir: "",
Env: []string{},
Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
Sys: nil}
var ws syscall.WaitStatus
cmd := "echo"
if on == false {
cmd = "-echo"
}
// Enable/disable echoing.
pid, err := syscall.ForkExec(
"/bin/stty",
[]string{"stty", cmd},
&attrs)
if err != nil {
panic(err)
}
// Wait for the stty process to complete.
_, err = syscall.Wait4(pid, &ws, 0, nil)
if err != nil {
panic(err)
}
}
// getPassword - Prompt for password.
func getPassword(prompt string) string {
fmt.Print(prompt)
// Catch a ^C interrupt.
// Make sure that we reset term echo before exiting.
signalChannel := make(chan os.Signal, 1)
signal.Notify(signalChannel, os.Interrupt)
go func() {
for _ = range signalChannel {
fmt.Println("\n^C interrupt.")
termEcho(true)
os.Exit(1)
}
}()
// Echo is disabled, now grab the data.
termEcho(false) // disable terminal echo
reader := bufio.NewReader(os.Stdin)
text, err := reader.ReadString('\n')
termEcho(true) // always re-enable terminal echo
fmt.Println("")
if err != nil {
// The terminal has been reset, go ahead and exit.
fmt.Println("ERROR:", err.Error())
os.Exit(1)
}
return strings.TrimSpace(text)
}
// License: MIT Open Source
// Copyright (c) Joe Linoff 2016
// Go code to prompt for password using only standard packages by utilizing syscall.Syscall6().
// Could be used to modify golang.org/x/crypto/ssh/terminal.
// Correctly resets terminal echo after ^C interrupts.
package main
import (
"fmt"
"io"
"os"
"os/signal"
"runtime"
"syscall"
"unsafe"
)
func main() {
// Get password.
p, e := getPassword("password: ")
if e != nil {
panic(e)
}
fmt.Println("value:", string(p))
}
// getPassword() gets the password.
// Similar to terminal.ReadPassword().
func getPassword(prompt string) ([]byte, error) {
fmt.Print(prompt)
// CITATION: https://github.com/golang/crypto/blob/master/ssh/terminal/util_linux.go
// CITATION: https://github.com/golang/crypto/blob/master/ssh/terminal/util_bsd.go
var ioctlReadTermios uintptr
var ioctlWriteTermios uintptr
switch runtime.GOOS {
case "linux":
ioctlReadTermios = 0x5401 // syscall.TCGETS
ioctlWriteTermios = 0x5402 // syscall.TCSETS
case "darwin":
ioctlReadTermios = syscall.TIOCGETA
ioctlWriteTermios = syscall.TIOCSETA
default:
return nil, fmt.Errorf("unsupported OS: " + runtime.GOOS)
}
// CITATION: https://github.com/golang/crypto/blob/master/ssh/terminal/util.go
fd := 1 // stdin
var oldState syscall.Termios
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0); err != 0 {
return nil, err
}
// Disable echo.
newState := oldState
newState.Lflag &^= syscall.ECHO
newState.Lflag |= syscall.ICANON | syscall.ISIG
newState.Iflag |= syscall.ICRNL
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 {
return nil, err
}
// Re-enable echo.
// Doesn't work for ^C interrupts.
defer func() {
syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0)
fmt.Println("")
}()
// Added this modification to handle a ^C interrupt.
// Make sure that we reset term echo before exiting.
signalChannel := make(chan os.Signal, 1)
signal.Notify(signalChannel, os.Interrupt)
go func() {
for _ = range signalChannel {
fmt.Println("\n^C interrupt.")
syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0)
os.Exit(1)
}
}()
// Read value byte by byte.
var buf [64]byte
var ret []byte
for {
n, err := syscall.Read(fd, buf[:])
if err != nil {
return nil, io.EOF
}
if n == 0 {
if len(ret) == 0 {
return nil, io.EOF
}
break
}
if buf[n-1] == '\n' {
n--
}
ret = append(ret, buf[:n]...)
if n < len(buf) {
break
}
}
return ret, nil
}
// License: MIT Open Source
// Copyright (c) Joe Linoff 2016
// Wrap around golang.org/x/crypto/ssh/terminal to handle ^C interrupts based on a suggestion by Konstantin Shaposhnikov in
// this thread: https://groups.google.com/forum/#!topic/golang-nuts/kTVAbtee9UA.
// Correctly resets terminal echo after ^C interrupts.
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"golang.org/x/crypto/ssh/terminal"
)
func main() {
// Get password.
p := getPassword("password: ")
fmt.Println("value:", p)
}
func getPassword(prompt string) string {
// Get the initial state of the terminal.
initialTermState, e1 := terminal.GetState(syscall.Stdin)
if e1 != nil {
panic(e1)
}
// Restore it in the event of an interrupt.
// CITATION: Konstantin Shaposhnikov - https://groups.google.com/forum/#!topic/golang-nuts/kTVAbtee9UA
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, os.Kill)
go func() {
<-c
_ = terminal.Restore(syscall.Stdin, initialTermState)
os.Exit(1)
}()
// Now get the password.
fmt.Print(prompt)
p, err := terminal.ReadPassword(syscall.Stdin)
fmt.Println("")
if err != nil {
panic(err)
}
// Stop looking for ^C on the channel.
signal.Stop(c)
// Return the password as a string.
return string(p)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment