chore(canary): drop unused argon2 password machinery from core
internal/core/security.go was preserved through Phase 0's auth prune untouched. Audit: zero callers anywhere in the codebase, yet the file kept golang.org/x/crypto as a direct dep and ran a full Argon2id KDF on every process start via a package-level init() that pre-computed a dummy hash "to prevent timing attacks" — wasted boot CPU for an unused codepath. Deleted symbols: - HashPassword, VerifyPassword, VerifyPasswordWithRehash - VerifyPasswordTimingSafe + dummyHash init() - decodeHash, needsRehash internal helpers - GenerateSecureToken, GenerateRefreshToken - HashToken, CompareTokenHash go mod tidy demotes golang.org/x/crypto from direct to indirect (pgx/v5 still pulls it transitively). Surfaced by the pre-Phase-5 cross-phase alignment audit. Cleared as part of finishing Phase 0's auth prune rather than logged as deferred work. BACKLOG closed section records the item + resolution.
This commit is contained in:
parent
0e47274525
commit
31f1fff343
|
|
@ -22,7 +22,6 @@ require (
|
|||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0
|
||||
go.opentelemetry.io/otel/sdk v1.43.0
|
||||
go.opentelemetry.io/otel/trace v1.43.0
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/time v0.14.0
|
||||
google.golang.org/grpc v1.80.0
|
||||
)
|
||||
|
|
@ -91,6 +90,7 @@ require (
|
|||
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
golang.org/x/net v0.53.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
|
|
|
|||
|
|
@ -1,218 +0,0 @@
|
|||
// ©AngelaMos | 2026
|
||||
// security.go
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
const (
|
||||
argonTime = 1
|
||||
argonMemory = 64 * 1024
|
||||
argonThreads = 4
|
||||
argonKeyLen = 32
|
||||
saltLength = 16
|
||||
)
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
salt := make([]byte, saltLength)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return "", fmt.Errorf("generate salt: %w", err)
|
||||
}
|
||||
|
||||
hash := argon2.IDKey(
|
||||
[]byte(password),
|
||||
salt,
|
||||
argonTime,
|
||||
argonMemory,
|
||||
argonThreads,
|
||||
argonKeyLen,
|
||||
)
|
||||
|
||||
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
|
||||
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
|
||||
|
||||
encoded := fmt.Sprintf(
|
||||
"$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
||||
argon2.Version,
|
||||
argonMemory,
|
||||
argonTime,
|
||||
argonThreads,
|
||||
b64Salt,
|
||||
b64Hash,
|
||||
)
|
||||
|
||||
return encoded, nil
|
||||
}
|
||||
|
||||
func VerifyPassword(password, encodedHash string) (bool, error) {
|
||||
params, salt, hash, err := decodeHash(encodedHash)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
otherHash := argon2.IDKey(
|
||||
[]byte(password),
|
||||
salt,
|
||||
params.time,
|
||||
params.memory,
|
||||
params.threads,
|
||||
params.keyLen,
|
||||
)
|
||||
|
||||
if subtle.ConstantTimeCompare(hash, otherHash) == 1 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func VerifyPasswordWithRehash(
|
||||
password, encodedHash string,
|
||||
) (bool, string, error) {
|
||||
valid, err := VerifyPassword(password, encodedHash)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
if needsRehash(encodedHash) {
|
||||
newHash, hashErr := HashPassword(password)
|
||||
if hashErr != nil {
|
||||
//nolint:nilerr // password verified successfully; rehash failure is non-critical
|
||||
return true, "", nil
|
||||
}
|
||||
return true, newHash, nil
|
||||
}
|
||||
|
||||
return true, "", nil
|
||||
}
|
||||
|
||||
var dummyHash string
|
||||
|
||||
func init() {
|
||||
hash, err := HashPassword("dummy_password_for_timing_attack_prevention")
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("security: failed to generate dummy hash: %v", err))
|
||||
}
|
||||
dummyHash = hash
|
||||
}
|
||||
|
||||
func VerifyPasswordTimingSafe(
|
||||
password string,
|
||||
encodedHash *string,
|
||||
) (bool, string, error) {
|
||||
hashToVerify := dummyHash
|
||||
if encodedHash != nil && *encodedHash != "" {
|
||||
hashToVerify = *encodedHash
|
||||
}
|
||||
|
||||
valid, newHash, err := VerifyPasswordWithRehash(password, hashToVerify)
|
||||
|
||||
if encodedHash == nil || *encodedHash == "" {
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
return valid, newHash, err
|
||||
}
|
||||
|
||||
type argonParams struct {
|
||||
memory uint32
|
||||
time uint32
|
||||
threads uint8
|
||||
keyLen uint32
|
||||
}
|
||||
|
||||
func decodeHash(encodedHash string) (*argonParams, []byte, []byte, error) {
|
||||
parts := strings.Split(encodedHash, "$")
|
||||
if len(parts) != 6 {
|
||||
return nil, nil, nil, fmt.Errorf("invalid hash format")
|
||||
}
|
||||
|
||||
if parts[1] != "argon2id" {
|
||||
return nil, nil, nil, fmt.Errorf("unsupported algorithm: %s", parts[1])
|
||||
}
|
||||
|
||||
var version int
|
||||
_, err := fmt.Sscanf(parts[2], "v=%d", &version)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("invalid version: %w", err)
|
||||
}
|
||||
|
||||
if version != argon2.Version {
|
||||
return nil, nil, nil, fmt.Errorf("incompatible version: %d", version)
|
||||
}
|
||||
|
||||
params := &argonParams{}
|
||||
_, err = fmt.Sscanf(
|
||||
parts[3],
|
||||
"m=%d,t=%d,p=%d",
|
||||
¶ms.memory,
|
||||
¶ms.time,
|
||||
¶ms.threads,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("invalid params: %w", err)
|
||||
}
|
||||
|
||||
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("decode salt: %w", err)
|
||||
}
|
||||
|
||||
hash, err := base64.RawStdEncoding.DecodeString(parts[5])
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("decode hash: %w", err)
|
||||
}
|
||||
|
||||
//nolint:gosec // G115: hash length is always small (32 bytes for Argon2id)
|
||||
params.keyLen = uint32(len(hash))
|
||||
|
||||
return params, salt, hash, nil
|
||||
}
|
||||
|
||||
func needsRehash(encodedHash string) bool {
|
||||
params, _, _, err := decodeHash(encodedHash)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return params.memory != argonMemory ||
|
||||
params.time != argonTime ||
|
||||
params.threads != argonThreads ||
|
||||
params.keyLen != argonKeyLen
|
||||
}
|
||||
|
||||
func GenerateSecureToken(length int) (string, error) {
|
||||
bytes := make([]byte, length)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", fmt.Errorf("generate random bytes: %w", err)
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
func GenerateRefreshToken() (string, error) {
|
||||
return GenerateSecureToken(32)
|
||||
}
|
||||
|
||||
func HashToken(token string) string {
|
||||
hash := sha256.Sum256([]byte(token))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func CompareTokenHash(token, hash string) bool {
|
||||
tokenHash := HashToken(token)
|
||||
return subtle.ConstantTimeCompare([]byte(tokenHash), []byte(hash)) == 1
|
||||
}
|
||||
Loading…
Reference in New Issue