Crypto (PGP)
Generate a PGP key pair, persist it to disk, encrypt and sign a message, then reload the keyring from disk.
What you'll build
A host that registers the PGP keyring, service, and a file-backed key store
(SquidStd.Crypto.Pgp), generates a key, performs an encrypt-and-sign round-trip, and proves the
keys survive a restart by reloading them from the armored .asc files on disk.
Prerequisites
- .NET 10 SDK
dotnet add package SquidStd.Crypto
Steps
1. Register the PGP services and a file-backed key store
RegisterPgp wires the keyring and IPgpService; the factory chooses the key store - here a
FilePgpKeyStore that reads and writes armored .asc files in a directory.
// Register the PGP keyring, service, and a file-backed key store (armored .asc files).
bootstrap.ConfigureServices(container => container.RegisterCoreServices().RegisterPgp(_ => new FilePgpKeyStore(keyStoreDirectory)));
2. Generate a key and save it to disk
Generate a key pair, import it into the keyring so the service can resolve it by identity, then persist the keyring to the file-backed store.
const string identity = "alice@example.com";
const string passphrase = "correct horse battery staple";
var pgp = bootstrap.Resolve<IPgpService>();
var keyring = bootstrap.Resolve<IPgpKeyring>();
var keyStore = bootstrap.Resolve<IPgpKeyStore>();
// Generate a key pair and import it so the service can resolve it by identity. A freshly
// generated key always carries secret material, so PrivateArmored is non-null here.
var key = pgp.GenerateKey(identity, passphrase);
keyring.Import(key.PrivateArmored!);
// Persist the keyring to the file-backed store: this creates the directory and writes the
// armored .asc files to disk.
await keyring.SaveAsync(keyStore);
Console.WriteLine($"Generated key {key.KeyId} for {key.Identity}; saved to {keyStoreDirectory}");
3. Encrypt, sign, decrypt and verify
EncryptAndSignForAsync produces armored ciphertext for a recipient; DecryptAndVerifyAsync
returns the plaintext together with the signature status.
// Encrypt + sign for the recipient, then decrypt + verify the round-trip.
var armored = await pgp.EncryptAndSignForAsync(
identity,
Encoding.UTF8.GetBytes("attack at dawn"),
identity,
passphrase
);
var result = await pgp.DecryptAndVerifyAsync(armored, passphrase);
Console.WriteLine(
$"Decrypted: '{Encoding.UTF8.GetString(result.Data)}' (signed: {result.IsSigned}, valid: {result.IsValid})"
);
4. Reload the keyring from disk
A brand-new PgpKeyring loads the same keys back from the store, proving the on-disk material
survives a restart.
// Persistence round-trip: a brand-new keyring loads the same keys back from disk.
var reloaded = new PgpKeyring();
await reloaded.LoadAsync(keyStore);
Console.WriteLine($"Reloaded {reloaded.Keys.Count} key(s) from disk; contains '{identity}': {reloaded.Contains(identity)}");
Password-based encryption
PasswordCipher encrypts a payload directly under a password - no key management required. 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). Decryption needs only the password and the blob.
using SquidStd.Crypto.Password;
using SquidStd.Crypto.Password.Data;
// Bytes round-trip.
byte[] blob = PasswordCipher.Encrypt(payloadBytes, "correct horse battery staple");
byte[] back = PasswordCipher.Decrypt(blob, "correct horse battery staple");
// Text round-trip - the envelope is base64-encoded, safe to store in config or JSON.
string protectedText = PasswordCipher.EncryptString("a secret", "pw");
string clear = PasswordCipher.DecryptString(protectedText, "pw");
The cost of the Argon2id key derivation defaults to PbkdfCost.Moderate. Raise it when protecting
long-lived secrets:
// PbkdfCost.Sensitive - slower derivation, stronger resistance to offline attacks.
byte[] strong = PasswordCipher.Encrypt(payloadBytes, "pw", PbkdfCost.Sensitive);
A wrong password or tampered data raises PasswordDecryptionException. Use PasswordCipher for
user-supplied passwords; for app-key or KMS encryption use CryptoUtils / ISecretProtector
instead.
Run it
dotnet run --project samples/SquidStd.Samples.Crypto