GitHub: source, binaries

Nuget: Inferno

Copyright © Stan Drapkin sdrapkin

Introduction

While many developers are aware enough not to roll their own crypto, they either pick the wrong approach, screw up the implementation, or both. I've written the SecurityDriven.NET book to highlight many challenges, misperceptions, and false assumptions of producing secure, implementationally correct .NET solutions. However, while recognizing the pitfalls of .NET cryptography is certainly useful, most of you would feel a lot more comfortable using an existing .NET library for common crypto needs rather than creating a risky ad hoc implementation. I know I would. Unfortunately, most .NET crypto libraries are awful. Many of these libraries focus on providing as many crypto primitives as possible, which is a huge disservice. For example, if you follow “Internet advice”, you are likely to come across the Bouncy Castle c# library (a typical StackOverflow recommendation). Bouncy Castle c# is a huge (145k LOC ), poorly-performing museum catalogue of crypto (some of it ancient), with old Java implementations ported to equally-old .NET (2.0?). If you have a crypto archaeology itch, Bouncy Castle will scratch it. However, for typical practical purposes a new, modern, trusted, general-purpose .NET crypto library is required. Inferno library is .NET crypto done right. How do you build trust in a crypto library? Trust takes time, but keeping the codebase clean, small (<1k LOC ), well-tested, open, and deferring critical pieces to existing time-tested & trust-worthy implementations certainly helps.

Inferno has been in production since 2015, and has also been professionally audited.

Approach

Inferno has the following design goals: .NET crypto done right .

. Free, open source (MIT license).

Developer-friendly, misuse-resistant API.

Safe by design: safe algorithms, safe modes, safe choices.

Does not re-implement crypto primitives.

Uses FIPS-certified implementations where possible.

100% managed modern c# 7.3.

Performance-oriented (within reason - unsafe code is not a reason).

Minimal codebase and dependencies.

High maintainability & introspectability (easy security audits).

Unit testing, fuzz testing.

Streaming authenticated encryption (secure channels).

(secure channels). Symmetric crypto: AEAD only.

Asymmetric crypto: NSA/CNSA Suite B API only (Elliptic Curves). No RSA.

Decent documentation & code examples.

Features

[random]: CryptoRandom (.NET Random done right).

[ciphers]: AES-256 only (fast, constant-time, side-channel-resistant AES-NI).

[hi-level]: AEAD (AES-CTR-HMAC). Streaming AEAD (EtM Transforms).

[ciphers-misc]: AES-CTR implementation (CryptoTransform).

[ciphers-misc]: AEAD (AES-CBC-HMAC).

[hash]: SHA2 hash factories (256, 384, 512). SHA-384 is recommended (default).

(default). [hash]: SHA1 hash factory (mostly for legacy integration).

[mac]: HMAC2 (.NET HMAC done right).

[mac]: HMAC-SHA1, HMAC-SHA2 factories.

[kdf]: HKDF, PBKDF2, SP800_108_Ctr. Any HMAC factory is supported.

[otp]: TOTP.

[helpers]: Constant-time byte & string comparison. Safe UTF8. Fast 64-bit byte-array Xor.

[extensions]: string-to-byte and byte-to-string (de-)serialization done right.

[extensions]: Fast Base16 & Base32 encodings with custom alphabets. Fast Base64 and Base64Url encoding.

[extensions]: CngKey extensions. ECDSA (signature). DHM (key exchange).

High-level API

Encryption public static class SuiteB // namespace SecurityDriven.Inferno { public static byte[] Encrypt(byte[] masterKey, ArraySegment<byte> plaintext, ArraySegment<byte>? salt = null) public static byte[] Decrypt(byte[] masterKey, ArraySegment<byte> ciphertext, ArraySegment<byte>? salt = null) public static bool Authenticate(byte[] masterKey, ArraySegment<byte> ciphertext, ArraySegment<byte>? salt = null) } Encrypt / Decrypt should do what you expect. Any failure causes NULL to be returned.

