Table of Contents

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

Next steps