优化管理员重置验证输入显示
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-19 21:44:53 +08:00
parent b69043fc3a
commit b9d81bba3d
7 changed files with 32 additions and 22 deletions

View File

@ -43,7 +43,7 @@ func run(args []string) int {
return 1
}
defer conn.Close()
if err := adminreset.SendVerifiedReset(ctx, conn, adminreset.ReadProtectedInput, adminreset.ReadProtectedPassword); err != nil {
if err := adminreset.SendVerifiedReset(ctx, conn, adminreset.ReadVisibleInput, adminreset.ReadProtectedPassword); err != nil {
fmt.Fprintln(os.Stderr, "超级管理员重置失败:", safeError(err))
return 1
}

View File

@ -30,7 +30,7 @@ func ReadProtectedPassword() (string, error) {
}
func askPassword(prompt string) (string, error) {
value, err := ReadProtectedInput(prompt)
value, err := readSystemdInput(prompt, "no")
if err != nil {
return "", err
}
@ -40,8 +40,12 @@ func askPassword(prompt string) (string, error) {
return value, nil
}
func ReadProtectedInput(prompt string) (string, error) {
command := exec.Command("systemd-ask-password", "--no-tty", prompt)
func ReadVisibleInput(prompt string) (string, error) {
return readSystemdInput(prompt, "yes")
}
func readSystemdInput(prompt, echo string) (string, error) {
command := exec.Command("systemd-ask-password", "--no-tty", "--echo="+echo, prompt)
command.Stderr = io.Discard
output, err := command.Output()
if err != nil {

View File

@ -2,5 +2,5 @@
package adminreset
func ReadProtectedPassword() (string, error) { return "", ErrUnsupported }
func ReadProtectedInput(string) (string, error) { return "", ErrUnsupported }
func ReadProtectedPassword() (string, error) { return "", ErrUnsupported }
func ReadVisibleInput(string) (string, error) { return "", ErrUnsupported }

View File

@ -3,9 +3,11 @@
package adminreset
import (
"bufio"
"errors"
"fmt"
"os"
"strings"
"git.pcux.cn/xuans/Smail/utils/password"
@ -35,9 +37,18 @@ func ReadProtectedPassword() (string, error) {
}
func readWindowsPassword(prompt string) (string, error) {
value, err := ReadProtectedInput(prompt)
if !term.IsTerminal(int(os.Stdin.Fd())) {
return "", errors.New("受保护的密码输入需要交互式控制台")
}
fmt.Fprint(os.Stderr, prompt+": ")
valueBytes, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Fprintln(os.Stderr)
if err != nil {
return "", err
return "", errors.New("受保护的密码输入失败")
}
value := string(valueBytes)
for index := range valueBytes {
valueBytes[index] = 0
}
if password.ValidateUserPassword(value) != nil {
return "", errors.New("密码长度必须为 8 到 12 个字符")
@ -45,19 +56,14 @@ func readWindowsPassword(prompt string) (string, error) {
return value, nil
}
func ReadProtectedInput(prompt string) (string, error) {
func ReadVisibleInput(prompt string) (string, error) {
if !term.IsTerminal(int(os.Stdin.Fd())) {
return "", errors.New("安全验证需要交互式控制台")
}
fmt.Fprint(os.Stderr, prompt+": ")
value, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Fprintln(os.Stderr)
value, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
return "", errors.New("受保护的密码输入失败")
return "", errors.New("读取安全验证输入失败")
}
normalized := string(value)
for i := range value {
value[i] = 0
}
return normalized, nil
return strings.TrimSuffix(strings.TrimSuffix(value, "\n"), "\r"), nil
}

View File

@ -18,7 +18,7 @@ PrivateNetwork=true
PrivateDevices=true
ProtectHome=true
ProtectSystem=strict
# 密码代理只在本机临时目录收集无回显输入,不允许写入业务配置或数据库。
# 密码代理在本机临时目录收集交互输入;验证内容可见,新密码不回显,且均不写入业务配置或数据库。
ReadWritePaths=/run/systemd/ask-password
ProtectKernelTunables=true
ProtectKernelModules=true

View File

@ -106,7 +106,7 @@ flowchart LR
### 4.1 部署端超级管理员重置
1. 部署人员在受控主机上按正式部署命令启动 `smail-admin`,安全凭据与新密码从无回显交互读取,不通过命令行参数、环境变量、服务名或普通文件传递。
1. 部署人员在受控主机上按正式部署命令启动 `smail-admin`,验证方式和安全凭据使用可见交互输入,新密码使用无回显输入;这些内容均不通过命令行参数、环境变量、服务名或普通文件传递。
2. `smail-admin` 连接固定本机 IPC 地址:Linux 为 Unix Domain Socket,Windows 为 Named Pipe。
3. 主服务先校验对端身份,再签发一次性 nonce 和可用安全方式;客户端依次选择方式、验证邮箱验证码或 TOTP 或安全码,最后才提交新密码。选择邮箱前不发送验证码。
4. 主服务校验协议版本、单帧大小、字段集合、密码长度、nonce 一致性和 nonce 未过期/未消费。

View File

@ -20,10 +20,10 @@
systemctl start smail-admin
```
1. 输入可用验证方式的序号。仅显示已配置且有效的方式。
1. 输入可用验证方式的序号。仅显示已配置且有效的方式,输入的序号会正常显示。
2. 选择邮箱后才发送验证码;选择动态码或安全码不发邮件。
3. 输入邮箱验证码、手机验证器动态码或一个安全码。
4. 验证通过后,输入两次新密码。
3. 输入邮箱验证码、手机验证器动态码或一个安全码,验证内容会正常显示,便于核对。
4. 验证通过后,输入两次新密码;只有密码输入不显示。
5. 事务成功后旧会话失效,旧安全码和协议凭据撤销。已绑定邮箱和 TOTP 保留。
未绑定邮箱时可以使用 TOTP 或安全码。若唯一方式是安全码,重置后重新登录会要求再次配置安全方式。新安全码只展示一次,请妥善保存。