Authenticate is similar to Decrypt but only verifies authenticity of ciphertext without decrypting it (and thus is faster than Decrypt ). The "salt" parameter can include Additional Data (AD) or its hash - which will also be authenticated. Hash public static Func<SHA384> HashFactory Don't forget to Dispose after use. The particular choice of SHA384 is explained in "Implementation Details" section. HMAC // example: var data = Utils.SafeUTF8.GetBytes("Inferno"); using (var hmac = SuiteB.HmacFactory()) // HMACSHA384 { hmac.Key = new byte[] { 1, 2, 3, 4, 5 }; hmac.ComputeHash(data).ToBase16().Dump(); } // output: // AD0E5CA84DA23AE4A0537FC9008CB6AF91E16CA8429098B099E5066D30CBE0DA34ABCE4D71A02FB09786A53B523492A3 Generates HMACSHA384 instance. Don't forget to Dispose after use.

CryptoRandom

public class CryptoRandom : Random // implements all Random methods, as well as: public byte[] NextBytes(int count) public long NextLong() public long NextLong(long maxValue) public long NextLong(long minValue, long maxValue) CryptoRandom generates cryptographically-strong random values. Unlike Random , all methods of CryptoRandom are thread-safe. CryptoRandom descends from Random and thus should be used instead (in most cases). CryptoRandom will be faster than RNGCryptoServiceProvider in most scenarios, making it the preferred choice for all your strong-randomness needs. A good coverage of serious flaws in Random class can be found in SecurityDriven.NET book.

KDF

An in-depth discussion of which KDF to use when can be found in SecurityDriven.NET book.

HKDF public class HKDF : DeriveBytes public HKDF(Func<HMAC> hmacFactory, byte[] ikm, byte[] salt = null, byte[] context = null) HKDF class implements RFC 5869 as well as DeriveBytes interface. The constructor takes an HMAC factory, initial key material (ie. the secret key), and optional salt and context. Key derivation is obtained by calling GetBytes() method. Don't forget to Dispose. PBKDF2 public class PBKDF2 : DeriveBytes PBKDF2 class reimplements Rfc2898DeriveBytes and provides DeriveBytes interface. One of the major flaws of Rfc2898DeriveBytes (but not the only one) is inability to use different hash functions, which PBKDF2 corrects (see SecurityDriven.NET for more in-depth coverage). The various constructors mirror the Rfc2898DeriveBytes constructors, with the added HMAC factory parameter. Key derivation is obtained by calling GetBytes() method. Don't forget to Dispose. SP800_108_Ctr public static class SP800_108_Ctr public static void DeriveKey(Func<HMAC> hmacFactory, byte[] key, ArraySegment<byte>? label, ArraySegment<byte>? context, ArraySegment<byte> derivedOutput, uint counter = 1) SP_800_108_Ctr implements NIST SP800-108 Counter-mode KDF. It is a counter-based (as opposed to iterating) mode, so it is parallelizable and is implemented as a static method (does not follow DeriveBytes API).

AEAD Transform (Streaming)

public class EtM_EncryptTransform : ICryptoTransform public EtM_EncryptTransform(byte[] key, ArraySegment<byte>? salt = null, uint chunkNumber = 1) //ctor public class EtM_DecryptTransform : ICryptoTransform public EtM_DecryptTransform(byte[] key, ArraySegment<byte>? salt = null, uint chunkNumber = 1, bool authenticateOnly = false) //ctor

The EtM Encrypt/Decrypt transforms implement chunked authenticated encryption, and can wrap any .NET stream. Each chunk is independently keyed and authenticated. The "salt" parameter can include Additional Data (AD) or its hash - which will also be authenticated.

Here is a simple example of EtM stream encryption/decryption/authentication:

static CryptoRandom random = new CryptoRandom(); static byte[] key = random.NextBytes(32); static string originalFilename = @"c:\Test.pdf"; static string encryptedFilename = originalFilename + ".enc" + Path.GetExtension(originalFilename); static string decryptedFilename = originalFilename + ".dec" + Path.GetExtension(originalFilename); public static void Encrypt() { using (var originalStream = new FileStream(originalFilename, FileMode.Open)) using (var encryptedStream = new FileStream(encryptedFilename, FileMode.Create)) using (var encTransform = new EtM_EncryptTransform(key: key)) using (var cryptoStream = new CryptoStream(encryptedStream, encTransform, CryptoStreamMode.Write)) { originalStream.CopyTo(cryptoStream); } } public static void Decrypt() { using (var encryptedStream = new FileStream(encryptedFilename, FileMode.Open)) using (var decryptedStream = new FileStream(decryptedFilename, FileMode.Create)) using (var decTransform = new EtM_DecryptTransform(key: key)) { using (var cryptoStream = new CryptoStream(encryptedStream, decTransform, CryptoStreamMode.Read)) cryptoStream.CopyTo(decryptedStream); if (!decTransform.IsComplete) throw new Exception("Not all blocks are decrypted."); } } public static void Authenticate() { using (var encryptedStream = new FileStream(encryptedFilename, FileMode.Open)) using (var decTransform = new EtM_DecryptTransform(key: key, authenticateOnly: true)) { using (var cryptoStream = new CryptoStream(encryptedStream, decTransform, CryptoStreamMode.Read)) cryptoStream.CopyTo(Stream.Null); if (!decTransform.IsComplete) throw new Exception("Not all blocks are authenticated."); } }

Note that the decryption transform does not automatically throw if the transform is not complete (ie. there are missing chunks when transform is closed/disposed): an explicit "IsComplete" flag provides that information.

DSA Signatures

CngKey dsaKeyPrivate = CngKeyExtensions.CreateNewDsaKey(); // generate DSA keys byte[] dsaKeyPrivateBlob = dsaKeyPrivate.GetPrivateBlob(); // export private key as bytes byte[] dsaKeyPublicBlob = dsaKeyPrivate.GetPublicBlob(); // export public key as bytes CngKey dsaKeyPublic = dsaKeyPublicBlob.ToPublicKeyFromBlob(); // convert public key into CngKey byte[] data = Guid.NewGuid().ToByteArray(); // sample data byte[] signature = null; using (var ecdsa = new ECDsaCng(dsaKeyPrivate) { HashAlgorithm = CngAlgorithm.SHA384 }) // generate DSA signature with private key { signature = ecdsa.SignData(data); } data[5] ^= 1; // mess with the data using (var ecdsa = new ECDsaCng(dsaKeyPublic) { HashAlgorithm = CngAlgorithm.SHA384 }) // verify DSA signature with public key { if (ecdsa.VerifyData(data, signature)) Console.WriteLine("Signature verified."); else Console.WriteLine("Signature verification failed."); }

DHM Key Exchange

var keyA = CngKey.Open("keyA"); // create with: CngKeyExtensions.CreateNewDhmKey var keyB = CngKey.Open("keyB"); // create with: CngKeyExtensions.CreateNewDhmKey var staticSharedSecret = keyA.GetSharedDhmSecret(publicDhmKey: keyB); staticSharedSecret.ToB64().Dump("staticSharedSecret"); // static for a given {private key A, public key B} pair var ephemeralBundle = keyB.GetSharedEphemeralDhmSecret(); // must be communicated to B so that B can also derive the ephemeral shared secret ephemeralBundle.EphemeralDhmPublicKeyBlob.ToB64().Dump("ephemeral public DHM key"); // the ephemeral private key is forgotten by A ephemeralBundle.SharedSecret.ToB64().Dump("ephemeralSharedSecret"); /* SAMPLE OUTPUT: staticSharedSecret (48 bytes): wjlD6GTqj3xoPF0llOezb5_LdqQSKlRGurpr6a8tV985twcuOz5LmSDiuyBkzMbl0 ephemeral public DHM key (104 bytes): RUNLMzAAAAAOYH0a-XwvNDeTNq-kCs_MnS9TDQPoHebgHV9tAM4GjHhsOQLpBNLZhCql2V-HE5LTVdA9t4ye_6zXmjJu_nCjY1mf61S2T2j2CPuzIJtHTlUvyaDlgXcznNJ9P2Ci-uY1 ephemeralSharedSecret (48 bytes): S1pbq7bkrGucUC51IswbsIql3SicScTzwASr_UII1Af3vY_YvV8iRi7SM4P4VKvW0 */

ECIES example

Elliptic Curve Integrated Encryption Scheme (ECIES):

var keyA = CngKey.Open("keyA"); // create with: CngKeyExtensions.CreateNewDhmKey var keyB = CngKey.Open("keyB"); // create with: CngKeyExtensions.CreateNewDhmKey // sender A creates an ephemeral {public-key, DHM secret} pair against B's public key and encrypts: var ephemeralBundle = keyB.GetSharedEphemeralDhmSecret(); ephemeralBundle.EphemeralDhmPublicKeyBlob.ToB64().Dump("ephemeral public DHM key"); // must be communicated to B so that B can also derive the ephemeral shared secret ephemeralBundle.SharedSecret.ToB64().Dump("ephemeralSharedSecret"); // the ephemeral private key is forgotten var secretMessage = new ArraySegment<byte>("There is no spoon.".ToBytes()); var ciphertext = SuiteB.Encrypt(masterKey: ephemeralBundle.SharedSecret, plaintext: secretMessage); ciphertext.ToB64().Dump("ciphertext"); // receiver B re-derives the ephemeral shared secret and decrypts: var sharedSecret = keyB.GetSharedDhmSecret(publicDhmKey: ephemeralBundle.EphemeralDhmPublicKeyBlob.ToPublicKeyFromBlob()); var decrypted = SuiteB.Decrypt(masterKey: sharedSecret, ciphertext: new ArraySegment<byte>(ciphertext)); decrypted.FromBytes().Dump();

Miscellaneous

Safe UTF8 var bytes1 = new byte[] {200,201,202,203}; var bytes2 = new byte[] {204,205,206,207}; var s1 = Encoding.UTF8.GetString(bytes1); var s2 = Encoding.UTF8.GetString(bytes2); (s1 == s2).Dump(); // returns "True", which is a problem var s3 = Utils.SafeUTF8.GetString(bytes1); // will throw var s4 = Utils.SafeUTF8.GetString(bytes2); The built-in .NET "Encoding.UTF8" encoding instance does not raise exceptions when asked to encode bytes which cannot represent a valid encoding, and returns the same 'unknown' character instead. This creates a potential security vulnerability, due to likely entropy loss when converting from bytes to strings. The SafeUTF8 encoding instance will instead throw on any invalid byte sequence. For an alternative approach to preventing entropy loss without exceptions see "String serialization" section. Constant-time Equality static bool ConstantTimeEqual(byte[] x, int xOffset, byte[] y, int yOffset, int length) static bool ConstantTimeEqual(ArraySegment<byte> x, ArraySegment<byte> y) static bool ConstantTimeEqual(byte[] x, byte[] y) static bool ConstantTimeEqual(string x, string y) Base16, Base32, B64 var bytes = Guid.NewGuid().ToByteArray().Take(15).ToArray(); bytes.ToBase16().Dump(); bytes.ToBase16(config: Base16Config.HexLowercase).Dump(); bytes.ToBase32().Dump(); bytes.ToBase32(config: Base32Config.Rfc).Dump(); bytes.ToB64().Dump(); /* SAMPLE OUTPUT: 1B62A20832A0FF489AAC817EEBD992 1b62a20832a0ff489aac817eebd992 4EJB532KN4ZNJ7PDH6ZFRQDK DNRKECBSUD7URGVMQF7OXWMS G2KiCDKg_0iarIF-69mS0 */ Custom alphabets are supported for Base16 and Base32 (your own 'config' instances). String serialization string text1 = "abcd"; byte [] bytes = text1.ToBytes(); string text2 = bytes.FromBytes(); bytes.Dump(); text2.Dump(); /* OUTPUT: 8 bytes: [97, 0, 98, 0, 99, 0, 100, 0] abcd */ String serialization converts any .NET string into a byte array, where each string character is represented as 2 bytes (native .NET representation). The resulting array is twice as long as the string, but serialization is completely encoding-agnostic (ie. safe). AES-CTR Transform public class AesCtrCryptoTransform : ICryptoTransform public AesCtrCryptoTransform(byte[] key, ArraySegment<byte> counterBufferSegment, Func<Aes> aesFactory = null) // ctor .NET lacks an implementation of CTR mode, and most of the Internet-available .NET implementations of AES-CTR are poorly implemented. Inferno implementation should satisfy anyone who needs a generic .NET AES-CTR transform. It is a key building block of Inferno AEAD as well. TOTP public static int GenerateTOTP(byte[] secret, Func<DateTime> timeFactory = null, int totpLength = DEFAULT_TOTP_LENGTH, string modifier = null) public static bool ValidateTOTP(byte[] secret, int totp, Func<DateTime> timeFactory = null, int totpLength = 6, string modifier = null) If timeFactory is null, the current time (DateTime.UtcNow) is used. If modifier is not null, it will be hashed along with the timestamp, creating a custom TOTP.

Implementation Details