- Added `web/templates/{dashboard,audit,base,accounts,account_detail}.html` for a consistent UI.
- Implemented new audit log endpoint (`GET /v1/audit`) with filtering and pagination via `ListAuditEventsPaged`.
- Extended `AuditQueryParams`, added `AuditEventView` for joined actor/target usernames.
- Updated configuration (`goimports` preference), linting rules, and E2E tests.
- No logic changes to existing APIs.
263 lines
7.1 KiB
Go
263 lines
7.1 KiB
Go
package crypto
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ed25519"
|
|
"testing"
|
|
)
|
|
|
|
// TestGenerateEd25519KeyPair verifies that key generation returns valid,
|
|
// distinct keys and that the public key is derivable from the private key.
|
|
func TestGenerateEd25519KeyPair(t *testing.T) {
|
|
pub1, priv1, err := GenerateEd25519KeyPair()
|
|
if err != nil {
|
|
t.Fatalf("GenerateEd25519KeyPair: %v", err)
|
|
}
|
|
pub2, priv2, err := GenerateEd25519KeyPair()
|
|
if err != nil {
|
|
t.Fatalf("GenerateEd25519KeyPair second call: %v", err)
|
|
}
|
|
|
|
// Keys should be different across calls.
|
|
if bytes.Equal(priv1, priv2) {
|
|
t.Error("two calls produced identical private keys")
|
|
}
|
|
if bytes.Equal(pub1, pub2) {
|
|
t.Error("two calls produced identical public keys")
|
|
}
|
|
|
|
// Public key must be extractable from private key.
|
|
derived, ok := priv1.Public().(ed25519.PublicKey)
|
|
if !ok {
|
|
t.Fatal("priv1.Public() did not return ed25519.PublicKey")
|
|
}
|
|
if !bytes.Equal(derived, pub1) {
|
|
t.Error("public key derived from private key does not match generated public key")
|
|
}
|
|
}
|
|
|
|
// TestEd25519PEMRoundTrip verifies that a private key can be encoded to PEM
|
|
// and decoded back to the identical key.
|
|
func TestEd25519PEMRoundTrip(t *testing.T) {
|
|
_, priv, err := GenerateEd25519KeyPair()
|
|
if err != nil {
|
|
t.Fatalf("GenerateEd25519KeyPair: %v", err)
|
|
}
|
|
|
|
pem, err := MarshalPrivateKeyPEM(priv)
|
|
if err != nil {
|
|
t.Fatalf("MarshalPrivateKeyPEM: %v", err)
|
|
}
|
|
if len(pem) == 0 {
|
|
t.Fatal("MarshalPrivateKeyPEM returned empty PEM")
|
|
}
|
|
|
|
decoded, err := ParsePrivateKeyPEM(pem)
|
|
if err != nil {
|
|
t.Fatalf("ParsePrivateKeyPEM: %v", err)
|
|
}
|
|
if !bytes.Equal(priv, decoded) {
|
|
t.Error("decoded private key does not match original")
|
|
}
|
|
}
|
|
|
|
// TestParsePrivateKeyPEMErrors validates error cases.
|
|
func TestParsePrivateKeyPEMErrors(t *testing.T) {
|
|
// Empty input
|
|
if _, err := ParsePrivateKeyPEM([]byte{}); err == nil {
|
|
t.Error("expected error for empty PEM, got nil")
|
|
}
|
|
|
|
// Wrong PEM type (using a fake RSA block header)
|
|
fakePEM := []byte("-----BEGIN RSA PRIVATE KEY-----\nYWJj\n-----END RSA PRIVATE KEY-----\n")
|
|
if _, err := ParsePrivateKeyPEM(fakePEM); err == nil {
|
|
t.Error("expected error for wrong PEM type, got nil")
|
|
}
|
|
|
|
// Corrupt DER inside valid PEM block
|
|
corruptPEM := []byte("-----BEGIN PRIVATE KEY-----\nYWJj\n-----END PRIVATE KEY-----\n")
|
|
if _, err := ParsePrivateKeyPEM(corruptPEM); err == nil {
|
|
t.Error("expected error for corrupt DER, got nil")
|
|
}
|
|
}
|
|
|
|
// TestSealOpenAESGCMRoundTrip verifies that sealed data can be opened.
|
|
func TestSealOpenAESGCMRoundTrip(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
for i := range key {
|
|
key[i] = byte(i)
|
|
}
|
|
plaintext := []byte("hello world secret data")
|
|
|
|
ct, nonce, err := SealAESGCM(key, plaintext)
|
|
if err != nil {
|
|
t.Fatalf("SealAESGCM: %v", err)
|
|
}
|
|
if len(ct) == 0 || len(nonce) == 0 {
|
|
t.Fatal("SealAESGCM returned empty ciphertext or nonce")
|
|
}
|
|
|
|
got, err := OpenAESGCM(key, nonce, ct)
|
|
if err != nil {
|
|
t.Fatalf("OpenAESGCM: %v", err)
|
|
}
|
|
if !bytes.Equal(got, plaintext) {
|
|
t.Errorf("decrypted = %q, want %q", got, plaintext)
|
|
}
|
|
}
|
|
|
|
// TestSealNoncesAreUnique verifies that repeated seals produce different nonces.
|
|
func TestSealNoncesAreUnique(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
plaintext := []byte("same plaintext")
|
|
|
|
_, nonce1, err := SealAESGCM(key, plaintext)
|
|
if err != nil {
|
|
t.Fatalf("SealAESGCM (1): %v", err)
|
|
}
|
|
_, nonce2, err := SealAESGCM(key, plaintext)
|
|
if err != nil {
|
|
t.Fatalf("SealAESGCM (2): %v", err)
|
|
}
|
|
|
|
if bytes.Equal(nonce1, nonce2) {
|
|
t.Error("two seals of the same plaintext produced identical nonces — crypto/rand may be broken")
|
|
}
|
|
}
|
|
|
|
// TestOpenAESGCMWrongKey verifies that decryption with the wrong key fails.
|
|
func TestOpenAESGCMWrongKey(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
wrongKey := make([]byte, 32)
|
|
wrongKey[0] = 0xFF
|
|
|
|
ct, nonce, err := SealAESGCM(key, []byte("secret"))
|
|
if err != nil {
|
|
t.Fatalf("SealAESGCM: %v", err)
|
|
}
|
|
|
|
if _, err := OpenAESGCM(wrongKey, nonce, ct); err == nil {
|
|
t.Error("expected error when opening with wrong key, got nil")
|
|
}
|
|
}
|
|
|
|
// TestOpenAESGCMTamperedCiphertext verifies that tampering is detected.
|
|
func TestOpenAESGCMTamperedCiphertext(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
ct, nonce, err := SealAESGCM(key, []byte("secret"))
|
|
if err != nil {
|
|
t.Fatalf("SealAESGCM: %v", err)
|
|
}
|
|
|
|
// Flip one bit in the ciphertext.
|
|
ct[0] ^= 0x01
|
|
if _, err := OpenAESGCM(key, nonce, ct); err == nil {
|
|
t.Error("expected error for tampered ciphertext, got nil")
|
|
}
|
|
}
|
|
|
|
// TestOpenAESGCMWrongKeySize verifies that keys with wrong size are rejected.
|
|
func TestOpenAESGCMWrongKeySize(t *testing.T) {
|
|
if _, _, err := SealAESGCM([]byte("short"), []byte("data")); err == nil {
|
|
t.Error("expected error for short key in Seal, got nil")
|
|
}
|
|
if _, err := OpenAESGCM([]byte("short"), make([]byte, 12), []byte("data")); err == nil {
|
|
t.Error("expected error for short key in Open, got nil")
|
|
}
|
|
}
|
|
|
|
// TestDeriveKey verifies that DeriveKey produces consistent, non-empty output.
|
|
func TestDeriveKey(t *testing.T) {
|
|
salt, err := NewSalt()
|
|
if err != nil {
|
|
t.Fatalf("NewSalt: %v", err)
|
|
}
|
|
|
|
key1, err := DeriveKey("my-passphrase", salt)
|
|
if err != nil {
|
|
t.Fatalf("DeriveKey: %v", err)
|
|
}
|
|
if len(key1) != 32 {
|
|
t.Errorf("DeriveKey returned %d bytes, want 32", len(key1))
|
|
}
|
|
|
|
// Same inputs → same output (deterministic).
|
|
key2, err := DeriveKey("my-passphrase", salt)
|
|
if err != nil {
|
|
t.Fatalf("DeriveKey (2): %v", err)
|
|
}
|
|
if !bytes.Equal(key1, key2) {
|
|
t.Error("DeriveKey is not deterministic")
|
|
}
|
|
|
|
// Different passphrase → different key.
|
|
key3, err := DeriveKey("different-passphrase", salt)
|
|
if err != nil {
|
|
t.Fatalf("DeriveKey (3): %v", err)
|
|
}
|
|
if bytes.Equal(key1, key3) {
|
|
t.Error("different passphrases produced the same key")
|
|
}
|
|
|
|
// Different salt → different key.
|
|
salt2, err := NewSalt()
|
|
if err != nil {
|
|
t.Fatalf("NewSalt (2): %v", err)
|
|
}
|
|
key4, err := DeriveKey("my-passphrase", salt2)
|
|
if err != nil {
|
|
t.Fatalf("DeriveKey (4): %v", err)
|
|
}
|
|
if bytes.Equal(key1, key4) {
|
|
t.Error("different salts produced the same key")
|
|
}
|
|
}
|
|
|
|
// TestDeriveKeyErrors verifies invalid input rejection.
|
|
func TestDeriveKeyErrors(t *testing.T) {
|
|
// Short salt
|
|
if _, err := DeriveKey("passphrase", []byte("short")); err == nil {
|
|
t.Error("expected error for short salt, got nil")
|
|
}
|
|
|
|
// Empty passphrase
|
|
salt, _ := NewSalt()
|
|
if _, err := DeriveKey("", salt); err == nil {
|
|
t.Error("expected error for empty passphrase, got nil")
|
|
}
|
|
}
|
|
|
|
// TestNewSaltUniqueness verifies that two salts are different.
|
|
func TestNewSaltUniqueness(t *testing.T) {
|
|
s1, err := NewSalt()
|
|
if err != nil {
|
|
t.Fatalf("NewSalt (1): %v", err)
|
|
}
|
|
s2, err := NewSalt()
|
|
if err != nil {
|
|
t.Fatalf("NewSalt (2): %v", err)
|
|
}
|
|
if bytes.Equal(s1, s2) {
|
|
t.Error("two NewSalt calls returned identical salts")
|
|
}
|
|
}
|
|
|
|
// TestRandomBytes verifies length and uniqueness.
|
|
func TestRandomBytes(t *testing.T) {
|
|
b1, err := RandomBytes(32)
|
|
if err != nil {
|
|
t.Fatalf("RandomBytes: %v", err)
|
|
}
|
|
if len(b1) != 32 {
|
|
t.Errorf("RandomBytes returned %d bytes, want 32", len(b1))
|
|
}
|
|
|
|
b2, err := RandomBytes(32)
|
|
if err != nil {
|
|
t.Fatalf("RandomBytes (2): %v", err)
|
|
}
|
|
if bytes.Equal(b1, b2) {
|
|
t.Error("two RandomBytes calls returned identical values")
|
|
}
|
|
}
|