Files
cairnquire/apps/server/internal/auth/crypto.go
Tim Bendt 04a1f2bb6d ops design and app
add more comments feature, more tags feature, and other frontend updates
2026-06-09 18:27:59 -04:00

176 lines
4.1 KiB
Go

package auth
import (
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"fmt"
"strconv"
"strings"
"golang.org/x/crypto/argon2"
)
const (
passwordTime uint32 = 3
passwordMemory uint32 = 64 * 1024
passwordThreads uint8 = 4
passwordKeyLen uint32 = 32
passwordSaltLen = 16
)
func randomBytes(length int) ([]byte, error) {
buf := make([]byte, length)
if _, err := rand.Read(buf); err != nil {
return nil, err
}
return buf, nil
}
func randomToken(length int) (string, error) {
buf, err := randomBytes(length)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(buf), nil
}
func randomHex(length int) (string, error) {
buf, err := randomBytes(length)
if err != nil {
return "", err
}
return hex.EncodeToString(buf), nil
}
func hashSecret(secret string) string {
sum := sha256.Sum256([]byte(secret))
return hex.EncodeToString(sum[:])
}
func constantTimeEqualHex(a, b string) bool {
if len(a) != len(b) {
return false
}
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
}
func hashPassword(password string) (string, error) {
salt, err := randomBytes(passwordSaltLen)
if err != nil {
return "", err
}
hash := argon2.IDKey([]byte(password), salt, passwordTime, passwordMemory, passwordThreads, passwordKeyLen)
return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s",
passwordMemory,
passwordTime,
passwordThreads,
base64.RawStdEncoding.EncodeToString(salt),
base64.RawStdEncoding.EncodeToString(hash),
), nil
}
func verifyPassword(password, encoded string) (bool, error) {
parts := strings.Split(encoded, "$")
if len(parts) != 6 || parts[1] != "argon2id" {
return false, fmt.Errorf("invalid password hash")
}
var memory uint64
var timeCost uint64
var threads uint64
for _, param := range strings.Split(parts[3], ",") {
keyValue := strings.SplitN(param, "=", 2)
if len(keyValue) != 2 {
return false, fmt.Errorf("invalid password hash parameters")
}
value, err := strconv.ParseUint(keyValue[1], 10, 32)
if err != nil {
return false, fmt.Errorf("parse password hash parameter: %w", err)
}
switch keyValue[0] {
case "m":
memory = value
case "t":
timeCost = value
case "p":
threads = value
}
}
if memory == 0 || timeCost == 0 || threads == 0 {
return false, fmt.Errorf("missing password hash parameters")
}
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
return false, fmt.Errorf("decode password salt: %w", err)
}
expected, err := base64.RawStdEncoding.DecodeString(parts[5])
if err != nil {
return false, fmt.Errorf("decode password hash: %w", err)
}
actual := argon2.IDKey([]byte(password), salt, uint32(timeCost), uint32(memory), uint8(threads), uint32(len(expected)))
return subtle.ConstantTimeCompare(actual, expected) == 1, nil
}
func normalizeScopes(scopes []Scope) []Scope {
if len(scopes) == 0 {
return []Scope{ScopeDocsRead, ScopeDocsWrite, ScopeSyncRead, ScopeSyncWrite}
}
seen := make(map[Scope]struct{}, len(scopes))
var normalized []Scope
for _, scope := range scopes {
switch scope {
case ScopeDocsRead, ScopeDocsWrite, ScopeSyncRead, ScopeSyncWrite, ScopeAdmin:
if _, ok := seen[scope]; !ok {
seen[scope] = struct{}{}
normalized = append(normalized, scope)
}
}
}
if len(normalized) == 0 {
return []Scope{ScopeDocsRead}
}
return normalized
}
func scopesToString(scopes []Scope) string {
normalized := normalizeScopes(scopes)
parts := make([]string, 0, len(normalized))
for _, scope := range normalized {
parts = append(parts, string(scope))
}
return strings.Join(parts, ",")
}
func scopesFromString(raw string) []Scope {
if raw == "" {
return nil
}
parts := strings.Split(raw, ",")
scopes := make([]Scope, 0, len(parts))
for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed != "" {
scopes = append(scopes, Scope(trimmed))
}
}
return normalizeScopes(scopes)
}
func hasScope(scopes []Scope, required Scope) bool {
for _, scope := range scopes {
if scope == ScopeAdmin || scope == required {
return true
}
}
return false
}
func HasScope(scopes []Scope, required Scope) bool {
return hasScope(scopes, required)
}