SecurityDriven.Inferno: .NET crypto done right.

inferno crypto library

GitHub: source, binaries
Nuget: Inferno, Inferno.StrongName

Copyright © 2017 Stan Drapkin sdrapkin


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 also been professionally audited.


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# 6.0. The only reference is "System.dll".
  • Performance-oriented (within reason - unsafe code is not a reason).
  • Minimal codebase, high maintainability & introspectability (easy security audits).
  • Unit testing, fuzz testing.
  • Streaming authenticated encryption (secure channels).
  • Symmetric crypto: AEAD only.
  • Asymmetric crypto: NSA/CNSA Suite B API only (Elliptic Curves). No RSA.
  • Decent documentation & code examples.


  • [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).
  • [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


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.


public static Func<SHA384> HashFactory

Don't forget to Dispose after use. The particular choice of SHA384 is explained in "Implementation Details" section.


// example:
var data = Utils.SafeUTF8.GetBytes("Inferno");
using (var hmac = SuiteB.HmacFactory()) // HMACSHA384
	hmac.Key = new byte[] { 1, 2, 3, 4, 5 };

// output:
// AD0E5CA84DA23AE4A0537FC9008CB6AF91E16CA8429098B099E5066D30CBE0DA34ABCE4D71A02FB09786A53B523492A3

Generates HMACSHA384 instance. Don't forget to Dispose after use.


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.


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


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.


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.


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))

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))

		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))

		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


staticSharedSecret (48 bytes):

ephemeral public DHM key (104 bytes):

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

// 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);

// 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));


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(config: Base16Config.HexLowercase).Dump();

bytes.ToBase32(config: Base32Config.Rfc).Dump();




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();


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.


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

Primitive choices

  • A CSRNG is readily available in .NET (through Windows CSRNG). It is CTR_DRBG with AES-256 (NIST sp800-90a).
  • AES-256 is the only block cipher used.
  • CTR block cipher mode is used.
  • SHA-384 hash is used. SHA-384 is the best hash in the SHA2 hash family: it is as fast as SHA-512 on 64-bit platforms but, unlike SHA-512 or SHA-256, its truncated design serves as an effective defense against length extension attacks. This makes SHA-384 the safest hash to expose to developers, whose idea of a keyed hash is often "hash(key||message)".
  • [HMAC-SHA384]/128 (ie. truncated to 128 bits) is used as a MAC tag. The MAC key is also 128-bit.
  • NIST-approved sp800_108 "Counter Mode" is used as a KDF (with HMAC-SHA384 as the underlying PRF). It's simple, secure, and parallelizable. sp800_108_Ctr is chosen over HKDF because HKDF is feedback-based (not parallelizable) and we do not need an explicit entropy extraction step.
  • ECDSA and ECDHM are done over P-384 curve with SHA-384 hash. Microsoft's validated implementation is used.

EtM_CTR design

EtM_CTR encryption follows the following design:

nonce (40 bytes) ciphertext tag (16 bytes)
  • Total overhead is 56 bytes.
  • Nonce is 40 random bytes: 32 bytes are used as a key tweak (matches the size of AES-256 key), and the 8 remaining random bytes are the 64-bit IV for the CTR nonce (the other 64-bits of CTR nonce are a counter).
  • Ciphertext is AES-256-CTR. Ie. there is no padding and ciphertext length is plaintext length.
  • Tag is calculated as [HMAC-SHA384]/128 over [the last 8 bytes of nonce || ciphertext]. The 32-byte key tweak is not part of the tag because it affects key generation and is validated indirectly (a change in the key tweak would result in a different MAC key).

The keys are generated as follows:

  1. SessionKeys = sp800_108_Ctr(key: MasterKey, label: salt, context: key tweak, counter: 1). This produces 48 bytes (due to HMAC-SHA384 as the underlying PRF).
  2. MacKey = first 16 bytes of SessionKeys.
  3. EncKey = remaining 32 bytes of SessionKeys.

AEAD semantics are achieved by using optional "Associated Data" as salt (or salt component). EtM_CTR output is indistinguishable from random noise (poly1305 MAC might not have that property).

AEAD Transform design

AEAD Transform (streaming AEAD) follows the following design:

chunk 1 chunk 2 chunk ... chunk n
  • Each chunk except the last chunk has a fixed length of 84,984 bytes. This number is chosen as the largest buffer size that avoids .NET LOH.
  • The last chunk must have a length of less than 84,984 bytes (which is the definition of a last chunk). Chunk 1 can be the last chunk.
  • Each chunk is encrypted with EtM_CTR described above. The plaintext length of each chunk is thus at most 84,928 bytes (56 bytes are overhead). The storage efficiency is thus 99.93%.
  • Each chunk's encryption is independent, and uses the following modification in key generation logic:
    • SessionKeys = sp800_108_Ctr(key: MasterKey, label: salt, context: key tweak, counter: chunk number). The key tweak is per-chunk (ie. an independent application of EtM_CTR).

Each chunk can be independently processed and encrypted/decrypted/authenticated. The chunk counter is not part of the stream (tracked by the transform). The inclusion of chunk counter in key derivation prevents chunk reordering. The output is indistinguishable from random noise.

Why not XYZ?

But all the cool kids are using the NaCl library - why is Inferno not based on NaCl?

  • NaCl secret-key authenticated encryption provides AE, but not AEAD.
  • NaCl does not provide AES-256 (has been listed as [TO-DO] for 5 years).
  • As cool as Poly1305 nonce-based MAC is, it is not a ubiquitous MAC, and is limited to 128 bits. HMAC is much more versatile and failure-resistant (ex. even HMAC-MD5 is unbroken, while Poly1305 requires the underlying cipher, such as AES, to be secure).
  • NaCl seems to use HSalsa as a hash function for the shared DH key in crypto_box. I prefer a more conservative SHA2 with well-studied collision resistance, and so does NIST & SuiteB.
  • Inferno's goal & primary usecase is secure, long-term 'cold' storage. NaCl is more suited for short-term communication security. The CAESAR competition will soon provide a new portfolio of AEAD primitives.

Why is Inferno not using AES-GCM?

  • GCM has many restrictions and is very brittle and hard to implement correctly. The MAC part of GCM is weaker than HMAC. GCM MAC is nonce-based, which is an unnecessary complication. The 'recommended' GCM implementations use 96-bit tags, which can be at most 128 bits. Inferno uses 128-bit tags and can easily accommodate shorter or longer tags if needed. GCM does not handle tag truncation well. GCM is NIST-approved, but so are CTR, SHA2, and HMAC. GCM has been NIST-SuiteB-preferred for high bandwidth traffic - ie. its goal and primary usecase is similar to NaCl (ex. secure high-speed network communications). Inferno uses AES-CTR with HMAC instead.
  • GCM is not available in .NET framework (even though it is available in Windows). To be clear, Inferno would not use it even if it were, or becomes available.
  • GCM is not the best choice for long-term storage: CTR, SHA2, and HMAC will continue to be ubiquitous in ~50 years, but GCM is less likely to be around.
  • Authentication in Inferno should not require decryption (should not touch the cipher). It's safer that way (avoids needless memory exposure to plaintext, among other things).

All crypto primitives used by Inferno have Microsoft-maintained implementations that have been around since .NET 2.0 (ie. have been mass-deployed & scrutinized for 10+ years).