//go:build linux package adminreset import ( "context" "errors" "io" "os/exec" "strings" "syscall" "time" "git.pcux.cn/xuans/Smail/utils/password" ) func ReadProtectedPassword() (string, error) { first, err := askPassword("请输入新的超级管理员密码") if err != nil { return "", err } second, err := askPassword("请再次输入新的超级管理员密码") if err != nil { clearPassword(&first) return "", err } if first == "" || first != second { clearPassword(&first) clearPassword(&second) return "", errors.New("密码为空或两次输入不一致") } clearPassword(&second) return first, nil } func askPassword(prompt string) (string, error) { value, err := readSystemdInput(prompt, "no") if err != nil { return "", err } if password.ValidateUserPassword(value) != nil { return "", errors.New("密码长度无效") } return value, nil } func ReadVisibleInput(prompt string) (string, error) { return ReadVisibleInputContext(context.Background(), prompt) } func ReadVisibleInputContext(ctx context.Context, prompt string) (string, error) { return readSystemdInputContext(ctx, prompt, "yes") } // ShowResult 使用当前 systemctl 的密码代理显示结果,避免提示只进入系统日志。 func ShowResult(message string) { _, _ = readSystemdInputContext(context.Background(), message+"\n按回车结束", "yes", "--timeout=30") } func readSystemdInput(prompt, echo string) (string, error) { return readSystemdInputContext(context.Background(), prompt, echo) } func readSystemdInputContext(ctx context.Context, prompt, echo string, options ...string) (string, error) { prompt = strings.TrimRight(prompt, ":: ") + ":" // systemd 代理把提示保存在单行配置中,用终端换行控制显示列表,避免配置被拆行。 prompt = strings.ReplaceAll(strings.ReplaceAll(prompt, "\r\n", "\n"), "\n", "\x1bE") args := append([]string{"--no-tty", "--echo=" + echo}, options...) command := exec.CommandContext(ctx, "systemd-ask-password", append(args, prompt)...) // 先允许代理清理请求文件,避免取消后继续弹出已经失效的输入提示。 command.Cancel = func() error { return command.Process.Signal(syscall.SIGTERM) } command.WaitDelay = time.Second command.Stderr = io.Discard output, err := command.Output() if err != nil { return "", errors.New("受保护密码代理不可用") } output = stripPasswordLineEnding(output) value := string(output) defer clearBytes(output) return value, nil } func stripPasswordLineEnding(value []byte) []byte { if len(value) > 0 && value[len(value)-1] == '\n' { value = value[:len(value)-1] } if len(value) > 0 && value[len(value)-1] == '\r' { value = value[:len(value)-1] } return value } func clearBytes(value []byte) { for index := range value { value[index] = 0 } }