Add secure byte slice erase helper; add alternative input methods for Windows

This commit is contained in:
2026-02-07 22:54:02 -05:00
parent 9ee213eeb4
commit e1bb0012a7
6 changed files with 151 additions and 25 deletions
+9
View File
@@ -0,0 +1,9 @@
package back
// EraseBytesSecurely overwrites all
// bytes in a slice with zeros.
func EraseBytesSecurely(input []byte) {
for i := range input {
input[i] = 0
}
}
+2 -18
View File
@@ -1,3 +1,5 @@
//go:build !windows
package front
import (
@@ -5,8 +7,6 @@ import (
"fmt"
"os"
"strings"
"golang.org/x/term"
)
// Input prompts the user for input and returns the input as a string.
@@ -17,14 +17,6 @@ func Input(prompt string) string {
return strings.TrimRight(userInput, "\n\r ") // remove trailing newlines, carriage returns, and spaces
}
// InputHidden prompts the user for input and returns the input as a byte array, hiding the input from the terminal.
func InputHidden(prompt string) []byte {
fmt.Print(prompt + " ")
byteInput, _ := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
return byteInput
}
// InputBinary prompts the user with a yes/no question and returns the response as a boolean.
func InputBinary(prompt string) bool {
reader := bufio.NewReader(os.Stdin)
@@ -49,11 +41,3 @@ func InputInt(prompt string, min, max int) int {
fmt.Println()
}
}
// InputMenuGen prompts the user with a menu and returns the user's choice as an integer.
func InputMenuGen(prompt string, options []string) int {
for i, option := range options {
fmt.Printf("%d. %s\n", i+1, option)
}
return InputInt("\n"+prompt, 1, len(options))
}
+109
View File
@@ -0,0 +1,109 @@
//go:build windows
package front
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
"syscall"
"time"
"unsafe"
)
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procGetStdHandle = kernel32.NewProc("GetStdHandle")
procPeekConsoleInput = kernel32.NewProc("PeekConsoleInputW")
procReadConsoleInput = kernel32.NewProc("ReadConsoleInputW")
)
const (
stdInputHandle = ^uintptr(10) - 1 // STD_INPUT_HANDLE = -10
)
type inputRecord struct {
EventType uint16
_ [2]byte // padding
Event [16]byte
}
// pollForInput checks if input is available without blocking
func pollForInput() bool {
handle, _, _ := procGetStdHandle.Call(stdInputHandle)
var numEvents uint32
var record inputRecord
ret, _, _ := procPeekConsoleInput.Call(
handle,
uintptr(unsafe.Pointer(&record)),
1,
uintptr(unsafe.Pointer(&numEvents)),
)
return ret != 0 && numEvents > 0
}
// Input prompts the user for input and returns the input as a string.
// Uses polling to be more responsive to interrupts on Windows.
func Input(prompt string) string {
fmt.Print(prompt + " ")
// Poll until input is available
for !pollForInput() {
time.Sleep(50 * time.Millisecond)
}
reader := bufio.NewReader(os.Stdin)
userInput, _ := reader.ReadString('\n')
return strings.TrimRight(userInput, "\n\r ") // remove trailing newlines, carriage returns, and spaces
}
// InputBinary prompts the user with a yes/no question and returns the response as a boolean.
// Uses polling to be more responsive to interrupts on Windows.
func InputBinary(prompt string) bool {
fmt.Print(prompt + " (y/N) ")
// Poll until input is available
for !pollForInput() {
time.Sleep(50 * time.Millisecond)
}
reader := bufio.NewReader(os.Stdin)
char, _, _ := reader.ReadRune()
if char == 'y' || char == 'Y' {
return true
}
return false
}
// InputInt prompts the user for input and returns the input as an integer.
// A negative min/max value will disable the respective limit.
// Uses polling to be more responsive to interrupts on Windows.
func InputInt(prompt string, min, max int) int {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print(prompt + " ")
// Poll until input is available
for !pollForInput() {
time.Sleep(50 * time.Millisecond)
}
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println()
continue
}
line = strings.TrimSpace(line)
userInput, err := strconv.Atoi(line)
if err == nil && (userInput >= min || min < 0) && (userInput <= max || max < 0) {
return userInput
}
fmt.Println()
}
}
+24
View File
@@ -0,0 +1,24 @@
package front
import (
"fmt"
"os"
"golang.org/x/term"
)
// InputMenuGen prompts the user with a menu and returns the user's choice as an integer.
func InputMenuGen(prompt string, options []string) int {
for i, option := range options {
fmt.Printf("%d. %s\n", i+1, option)
}
return InputInt("\n"+prompt, 1, len(options))
}
// InputHidden prompts the user for input and returns the input as a byte array, hiding the input from the terminal.
func InputHidden(prompt string) []byte {
fmt.Print(prompt + " ")
byteInput, _ := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
return byteInput
}
+3 -3
View File
@@ -1,7 +1,7 @@
module github.com/rwinkhart/go-boilerplate
go 1.25.5
go 1.25.6
require golang.org/x/term v0.38.0
require golang.org/x/term v0.39.0
require golang.org/x/sys v0.39.0 // indirect
require golang.org/x/sys v0.40.0 // indirect
+4 -4
View File
@@ -1,4 +1,4 @@
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=