package secure import ( "encoding/base64" "os" "path/filepath" "strings" "testing" ) func TestCipherRoundTripAndFingerprint(t *testing.T) { t.Parallel() dir := t.TempDir() key := make([]byte, 32) for index := range key { key[index] = byte(index + 1) } path := filepath.Join(dir, "master-key") if err := os.WriteFile(path, []byte(base64.RawStdEncoding.EncodeToString(key)), 0o600); err != nil { t.Fatal(err) } cipher, err := LoadCipher(path) if err != nil { t.Fatal(err) } ciphertext, nonce, err := cipher.Encrypt("hetzner-token-value") if err != nil { t.Fatal(err) } if string(ciphertext) == "hetzner-token-value" { t.Fatal("token was not encrypted") } plaintext, err := cipher.Decrypt(ciphertext, nonce) if err != nil { t.Fatal(err) } if plaintext != "hetzner-token-value" { t.Fatalf("got %q", plaintext) } if cipher.Fingerprint("same") != cipher.Fingerprint("same") || cipher.Fingerprint("same") == cipher.Fingerprint("different") { t.Fatal("fingerprint must be deterministic and token-specific") } } func TestLoadOrCreateCipherPersistsSecureKey(t *testing.T) { t.Parallel() path := filepath.Join(t.TempDir(), "secrets", "master_key") first, err := LoadOrCreateCipher(path) if err != nil { t.Fatal(err) } info, err := os.Stat(path) if err != nil { t.Fatal(err) } if permissions := info.Mode().Perm(); permissions != 0o600 { t.Fatalf("master key permissions = %o, want 600", permissions) } ciphertext, nonce, err := first.Encrypt("persistent token") if err != nil { t.Fatal(err) } second, err := LoadOrCreateCipher(path) if err != nil { t.Fatal(err) } plaintext, err := second.Decrypt(ciphertext, nonce) if err != nil || plaintext != "persistent token" { t.Fatalf("created key was not reused: plaintext=%q err=%v", plaintext, err) } } func TestPasswordHash(t *testing.T) { t.Parallel() hash, err := HashPassword("a secure password") if err != nil { t.Fatal(err) } if !VerifyPassword(hash, "a secure password") { t.Fatal("expected password verification to succeed") } if VerifyPassword(hash, "wrong password") { t.Fatal("wrong password verified") } if VerifyPassword(strings.Replace(hash, "m=65536", "m=4294967295", 1), "a secure password") { t.Fatal("password hash with unsafe Argon2 parameters was accepted") } if _, err := HashPassword(strings.Repeat("a", PasswordMinLength-1)); err == nil { t.Fatal("short password was accepted") } if _, err := HashPassword(strings.Repeat("a", PasswordMaxLength+1)); err == nil { t.Fatal("overlong password was accepted") } } func TestPasswordTimingHashMatchesCurrentArgonParameters(t *testing.T) { t.Parallel() if !VerifyPassword(PasswordTimingHash, "login timing normalization only") { t.Fatal("precomputed login timing hash no longer matches the configured Argon2 parameters") } } func TestProvisionedPasswordHasNoMinimumLengthBeyondNonEmpty(t *testing.T) { t.Parallel() hash, err := HashProvisionedPassword("x") if err != nil || !VerifyPassword(hash, "x") { t.Fatalf("short provisioned password was not hashed correctly: %v", err) } if _, err := HashProvisionedPassword(""); err == nil { t.Fatal("empty provisioned password was accepted") } if _, err := HashProvisionedPassword(strings.Repeat("x", PasswordMaxLength+1)); err == nil { t.Fatal("overlong provisioned password was accepted") } }