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", "weak bounded policy": "$argon2id$v=19$m=8,t=1,p=1$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", "noncanonical salt": "$argon2id$v=19$m=65536,t=3,p=1$AAECAwQFBgcICQoLDA0ODx$DRo8ZSPI8G5OCvnFFapbVEjP69aDjy1Sw9i2743cPC4", "noncanonical digest": "$argon2id$v=19$m=65536,t=3,p=1$AAECAwQFBgcICQoLDA0ODw$DRo8ZSPI8G5OCvnFFapbVEjP69aDjy1Sw9i2743cPC5", } { t.Run(name, func(t *testing.T) { if VerifyPassword(password, encoded) { t.Fatal("VerifyPassword() accepted an invalid PHC string") } if err := validatePasswordHash(encoded); err == nil { t.Fatal("validatePasswordHash() 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] }