feat(auth): add safe Argon2id local user registry
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
package authconfig
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type passwordVector struct {
|
||||
Password string `json:"password"`
|
||||
SaltHex string `json:"saltHex"`
|
||||
MemoryKiB uint32 `json:"memoryKiB"`
|
||||
Passes uint32 `json:"passes"`
|
||||
Parallelism uint8 `json:"parallelism"`
|
||||
KeyLength uint32 `json:"keyLength"`
|
||||
PHC string `json:"phc"`
|
||||
}
|
||||
|
||||
func TestHashPasswordMatchesSharedFixedArgon2idVector(t *testing.T) {
|
||||
vector := loadPasswordVector(t)
|
||||
salt, err := hex.DecodeString(vector.SaltHex)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := HashPassword([]byte(vector.Password), bytes.NewReader(salt))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != vector.PHC {
|
||||
t.Fatalf("HashPassword() = %q, want shared PHC vector %q", got, vector.PHC)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyPasswordAcceptsOnlyTheMatchingPassword(t *testing.T) {
|
||||
vector := loadPasswordVector(t)
|
||||
if !VerifyPassword([]byte(vector.Password), vector.PHC) {
|
||||
t.Fatal("VerifyPassword() rejected the shared fixed vector")
|
||||
}
|
||||
if VerifyPassword([]byte(vector.Password+"!"), vector.PHC) {
|
||||
t.Fatal("VerifyPassword() accepted a one-byte password change")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyPasswordRejectsMalformedAndOversizedPHCBeforeHashing(t *testing.T) {
|
||||
password := []byte("correct horse battery staple")
|
||||
for name, encoded := range map[string]string{
|
||||
"wrong algorithm": "$argon2i$v=19$m=65536,t=3,p=1$AAECAwQFBgcICQoLDA0ODw$DRo8ZSPI8G5OCvnFFapbVEjP69aDjy1Sw9i2743cPC4",
|
||||
"wrong version": "$argon2id$v=18$m=65536,t=3,p=1$AAECAwQFBgcICQoLDA0ODw$DRo8ZSPI8G5OCvnFFapbVEjP69aDjy1Sw9i2743cPC4",
|
||||
"wrong parameter order": "$argon2id$v=19$t=3,m=65536,p=1$AAECAwQFBgcICQoLDA0ODw$DRo8ZSPI8G5OCvnFFapbVEjP69aDjy1Sw9i2743cPC4",
|
||||
"padded base64": "$argon2id$v=19$m=65536,t=3,p=1$AAECAwQFBgcICQoLDA0ODw==$DRo8ZSPI8G5OCvnFFapbVEjP69aDjy1Sw9i2743cPC4",
|
||||
"memory too large": "$argon2id$v=19$m=262145,t=1,p=1$AAECAwQFBgcICQoLDA0ODw$DRo8ZSPI8G5OCvnFFapbVEjP69aDjy1Sw9i2743cPC4",
|
||||
"passes too large": "$argon2id$v=19$m=65536,t=11,p=1$AAECAwQFBgcICQoLDA0ODw$DRo8ZSPI8G5OCvnFFapbVEjP69aDjy1Sw9i2743cPC4",
|
||||
"parallelism too large": "$argon2id$v=19$m=65536,t=3,p=5$AAECAwQFBgcICQoLDA0ODw$DRo8ZSPI8G5OCvnFFapbVEjP69aDjy1Sw9i2743cPC4",
|
||||
"short salt": "$argon2id$v=19$m=65536,t=3,p=1$AAECAwQFBgcICQoLDA0O$DRo8ZSPI8G5OCvnFFapbVEjP69aDjy1Sw9i2743cPC4",
|
||||
"long digest": "$argon2id$v=19$m=65536,t=3,p=1$AAECAwQFBgcICQoLDA0ODw$AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if VerifyPassword(password, encoded) {
|
||||
t.Fatal("VerifyPassword() accepted an invalid PHC string")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashPasswordRejectsPasswordsOutsideUTF8ByteBounds(t *testing.T) {
|
||||
for name, password := range map[string][]byte{
|
||||
"too short": bytes.Repeat([]byte("a"), 11),
|
||||
"too long": bytes.Repeat([]byte("a"), 1025),
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if _, err := HashPassword(password, bytes.NewReader(make([]byte, 16))); err == nil {
|
||||
t.Fatal("HashPassword() accepted an out-of-bounds password")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func loadPasswordVector(t *testing.T) passwordVector {
|
||||
t.Helper()
|
||||
_, testFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("locate password test fixture")
|
||||
}
|
||||
path := filepath.Join(filepath.Dir(testFile), "..", "..", "..", "..", "backend", "test", "fixtures", "argon2id-vectors.json")
|
||||
contents, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var vectors []passwordVector
|
||||
if err := json.Unmarshal(contents, &vectors); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(vectors) != 1 || vectors[0].Password == "" || strings.Contains(vectors[0].PHC, "\n") {
|
||||
t.Fatal("shared Argon2id fixture is invalid")
|
||||
}
|
||||
return vectors[0]
|
||||
}
|
||||
Reference in New Issue
Block a user