Recursive AI

Encrypt and decrypt with CMS in C#

All onecrypto recipes

AuthEnvelopedData.Create encrypts data for the holder of a certificate, in the CMS format that S/MIME mail and .p7m files use; Decrypt opens it with the private key. The content is encrypted with AES-256-GCM, which detects any change, and the AES key is wrapped with RSA-OAEP for the recipient. The program below encrypts for a certificate made by OpenSSL and also opens a file OpenSSL encrypted.

Terminal
dotnet new console -o CmsEncrypt && cd CmsEncrypt
dotnet add package RecursiveAI.OneCrypto --version 0.1.0-preview.1
openssl req -x509 -newkey rsa:3072 -nodes -days 365 -subj "/CN=Alice" \
  -keyout alice-key.pem -out alice.pem
echo "Q3 numbers: draft 2" > report.txt
openssl cms -encrypt -binary -aes-256-gcm -recip alice.pem \
  -keyopt rsa_padding_mode:oaep -in report.txt -outform DER -out from-openssl.p7m

OneCrypto is a preview, so dotnet add package needs --version or --prerelease. Put this in Program.cs and run dotnet run:

C#
using System.Text;
using RecursiveAI.OneCrypto;
 
using LoadedItems certFile = Crypto.Load("alice.pem");
Certificate alice = certFile.Certificates[0];
 
// Encrypt for Alice: AES-256-GCM content, AES key wrapped with RSA-OAEP (SHA-256)
byte[] plaintext = File.ReadAllBytes("report.txt");
AuthEnvelopedData message = AuthEnvelopedData.Create(plaintext,
    [CmsRecipient.KeyTransport(alice)]);
File.WriteAllBytes("report.p7m", message.Encoded.ToArray());
Console.WriteLine($"Encrypted report.p7m: {message.Encoded.Length} bytes, " +
    $"{message.RecipientInfos.Count} recipient");
 
// Decrypt with Alice's private key and certificate
using LoadedItems keyFile = Crypto.Load("alice-key.pem");
using CmsDecryptionKey key = CmsDecryptionKey.FromKey(
    keyFile.PrivateKeys[0].Decryption!, alice);
foreach (string path in new[] { "report.p7m", "from-openssl.p7m" })
{
    byte[] opened = AuthEnvelopedData.Parse(File.ReadAllBytes(path)).Decrypt(key);
    Console.WriteLine($"{path,-17} {Encoding.UTF8.GetString(opened).Trim()}");
}
Text
Encrypted report.p7m: 616 bytes, 1 recipient
report.p7m        Q3 numbers: draft 2
from-openssl.p7m  Q3 numbers: draft 2

Add more CmsRecipient entries to encrypt one message for several people; each opens it with their own key. EC certificates use CmsRecipient.KeyAgreement, and CmsRecipient.Password encrypts with a password instead of a certificate. EncryptStream and DecryptStream take streams.

Check it with OpenSSL

OpenSSL decrypts the file OneCrypto wrote:

Terminal
openssl cms -decrypt -binary -inform DER -in report.p7m \
  -recip alice.pem -inkey alice-key.pem
Text
Q3 numbers: draft 2

When decryption fails

Decryption fails closed: no plaintext is returned until the whole message is decrypted and the GCM tag has verified. A wrong key, a damaged key or a changed byte all raise the same CmsParseException with CmsError.DecryptionFailed; only “no recipient in this message is meant for this key” is told apart.

Limits

  • Not audited. This library has not been independently audited. It is a preview: 0.1.0-preview.1, and the API can change.
  • Memory. AES-GCM is one-shot on .NET, so AuthEnvelopedData holds the whole message in memory, up to 1 GiB by default (CmsLimits.MaxContentBytes). For larger files, split them into chunks, or use EnvelopedData, which streams with AES-CBC but cannot detect every change: the project recommends AuthEnvelopedData where authenticity matters.
  • No sender identity. Encryption says nothing about who sent the file. Sign it too if that matters: Sign a file with CMS.
  • Readers. AuthEnvelopedData (RFC 5083) is newer than EnvelopedData; OpenSSL 3 reads it, but check older S/MIME clients before relying on it.

Every recipient type, limit and the OpenSSL test matrix: Cryptographic Message Syntax.