Skip to content

feat(crypto): add password-based encryption (PasswordCipher)#20

Merged
tgiachi merged 6 commits into
developfrom
feature/crypto-pbe
Jun 30, 2026
Merged

feat(crypto): add password-based encryption (PasswordCipher)#20
tgiachi merged 6 commits into
developfrom
feature/crypto-pbe

Conversation

@tgiachi

@tgiachi tgiachi commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Summary

A static password-based encryption primitive in SquidStd.Crypto: PasswordCipher encrypts a payload under a password (Argon2id derives the key, AES-256-GCM seals it) and produces a self-describing, versioned, AAD-bound envelope — decryption needs only the password and the blob. Fills the gap between CryptoUtils (raw key) and ISecretProtector (app-key/KMS).

API

byte[] blob = PasswordCipher.Encrypt(data, "password");           // PbkdfCost.Moderate by default
byte[] back = PasswordCipher.Decrypt(blob, "password");
string s    = PasswordCipher.EncryptString("secret", "pw");        // base64 envelope
string clr  = PasswordCipher.DecryptString(s, "pw");
PasswordCipher.Encrypt(data, "pw", PbkdfCost.Sensitive);          // tune Argon2 cost

Wrong password or tampered data → PasswordDecryptionException.

Architecture

  • Pure format layer (PasswordEnvelope + PasswordEnvelopeCodec) — Terminal.Gui-free, unit-tested as plain data.
  • Argon2id derivation (PasswordKeyDerivation, BouncyCastle) — same params/version as the existing vault.
  • Orchestrator (PasswordCipher) — AES-256-GCM, random salt+nonce per call.
  • PbkdfCost presets (Interactive / Moderate / Sensitive) + custom.

Envelope SPBE v1

magic(4) | version(1) | iterations(4) | memoryKib(4) | parallelism(1) | salt(16) | nonce(12) | tag(16) | ciphertext
The 42 bytes before the tag are the GCM AAD, so version (no downgrade), KDF cost, salt and nonce are all authenticated.

Security notes

  • Salt (16) and nonce (12) are freshly random per Encrypt — no nonce reuse under a fixed key (the key also varies by salt).
  • The wrapped exception is generic (doesn't reveal wrong-password vs corruption); no AEAD padding oracle.
  • PbkdfCost rejects parallelism > 255 (the envelope byte field) to avoid silent truncation.

Test plan

  • Solution builds (0 errors)
  • Full suite green — 940 tests, 0 failures (23 new PBE tests: PbkdfCost 7, EnvelopeCodec 5, KeyDerivation 2, PasswordCipher 9)
  • Round-trip (bytes, string, empty plaintext, non-ASCII password), wrong-password, tampered-ciphertext, tampered-header(AAD), per-call randomness, cost presets

tgiachi added 6 commits June 30, 2026 14:32
…+ AES-GCM)

- Add PasswordCipher static class: Encrypt/Decrypt byte[], EncryptString/DecryptString (base64)
- Add PasswordDecryptionException (sealed, extends CryptographicException) for wrong password or tampered data
- AES-256-GCM with AAD-bound authentication; wrong password and ciphertext/header tampering all throw PasswordDecryptionException
- Encrypt produces unique blobs per call via random salt + nonce
- PbkdfCost defaults to Moderate when omitted
The envelope stores parallelism in a single byte, so a value above 255 would
truncate silently and produce an undecryptable blob (the key is derived with the
real lane count on encrypt but the envelope records the truncated one). Guard it
with ArgumentOutOfRangeException.ThrowIfGreaterThan up front. Add tests for the
guard plus empty-plaintext and non-ASCII-password round-trips.
@tgiachi
tgiachi merged commit 54ba732 into develop Jun 30, 2026
3 checks passed
@tgiachi
tgiachi deleted the feature/crypto-pbe branch June 30, 2026 12:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant