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.
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:
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()}"); }
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:
openssl cms -decrypt -binary -inform DER -in report.p7m \ -recip alice.pem -inkey alice-key.pem
Q3 numbers: draft 2When 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
AuthEnvelopedDataholds the whole message in memory, up to 1 GiB by default (CmsLimits.MaxContentBytes). For larger files, split them into chunks, or useEnvelopedData, 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.