Table of Contents

SquidStd.Crypto

OpenPGP key management and operations for SquidStd, built on PgpCore (a maintained MIT wrapper over BouncyCastle). Provides key generation, encrypt/decrypt, sign/verify, and the combined encrypt+sign / decrypt+verify flows over a stateful, indexed keyring with a pluggable persistence backend. SquidStd.Core stays dependency-free; this module is the home for higher-level crypto features, with PGP namespaced under SquidStd.Crypto.Pgp so future areas can coexist.

Install

dotnet add package SquidStd.Crypto

Usage

using DryIoc;
using SquidStd.Crypto.Pgp.Extensions;
using SquidStd.Crypto.Pgp.Interfaces;
using SquidStd.Crypto.Pgp.Services;

// Register the keyring, service, and a key store (all singletons).
container.RegisterPgp(_ => new FilePgpKeyStore("/var/lib/app/pgp"));
// or encrypted at rest with the app key:
// container.RegisterPgp(r => new AesGcmPgpKeyStore(r.Resolve<ISecretProtector>(), "/var/lib/app/pgp.bin"));

var keyring = container.Resolve<IPgpKeyring>();
var pgp = container.Resolve<IPgpService>();

// Generate a key and import it into the keyring.
var alice = pgp.GenerateKey("alice@example.com", "passphrase");
keyring.Import(alice.PrivateArmored!);
keyring.Import(bobPublicArmored); // a correspondent's public key

// Encrypt for a recipient, decrypt with the held secret key.
string armored = await pgp.EncryptForAsync("bob@example.com", payloadBytes);
byte[] plaintext = await pgp.DecryptAsync(armoredFromBob, "passphrase");

// Sign and verify (signed message - the data is embedded in the armored block).
string signed = await pgp.SignAsync(payloadBytes, "alice@example.com", "passphrase");
var verification = await pgp.VerifyAsync(signed);   // verification.IsValid, verification.Data

// Combined: encrypt + sign, then decrypt + verify.
string sealed = await pgp.EncryptAndSignForAsync("bob@example.com", payloadBytes, "alice@example.com", "passphrase");
var result = await pgp.DecryptAndVerifyAsync(sealedFromBob, "passphrase"); // result.Data, result.IsSigned, result.IsValid

// Persist / restore the keyring.
await keyring.SaveAsync(container.Resolve<IPgpKeyStore>());
await keyring.LoadAsync(container.Resolve<IPgpKeyStore>());

Password-based encryption

PasswordCipher encrypts a payload under a password - Argon2id derives the key, AES-256-GCM seals the data, and the result is a self-describing, versioned envelope (salt, nonce, tag and KDF cost are embedded, so decryption needs only the password and the blob).

using SquidStd.Crypto.Password;
using SquidStd.Crypto.Password.Data;

byte[] blob = PasswordCipher.Encrypt(payloadBytes, "correct horse battery staple");
byte[] back = PasswordCipher.Decrypt(blob, "correct horse battery staple");

// Text + base64 envelope for storing in config/JSON:
string protectedText = PasswordCipher.EncryptString("a secret", "pw");
string clear         = PasswordCipher.DecryptString(protectedText, "pw");

// Tune the Argon2id cost (defaults to PbkdfCost.Moderate):
byte[] strong = PasswordCipher.Encrypt(payloadBytes, "pw", PbkdfCost.Sensitive);

A wrong password or tampered data raises PasswordDecryptionException. For raw-key or app-key/KMS encryption use CryptoUtils / ISecretProtector instead.

Key types

Type Purpose
IPgpService Key generation, encrypt/decrypt, sign/verify, and combined encrypt+sign / decrypt+verify.
IPgpKeyring Stateful, indexed keyring: import keys and save/load via an IPgpKeyStore.
IPgpKeyStore Pluggable keyring persistence backend.
FilePgpKeyStore One armored .asc per key (gpg-interoperable).
AesGcmPgpKeyStore The whole keyring serialized to a single file, encrypted at rest via ISecretProtector.
CryptoFileSystem ILockableFileSystem that encrypts content and names over any IVirtualFileSystem.
PasswordCipher Password-based encryption: Argon2id + AES-256-GCM with a self-describing envelope.
PbkdfCost Argon2id cost presets (Interactive / Moderate / Sensitive) + custom.

Key stores

  • FilePgpKeyStore(directory) - one armored .asc per key (public, plus secret when held). gpg-interoperable.
  • AesGcmPgpKeyStore(ISecretProtector, path) - the whole keyring serialized to a single file, encrypted at rest with the application key via SquidStd's ISecretProtector.

Notes

  • Signatures are signed messages, not detached signatures: SignAsync embeds the data in the armored block and VerifyAsync recovers it. Verification is pass/fail - PgpCore does not expose the signer's key id or identity, so the results carry no signer attribution.
  • DecryptAndVerifyAsync never throws on a bad/absent signature when the ciphertext itself is valid: it always recovers Data and reports IsSigned / IsValid.
  • The streaming DecryptAsync(Stream, Stream, …) buffers its input internally so the recipient key id can be read before decrypting; the encrypt and sign stream paths flow straight through.
  • Passphrases are supplied per operation and never persisted (only passphrase-protected secret blocks are stored).

Crypto vault (encrypted virtual filesystem)

CryptoFileSystem is an ILockableFileSystem that decorates any IVirtualFileSystem (SquidStd.Vfs), encrypting file content and names. Crypto(Zip("vault.dat")) is a single-file encrypted vault you unlock with a passphrase, write to, and lock again.

using DryIoc;
using SquidStd.Crypto.Vfs.Extensions;
using SquidStd.Vfs.Abstractions.Interfaces;

// Single-file vault (crypto over a zip backend), registered as a singleton.
container.RegisterCryptoVault("/var/lib/app/vault.dat");

var vault = container.Resolve<ILockableFileSystem>();
vault.Unlock("my passphrase");          // derives the key (Argon2id)
await vault.WriteAllBytesAsync("docs/cv.pdf", bytes);
byte[]? back = await vault.ReadAllBytesAsync("docs/cv.pdf");
vault.Lock();                            // flushes, prunes, zeroes the key

Compose other backends directly:

using SquidStd.Crypto.Vfs.Services;
using SquidStd.Vfs.Services;

var folderVault = new CryptoFileSystem(new PhysicalFileSystem("/secure/dir"));

Notes

  • Unlock with a passphrase: a 256-bit key is derived with Argon2id (salt + cost params live in the cleartext header). Per-purpose subkeys are derived with HKDF-SHA256. The passphrase is never persisted.
  • Per-entry encryption: each file is stored as chunked AES-GCM (64 KiB chunks), so large files stream with bounded memory and tampering/truncation is detected.
  • Encrypted name index: logical paths and structure live in an encrypted index; backing entries use opaque ids, so a locked vault leaks neither file names nor layout.
  • Read-write, lockable: add/update/delete any time while unlocked; Lock()/Dispose() flush the encrypted index, prune orphaned blobs, and zero the key with CryptographicOperations.ZeroMemory.
  • A wrong passphrase fails the index authentication tag → CryptographicException; operations on a locked vault throw InvalidOperationException.

License

MIT - part of SquidStd.