package vault import ( "crypto/ed25519" "errors" "fmt" "git.wntrmute.dev/kyle/mcias/internal/crypto" "git.wntrmute.dev/kyle/mcias/internal/db" ) // DeriveFromPassphrase derives the master encryption key from a passphrase // using the Argon2id KDF with a salt stored in the database. // // Security: The Argon2id parameters used by crypto.DeriveKey exceed OWASP 2023 // minimums (time=3, memory=128MiB, threads=4). The salt is 32 random bytes // stored in the database on first run. func DeriveFromPassphrase(passphrase string, database *db.DB) ([]byte, error) { salt, err := database.ReadMasterKeySalt() if errors.Is(err, db.ErrNotFound) { return nil, fmt.Errorf("no master key salt in database (first-run requires startup passphrase)") } if err != nil { return nil, fmt.Errorf("read master key salt: %w", err) } key, err := crypto.DeriveKey(passphrase, salt) if err != nil { return nil, fmt.Errorf("derive master key: %w", err) } return key, nil } // DecryptSigningKey decrypts the Ed25519 signing key pair from the database // using the provided master key. // // Security: The private key is stored AES-256-GCM encrypted in the database. // A fresh random nonce is used for each encryption. The plaintext key only // exists in memory during the process lifetime. func DecryptSigningKey(database *db.DB, masterKey []byte) (ed25519.PrivateKey, ed25519.PublicKey, error) { enc, nonce, err := database.ReadServerConfig() if err != nil { return nil, nil, fmt.Errorf("read server config: %w", err) } if enc == nil || nonce == nil { return nil, nil, fmt.Errorf("no signing key in database (first-run requires startup passphrase)") } privPEM, err := crypto.OpenAESGCM(masterKey, nonce, enc) if err != nil { return nil, nil, fmt.Errorf("decrypt signing key: %w", err) } priv, err := crypto.ParsePrivateKeyPEM(privPEM) if err != nil { return nil, nil, fmt.Errorf("parse signing key PEM: %w", err) } // Security: ed25519.PrivateKey.Public() always returns ed25519.PublicKey, // but we use the ok form to make the type assertion explicit and safe. pub, ok := priv.Public().(ed25519.PublicKey) if !ok { return nil, nil, fmt.Errorf("signing key has unexpected public key type") } return priv, pub, nil }