PgpMessageWriter.Write encrypts a stream to one or more OpenPGP certificates (public keys), and PgpMessageReader.Read decrypts it with the matching secret key. Both stream, so a file of any size goes through in constant memory. The program below makes a test key on its first run, encrypts a small file to it as an armored .asc, and decrypts it again.
dotnet new console -o PgpEncrypt && cd PgpEncrypt dotnet add package RecursiveAI.OneCrypto.OpenPgp --version 0.3.0-preview.2
OneCrypto is a preview, so dotnet add package needs --version or --prerelease. Put this in Program.cs and run dotnet run:
using System.Text; using RecursiveAI.OneCrypto.OpenPgp; DateTimeOffset now = DateTimeOffset.UtcNow; // The key's password: read it from your secret store byte[] password = Encoding.UTF8.GetBytes("demo password"); // A test key, made on the first run: version 4 Curve25519, a key GnuPG 2.4 can read if (!File.Exists("alice.key.asc")) { PgpCertificate created = PgpKeyGenerator.Generate(new PgpKeyGenerationOptions { Now = now, UserIds = ["Alice <[email protected]>"], Profile = PgpKeyProfile.Rfc4880Curve25519, Password = password, }); // The secret key stays private; the certificate (public key) is what you share File.WriteAllBytes("alice.key.asc", created.Export(armor: true)); File.WriteAllBytes("alice.pub.asc", created.ExportPublic(armor: true)); } PgpCertificate certificate = PgpCertificate.Parse(File.ReadAllBytes("alice.pub.asc")); PgpCertificate secretKey = PgpCertificate.Parse(File.ReadAllBytes("alice.key.asc")); File.WriteAllText("report.txt", "Q3 numbers: revenue up 4%, costs flat.\n"); // Encrypt the file to the certificate (anyone can do this with the public part) using (FileStream input = File.OpenRead("report.txt")) using (FileStream output = File.Create("report.txt.asc")) { PgpWriteResult written = PgpMessageWriter.Write(input, output, new PgpWriteOptions { Now = now, Recipients = [certificate], Armor = true, FileName = Encoding.UTF8.GetBytes("report.txt"), }); Console.WriteLine($"Encrypted report.txt.asc: {written.Format}, {written.Cipher}"); } // Decrypt with the secret key: report.txt.asc, or a file named on the command line string path = args.Length > 0 ? args[0] : "report.txt.asc"; using (FileStream input = File.OpenRead(path)) using (FileStream output = File.Create("report.decrypted.txt")) { PgpReadResult read = PgpMessageReader.Read(input, output, new PgpReadOptions { Now = now, SecretKeys = [secretKey], KeyPasswords = [password], }); Console.WriteLine($"Decrypted {path}: protection {read.Protection}, signatures {read.Signatures.Count}"); } Console.Write(File.ReadAllText("report.decrypted.txt"));
Encrypted report.txt.asc: Seipd1, Aes256 Decrypted report.txt.asc: protection Mdc, signatures 0 Q3 numbers: revenue up 4%, costs flat.
To encrypt for someone else, parse their certificate (their .asc public key) into Recipients; list several to encrypt one file for all of them, and add Passwords to let a password open it too. The writer picks the container from what the recipients’ keys announce: this version 4 key gets SEIPD version 1 (AES-256 with a modification detection code), which every OpenPGP tool reads; recipients that are all version 6 keys get SEIPD version 2 (AES-256 with OCB) instead. written.Reason says why.
On the reading side, Protection reports what guarded the plaintext (Mdc here; Aead for SEIPD version 2). Signatures is empty because the file was only encrypted: decryption is not verification, since anyone with the certificate can encrypt to it. To know who sent it, sign as well (Sign and verify with PGP) or set Signers on the writer.
Check it with GnuPG
GnuPG 2.4.8, in a throwaway home directory, imports the secret key and decrypts the program’s message. GNUPGHOME points gpg at a new, empty home for this terminal session, so nothing here touches your own keyring; the later gpg commands on this page use the same home.
export GNUPGHOME="$(mktemp -d)" gpg --batch --pinentry-mode loopback --passphrase 'demo password' --import alice.key.asc gpg --batch --pinentry-mode loopback --passphrase 'demo password' --decrypt report.txt.asc
gpg: keybox '/tmp/tmp.m8PlqfCuaD/pubring.kbx' created gpg: /tmp/tmp.m8PlqfCuaD/trustdb.gpg: trustdb created gpg: key CE3FAA9E94BC645F: public key "Alice <[email protected]>" imported gpg: key CE3FAA9E94BC645F: secret key imported gpg: Total number processed: 1 gpg: imported: 1 gpg: secret keys read: 1 gpg: secret keys imported: 1 gpg: encrypted with cv25519 key, ID A9C57FDDD7DF4F28, created 2026-11-02 "Alice <[email protected]>" Q3 numbers: revenue up 4%, costs flat.
And the other way: GnuPG encrypts a file to the same certificate, and the program decrypts it.
echo 'Sent from GnuPG 2.4.8.' > from-gpg.txt gpg --batch --trust-model always --armor --recipient [email protected] \ --output from-gpg.txt.asc --encrypt from-gpg.txt dotnet run -- from-gpg.txt.asc
Encrypted report.txt.asc: Seipd1, Aes256 Decrypted from-gpg.txt.asc: protection Mdc, signatures 0 Sent from GnuPG 2.4.8.
Sequoia’s sqop 0.38.0 does the same in both directions (password.txt holds the key’s password):
printf 'demo password' > password.txt sqop decrypt --with-key-password=password.txt alice.key.asc < report.txt.asc echo 'Sent from Sequoia.' | sqop encrypt alice.pub.asc > from-sqop.txt.asc dotnet run -- from-sqop.txt.asc
Q3 numbers: revenue up 4%, costs flat. Encrypted report.txt.asc: Seipd1, Aes256 Decrypted from-sqop.txt.asc: protection Mdc, signatures 0 Sent from Sequoia.
Which keys work with which tools
The test key is version 4 on purpose. GnuPG 2.4, the version most systems ship, reads neither version 6 keys nor the native Ed25519 algorithms: the project’s key matrix lists those cells as “not readable by GnuPG 2.4”, and GnuPG 2.4.8 answers a version 6 certificate with packet(6) with unknown version 6. Sequoia reads both. Pick the key by who has to read your messages:
| Key profile | Read by |
|---|---|
Version 4: Rfc4880Curve25519 (the default of Pgp.GenerateKey), Rfc4880Rsa, NistP256 | GnuPG 2.4 and Sequoia |
Version 4: Rfc9580V4 (native Ed25519) | Sequoia, not GnuPG 2.4 |
Version 6: Rfc9580 (the default of PgpKeyGenerator) | Sequoia, not GnuPG 2.4 |
The first row gets SEIPD version 1 messages; the other two announce SEIPD version 2 and get it.
Since 0.3.0-preview.2, Pgp.GenerateKey(userId, password) makes the first row’s Rfc4880Curve25519 key too. Generate a PGP key shows both versions and what each tool says about them.
Limits
- Not audited. OneCrypto (cryptography, not cryptocurrency) has not been independently audited. It is a preview: 0.3.0-preview.2, and the API can change.
- Legacy ciphers are decrypt-only. OneCrypto opens old messages in IDEA, TripleDES, CAST5 and Blowfish but never writes them: new messages use AES, and “a password message is never written with a legacy cipher”. Old messages with no integrity protection (Symmetrically Encrypted Data) are refused unless you set
AllowUnauthenticatedEncryption. - Act on the output only when
Readreturns. A SEIPD version 1 message is decrypted into a spool (memory, then an owner-only temporary file) and released only after its check passes. A SEIPD version 2 message releases each chunk after its tag verifies, so a damaged final chunk throws after earlier chunks were written: ifReadthrows, discard the output file, or useReadAsync, which writes only after the whole message checks out. - Compression on writing is ZIP or ZLIB, none by default; BZip2 is read only.
How messages are read and written, every algorithm and the interop runs: OpenPGP. From the command line: The onecrypto sop command line.