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:
2026-03-28 11:40:11 -07:00
parent e4220b840e
commit 5b5e1a7ed6
142 changed files with 10241 additions and 7788 deletions

View File

@@ -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{}{