Initial commit
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Assisted-by: GLM 5.3
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/text/unicode/norm"
|
||||
|
||||
"sourcedock.dev/petrbalvin/volumen/internal/session"
|
||||
)
|
||||
|
||||
// commonPasswords is the blocklist of trivially guessable passwords.
|
||||
// This is the policy every admin password change goes through.
|
||||
var commonPasswords = map[string]bool{
|
||||
"password": true, "password1": true, "password123": true,
|
||||
"123456": true, "12345678": true, "123456789": true,
|
||||
"qwerty": true, "qwerty123": true, "letmein": true, "iloveyou": true,
|
||||
"admin": true, "admin123": true, "welcome": true, "welcome1": true,
|
||||
"monkey": true, "dragon": true, "football": true, "baseball": true,
|
||||
"sunshine": true, "princess": true, "abc123": true, "111111": true,
|
||||
"123123": true, "1q2w3e4r": true, "passw0rd": true, "trustno1": true,
|
||||
"changeme": true, "secret": true, "secret123": true, "test": true,
|
||||
"test123": true, "guest": true, "master": true, "000000": true,
|
||||
"696969": true, "qwertyuiop": true, "superman": true, "batman": true,
|
||||
"jordan": true, "harley": true, "hunter": true, "hunter2": true,
|
||||
"shadow": true, "michael": true, "jennifer": true, "abcdef": true,
|
||||
"abcdefg": true,
|
||||
}
|
||||
|
||||
// PasswordError validates a newly chosen password and reports the
|
||||
// first problem as a catalogue key. The key is either a plain sentence or
|
||||
// the id of a plural message whose numeral n is the offending length;
|
||||
// "" with n 0 means accepted.
|
||||
func PasswordError(password string, minLength, maxLength int) (string, int) {
|
||||
if strings.TrimSpace(password) == "" {
|
||||
return "New password cannot be empty.", 0
|
||||
}
|
||||
length := len([]rune(password))
|
||||
if length < minLength {
|
||||
return "password.min", minLength
|
||||
}
|
||||
if length > maxLength {
|
||||
return "password.max", maxLength
|
||||
}
|
||||
normalized := strings.ToLower(norm.NFKC.String(password))
|
||||
if commonPasswords[normalized] {
|
||||
return "This password is too common.", 0
|
||||
}
|
||||
return "", 0
|
||||
}
|
||||
|
||||
// CSRFToken returns (and lazily creates) the CSRF token stored in the
|
||||
// session.
|
||||
func CSRFToken(sess *session.Session) string {
|
||||
token := sess.Get("csrf")
|
||||
if token == "" {
|
||||
token = newTokenHex(32)
|
||||
sess.Set("csrf", token)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
// sessionFingerprint derives the value the session carries to bind it to
|
||||
// one password: it changes whenever the account's hash changes, so a
|
||||
// password change or an admin reset retires every cookie issued before
|
||||
// it. It is a digest of the stored hash, never of the password, and
|
||||
// carries too few bits to help anyone invert the hash.
|
||||
func sessionFingerprint(storedHash string) string {
|
||||
sum := sha256.Sum256([]byte("volumen-session-v1:" + storedHash))
|
||||
return hex.EncodeToString(sum[:8])
|
||||
}
|
||||
|
||||
// ValidateCSRF compares the form's _csrf field against the session
|
||||
// token in constant time.
|
||||
func ValidateCSRF(r *http.Request, sess *session.Session) bool {
|
||||
token := r.PostFormValue("_csrf")
|
||||
sessionToken := sess.Get("csrf")
|
||||
if token == "" || sessionToken == "" {
|
||||
return false
|
||||
}
|
||||
return subtle.ConstantTimeCompare([]byte(sessionToken), []byte(token)) == 1
|
||||
}
|
||||
|
||||
func newTokenHex(nBytes int) string {
|
||||
buf := make([]byte, nBytes)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return ""
|
||||
}
|
||||
return hex.EncodeToString(buf)
|
||||
}
|
||||
|
||||
// firstUpper returns the uppercased first rune, or fallback.
|
||||
func firstUpper(s, fallback string) string {
|
||||
for _, r := range s {
|
||||
if unicode.IsSpace(r) {
|
||||
continue
|
||||
}
|
||||
return string(unicode.ToUpper(r))
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
Reference in New Issue
Block a user