diff --git a/src/webui/internal/handlers/configure_users.go b/src/webui/internal/handlers/configure_users.go index 24975d84..03209a15 100644 --- a/src/webui/internal/handlers/configure_users.go +++ b/src/webui/internal/handlers/configure_users.go @@ -66,6 +66,8 @@ type cfgUsersPageData struct { Groups []cfgGroupDisplay Error string ShellOptions []schema.IdentityOption + CryptMethods []CryptMethod + Desc map[string]string // YANG field descriptions for hover tooltips } // ─── Handler ───────────────────────────────────────────────────────────────── @@ -101,7 +103,9 @@ func (h *ConfigureUsersHandler) Overview(w http.ResponseWriter, r *http.Request) data.Error = "Could not read user configuration" } } - const shellPath = "/ietf-system:system/authentication/user/infix-system:shell" + const userPath = "/ietf-system:system/authentication/user" + const shellPath = userPath + "/infix-system:shell" + data.CryptMethods = CryptMethods mgr := h.Schema.Manager() data.Loading = mgr == nil var defaultShell string @@ -113,6 +117,16 @@ func (h *ConfigureUsersHandler) Overview(w http.ResponseWriter, r *http.Request) break } } + data.Desc = map[string]string{ + "username": schema.DescriptionOf(mgr, userPath+"/name"), + "password": schema.DescriptionOf(mgr, userPath+"/password"), + "shell": schema.DescriptionOf(mgr, shellPath), + // No YANG leaf models the hash algorithm (it's a property of the + // stored crypt-hash); describe the offered set, verified against + // the infix-system:crypt-hash typedef. + "crypt": "Password hashing algorithm. yescrypt (recommended) is the strongest; " + + "SHA-512, SHA-256, and MD5 are offered for compatibility.", + } } for _, u := range raw.System.Auth.Users { // Effective shell: the user's explicit shell, else the YANG default. @@ -185,7 +199,7 @@ func (h *ConfigureUsersHandler) AddUser(w http.ResponseWriter, r *http.Request) return } - hash, err := HashPassword(password) + hash, err := HashPassword(password, r.FormValue("crypt")) if err != nil { log.Printf("configure users add: hash: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) @@ -259,7 +273,7 @@ func (h *ConfigureUsersHandler) ChangePassword(w http.ResponseWriter, r *http.Re return } - hash, err := HashPassword(password) + hash, err := HashPassword(password, r.FormValue("crypt")) if err != nil { log.Printf("configure users password %q: hash: %v", name, err) http.Error(w, "Internal server error", http.StatusInternalServerError) diff --git a/src/webui/internal/handlers/crypt.go b/src/webui/internal/handlers/crypt.go index 430ac96a..a9eef94d 100644 --- a/src/webui/internal/handlers/crypt.go +++ b/src/webui/internal/handlers/crypt.go @@ -8,15 +8,52 @@ import ( "strings" ) -// HashPassword returns a yescrypt crypt hash of password using mkpasswd(1). -// mkpasswd is available on Infix target systems and uses the system's libcrypt, -// so the output matches whatever the device expects (default: yescrypt $y$). -func HashPassword(password string) (string, error) { +// CryptMethod is a password-hashing algorithm offered in the UI. +type CryptMethod struct { + Value string // form value and mkpasswd --method argument + Label string + Default bool +} + +// CryptMethods is the single source of truth for the hashes the UI offers, +// ordered strongest first. Only the four the infix-system:crypt-hash YANG type +// accepts ($y$/$6$/$5$/$1$) are listed — mkpasswd supports more (bcrypt, +// scrypt, …), but the password leaf's pattern would reject those on save. +var CryptMethods = []CryptMethod{ + {"yescrypt", "yescrypt (recommended)", true}, + {"sha512crypt", "SHA-512", false}, + {"sha256crypt", "SHA-256", false}, + {"md5crypt", "MD5", false}, +} + +// normalizeCryptMethod returns method if it's one we offer, otherwise the +// default (the entry flagged Default). This guards an empty or tampered form +// value from reaching mkpasswd --method. +func normalizeCryptMethod(method string) string { + def := CryptMethods[0].Value + for _, m := range CryptMethods { + if m.Default { + def = m.Value + } + if m.Value == method { + return method + } + } + return def +} + +// HashPassword returns a crypt hash of password using mkpasswd(1) with the +// given method. mkpasswd is available on Infix target systems and uses the +// system's libcrypt. An empty or unrecognised method (e.g. one the YANG type +// would reject) falls back to the default, so callers can pass the raw form +// value safely. +func HashPassword(password, method string) (string, error) { + method = normalizeCryptMethod(method) path, err := exec.LookPath("mkpasswd") if err != nil { return "", fmt.Errorf("mkpasswd not found: %w", err) } - cmd := exec.Command(path, "--method=yescrypt", "--password-fd=0") + cmd := exec.Command(path, "--method="+method, "--password-fd=0") cmd.Stdin = strings.NewReader(password) out, err := cmd.Output() if err != nil { diff --git a/src/webui/templates/pages/configure-users.html b/src/webui/templates/pages/configure-users.html index d0decf22..6dfccd4b 100644 --- a/src/webui/templates/pages/configure-users.html +++ b/src/webui/templates/pages/configure-users.html @@ -74,15 +74,32 @@ - {{/* Change password */}} -
+ {{/* Change password. type=text + CSS mask (cfg-input-mask) + dodges the browser password-manager save prompt, same as + the keystore secret fields. */}} +
Change Password
- - + + + + + +
New passwordNew password{{template "field-info" (index $.Desc "password")}} + + + + +
Hash{{template "field-info" (index $.Desc "crypt")}}