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:
CarterPerez-dev 2026-05-13 13:28:17 -04:00
parent 0e47274525
commit 31f1fff343
2 changed files with 1 additions and 219 deletions

View File

@ -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

View File

@ -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",
&params.memory,
&params.time,
&params.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
}