Use mcdsl/terminal for all password prompts
Replace direct golang.org/x/term calls with mcdsl/terminal.ReadPassword across mciasctl (6 sites), mciasgrpcctl (1 site), and mciasdb (1 site). Aligns with the new CLI security standard in engineering-standards.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -59,7 +59,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
|
||||
"git.wntrmute.dev/mc/mcdsl/terminal"
|
||||
)
|
||||
|
||||
// Global flags bound by the root command's PersistentFlags.
|
||||
@@ -139,7 +140,7 @@ func authCmd() *cobra.Command {
|
||||
// appearing in shell history, ps output, and process argument lists.
|
||||
//
|
||||
// Security: terminal echo is disabled during password entry
|
||||
// (golang.org/x/term.ReadPassword); the raw byte slice is zeroed after use.
|
||||
// (mcdsl/terminal.ReadPassword).
|
||||
func authLoginCmd() *cobra.Command {
|
||||
var username string
|
||||
var totpCode string
|
||||
@@ -161,17 +162,10 @@ Example: export MCIAS_TOKEN=$(mciasctl auth login --username alice)`,
|
||||
// Security: always prompt interactively; never accept password as a flag.
|
||||
// This prevents the credential from appearing in shell history, ps output,
|
||||
// and /proc/PID/cmdline.
|
||||
fmt.Fprint(os.Stderr, "Password: ")
|
||||
raw, err := term.ReadPassword(int(os.Stdin.Fd())) //nolint:gosec // uintptr==int on all target platforms
|
||||
fmt.Fprintln(os.Stderr) // newline after hidden input
|
||||
passwd, err := terminal.ReadPassword("Password: ")
|
||||
if err != nil {
|
||||
fatalf("read password: %v", err)
|
||||
}
|
||||
passwd := string(raw)
|
||||
// Zero the raw byte slice once copied into the string.
|
||||
for i := range raw {
|
||||
raw[i] = 0
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
"username": username,
|
||||
@@ -206,10 +200,10 @@ Example: export MCIAS_TOKEN=$(mciasctl auth login --username alice)`,
|
||||
// command-line flags to prevent them from appearing in shell history, ps
|
||||
// output, and process argument lists.
|
||||
//
|
||||
// Security: terminal echo is disabled during entry (golang.org/x/term);
|
||||
// raw byte slices are zeroed after use. The server requires the current
|
||||
// password to prevent token-theft attacks. On success all other active
|
||||
// sessions are revoked server-side.
|
||||
// Security: terminal echo is disabled during entry
|
||||
// (mcdsl/terminal.ReadPassword). The server requires the current password
|
||||
// to prevent token-theft attacks. On success all other active sessions are
|
||||
// revoked server-side.
|
||||
func authChangePasswordCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "change-password",
|
||||
@@ -221,27 +215,15 @@ Revokes all other active sessions on success.`,
|
||||
c := newController()
|
||||
|
||||
// Security: always prompt interactively; never accept passwords as flags.
|
||||
fmt.Fprint(os.Stderr, "Current password: ")
|
||||
rawCurrent, err := term.ReadPassword(int(os.Stdin.Fd())) //nolint:gosec // uintptr==int on all target platforms
|
||||
fmt.Fprintln(os.Stderr)
|
||||
currentPasswd, err := terminal.ReadPassword("Current password: ")
|
||||
if err != nil {
|
||||
fatalf("read current password: %v", err)
|
||||
}
|
||||
currentPasswd := string(rawCurrent)
|
||||
for i := range rawCurrent {
|
||||
rawCurrent[i] = 0
|
||||
}
|
||||
|
||||
fmt.Fprint(os.Stderr, "New password: ")
|
||||
rawNew, err := term.ReadPassword(int(os.Stdin.Fd())) //nolint:gosec // uintptr==int on all target platforms
|
||||
fmt.Fprintln(os.Stderr)
|
||||
newPasswd, err := terminal.ReadPassword("New password: ")
|
||||
if err != nil {
|
||||
fatalf("read new password: %v", err)
|
||||
}
|
||||
newPasswd := string(rawNew)
|
||||
for i := range rawNew {
|
||||
rawNew[i] = 0
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
"current_password": currentPasswd,
|
||||
@@ -297,20 +279,15 @@ func accountCreateCmd() *cobra.Command {
|
||||
c := newController()
|
||||
|
||||
// Security: always prompt interactively for human-account passwords; never
|
||||
// accept them as a flag. Terminal echo is disabled; the raw byte slice is
|
||||
// zeroed after conversion to string. System accounts have no password.
|
||||
// accept them as a flag. Terminal echo is disabled via
|
||||
// mcdsl/terminal.ReadPassword. System accounts have no password.
|
||||
var passwd string
|
||||
if accountType == "human" {
|
||||
fmt.Fprint(os.Stderr, "Password: ")
|
||||
raw, err := term.ReadPassword(int(os.Stdin.Fd())) //nolint:gosec // uintptr==int on all target platforms
|
||||
fmt.Fprintln(os.Stderr)
|
||||
var err error
|
||||
passwd, err = terminal.ReadPassword("Password: ")
|
||||
if err != nil {
|
||||
fatalf("read password: %v", err)
|
||||
}
|
||||
passwd = string(raw)
|
||||
for i := range raw {
|
||||
raw[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
body := map[string]string{
|
||||
@@ -405,7 +382,7 @@ func accountDeleteCmd() *cobra.Command {
|
||||
// Security: the new password is always prompted interactively; it is never
|
||||
// accepted as a command-line flag to prevent it from appearing in shell
|
||||
// history, ps output, and process argument lists. Terminal echo is disabled
|
||||
// (golang.org/x/term); the raw byte slice is zeroed after use.
|
||||
// (mcdsl/terminal.ReadPassword).
|
||||
func accountSetPasswordCmd() *cobra.Command {
|
||||
var id string
|
||||
|
||||
@@ -423,16 +400,10 @@ Revokes all active sessions for the account.`,
|
||||
c := newController()
|
||||
|
||||
// Security: always prompt interactively; never accept password as a flag.
|
||||
fmt.Fprint(os.Stderr, "New password: ")
|
||||
raw, err := term.ReadPassword(int(os.Stdin.Fd())) //nolint:gosec // uintptr==int on all target platforms
|
||||
fmt.Fprintln(os.Stderr)
|
||||
passwd, err := terminal.ReadPassword("New password: ")
|
||||
if err != nil {
|
||||
fatalf("read password: %v", err)
|
||||
}
|
||||
passwd := string(raw)
|
||||
for i := range raw {
|
||||
raw[i] = 0
|
||||
}
|
||||
|
||||
body := map[string]string{"new_password": passwd}
|
||||
c.doRequest("PUT", "/v1/accounts/"+id+"/password", body, nil)
|
||||
@@ -684,20 +655,15 @@ func pgcredsSetCmd() *cobra.Command {
|
||||
|
||||
// Prompt for the Postgres password interactively if not supplied so it
|
||||
// stays out of shell history.
|
||||
// Security: terminal echo is disabled during entry; the raw byte slice is
|
||||
// zeroed after conversion to string.
|
||||
// Security: terminal echo is disabled during entry via
|
||||
// mcdsl/terminal.ReadPassword.
|
||||
passwd := password
|
||||
if passwd == "" {
|
||||
fmt.Fprint(os.Stderr, "Postgres password: ")
|
||||
raw, err := term.ReadPassword(int(os.Stdin.Fd())) //nolint:gosec // uintptr==int on all target platforms
|
||||
fmt.Fprintln(os.Stderr)
|
||||
var err error
|
||||
passwd, err = terminal.ReadPassword("Postgres password: ")
|
||||
if err != nil {
|
||||
fatalf("read password: %v", err)
|
||||
}
|
||||
passwd = string(raw)
|
||||
for i := range raw {
|
||||
raw[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
|
||||
@@ -8,7 +8,8 @@ import (
|
||||
|
||||
"git.wntrmute.dev/mc/mcias/internal/auth"
|
||||
"git.wntrmute.dev/mc/mcias/internal/model"
|
||||
"golang.org/x/term"
|
||||
|
||||
"git.wntrmute.dev/mc/mcdsl/terminal"
|
||||
)
|
||||
|
||||
func (t *tool) runAccount(args []string) {
|
||||
@@ -233,20 +234,14 @@ func (t *tool) accountResetTOTP(args []string) {
|
||||
// readPassword reads a password from the terminal without echo.
|
||||
// Falls back to a regular line read if stdin is not a terminal (e.g. in tests).
|
||||
func readPassword(prompt string) (string, error) {
|
||||
fmt.Fprint(os.Stderr, prompt)
|
||||
fd := int(os.Stdin.Fd()) //nolint:gosec // G115: file descriptors are non-negative and fit in int on all supported platforms
|
||||
if term.IsTerminal(fd) {
|
||||
pw, err := term.ReadPassword(fd)
|
||||
fmt.Fprintln(os.Stderr) // newline after hidden input
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read password from terminal: %w", err)
|
||||
}
|
||||
return string(pw), nil
|
||||
pw, err := terminal.ReadPassword(prompt)
|
||||
if err == nil {
|
||||
return pw, nil
|
||||
}
|
||||
// Not a terminal: read a plain line (for piped input in tests).
|
||||
// Fallback for piped input (e.g. tests).
|
||||
fmt.Fprint(os.Stderr, prompt)
|
||||
var line string
|
||||
_, err := fmt.Fscanln(os.Stdin, &line)
|
||||
if err != nil {
|
||||
if _, err := fmt.Fscanln(os.Stdin, &line); err != nil {
|
||||
return "", fmt.Errorf("read password: %w", err)
|
||||
}
|
||||
return line, nil
|
||||
|
||||
@@ -59,11 +59,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"git.wntrmute.dev/mc/mcdsl/terminal"
|
||||
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
|
||||
)
|
||||
|
||||
@@ -213,7 +213,7 @@ func authCmd(ctl *controller) *cobra.Command {
|
||||
// lists.
|
||||
//
|
||||
// Security: terminal echo is disabled during password entry
|
||||
// (golang.org/x/term.ReadPassword); the raw byte slice is zeroed after use.
|
||||
// (mcdsl/terminal.ReadPassword).
|
||||
func authLoginCmd(ctl *controller) *cobra.Command {
|
||||
var (
|
||||
username string
|
||||
@@ -230,17 +230,10 @@ func authLoginCmd(ctl *controller) *cobra.Command {
|
||||
// Security: always prompt interactively; never accept password as a flag.
|
||||
// This prevents the credential from appearing in shell history, ps output,
|
||||
// and /proc/PID/cmdline.
|
||||
fmt.Fprint(os.Stderr, "Password: ")
|
||||
raw, err := term.ReadPassword(int(os.Stdin.Fd())) //nolint:gosec // uintptr==int on all target platforms
|
||||
fmt.Fprintln(os.Stderr)
|
||||
passwd, err := terminal.ReadPassword("Password: ")
|
||||
if err != nil {
|
||||
fatalf("read password: %v", err)
|
||||
}
|
||||
passwd := string(raw)
|
||||
// Zero the raw byte slice once copied into the string.
|
||||
for i := range raw {
|
||||
raw[i] = 0
|
||||
}
|
||||
|
||||
authCl := mciasv1.NewAuthServiceClient(ctl.conn)
|
||||
// Login is a public RPC — no auth context needed.
|
||||
|
||||
Reference in New Issue
Block a user