---
title: "Encrypt and decrypt a file with PGP in C#"
url: https://recursiveai.org/onecrypto/cookbook/pgp-encrypt-decrypt-file/
description: "Encrypt and decrypt a file with PGP (OpenPGP) in C#: encrypt to a public key, decrypt with the password-protected secret key, and the same messages opened by GnuPG and Sequoia in both directions. Runnable code with OneCrypto 0.3.0-preview.2."
version: "0.3.0-preview.2"
---

# Encrypt and decrypt a file with PGP in C#

`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.

```shell
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`:

```csharp
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 <alice@example.org>"],
        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"));
```

```text
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](https://recursiveai.org/onecrypto/cookbook/pgp-sign-verify/)) 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.

```shell
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
```

```text
gpg: keybox '/tmp/tmp.m8PlqfCuaD/pubring.kbx' created
gpg: /tmp/tmp.m8PlqfCuaD/trustdb.gpg: trustdb created
gpg: key CE3FAA9E94BC645F: public key "Alice <alice@example.org>" 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 <alice@example.org>"
Q3 numbers: revenue up 4%, costs flat.
```

And the other way: GnuPG encrypts a file to the same certificate, and the program decrypts it.

```shell
echo 'Sent from GnuPG 2.4.8.' > from-gpg.txt
gpg --batch --trust-model always --armor --recipient alice@example.org \
  --output from-gpg.txt.asc --encrypt from-gpg.txt
dotnet run -- from-gpg.txt.asc
```

```text
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):

```shell
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
```

```text
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](https://recursiveai.org/onecrypto/cookbook/generate-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 `Read` returns.** 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: if `Read` throws, discard the output file, or use `ReadAsync`, 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](https://recursiveai.org/onecrypto/docs/openpgp/). From the command line: [The onecrypto sop command line](https://recursiveai.org/onecrypto/cookbook/onecrypto-sop-cli/).
