using System;
using System.Diagnostics;
using System.Security.Cryptography;
namespace Hazel.Crypto
{
///
/// Implementation of AEAD_AES128_GCM based on:
/// * RFC 5116 [1]
/// * NIST SP 800-38d [2]
///
/// [1] https://tools.ietf.org/html/rfc5116
/// [2] https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
///
/// Adapted from: https://gist.github.com/mendsley/777e6bd9ae7eddcb2b0c0fe18247dc60
///
public class Aes128Gcm : IDisposable
{
public const int KeySize = 16;
public const int NonceSize = 12;
public const int CiphertextOverhead = TagSize;
private const int TagSize = 16;
private readonly IAes encryptor_;
private readonly ByteSpan hashSubkey_;
private readonly ByteSpan blockJ_;
private readonly ByteSpan blockS_;
private readonly ByteSpan blockZ_;
private readonly ByteSpan blockV_;
private readonly ByteSpan blockScratch_;
///
/// Creates a new instance of an AEAD_AES128_GCM cipher
///
/// Symmetric key
public Aes128Gcm(ByteSpan key)
{
if (key.Length != KeySize)
{
throw new ArgumentException("Invalid key length", nameof(key));
}
// Create the AES block cipher
this.encryptor_ = CryptoProvider.CreateAes(key);
// Allocate scratch space
ByteSpan scratchSpace = new byte[96];
this.hashSubkey_ = scratchSpace.Slice(0, 16);
this.blockJ_ = scratchSpace.Slice(16, 16);
this.blockS_ = scratchSpace.Slice(32, 16);
this.blockZ_ = scratchSpace.Slice(48, 16);
this.blockV_ = scratchSpace.Slice(64, 16);
this.blockScratch_ = scratchSpace.Slice(80, 16);
// Create the GHASH subkey by encrypting the 0-block
this.encryptor_.EncryptBlock(this.hashSubkey_, this.hashSubkey_);
}
///
/// Encryptes the specified plaintext and generates an authentication
/// tag for the provided additional data. Returns the byte array
/// containg both the ciphertext and authentication tag.
///
///
/// Array in which to encode the encrypted ciphertext and
/// authentication tag. This array must be large enough to hold
/// `plaintext.Lengh + CiphertextOverhead` bytes.
///
/// Unique value for this message
/// Plaintext data to encrypt
///
/// Additional data used to authenticate the message
///
public void Seal(ByteSpan output, ByteSpan nonce, ByteSpan plaintext, ByteSpan associatedData)
{
if (nonce.Length != NonceSize)
{
throw new ArgumentException("Invalid nonce size", nameof(nonce));
}
if (output.Length < plaintext.Length + CiphertextOverhead)
{
throw new ArgumentException("Invalid output size", nameof(output));
}
// Create the initial counter block
nonce.CopyTo(this.blockJ_);
// Encrypt the plaintext to output
GCTR(output, this.blockJ_, 2, plaintext);
// Generate and append the authentication tag
int tagOffset = plaintext.Length;
GenerateAuthenticationTag(output.Slice(tagOffset), output.Slice(0, tagOffset), associatedData);
}
///
/// Validates the authentication tag against the provided additional
/// data, then decrypts the cipher text returning the original
/// plaintext.
///
///
/// The unique value used to seal this message
///
///
/// Combined ciphertext and authentication tag
///
///
/// Additional data used to authenticate the message
///
///
/// On successful validation and decryprion, Open writes the original
/// plaintext to output. Must contain enough space to hold
/// `ciphertext.Length - CiphertextOverhead` bytes.
///
///
/// True if the data was validated and successfully decrypted.
/// Otherwise, false.
///
public bool Open(ByteSpan output, ByteSpan nonce, ByteSpan ciphertext, ByteSpan associatedData)
{
if (nonce.Length != NonceSize)
{
throw new ArgumentException("Invalid nonce size", nameof(nonce));
}
if (ciphertext.Length < CiphertextOverhead)
{
throw new ArgumentException("Invalid ciphertext size", nameof(ciphertext));
}
else if (output.Length < ciphertext.Length - CiphertextOverhead)
{
throw new ArgumentException("Invalid output size", nameof(output));
}
// Split ciphertext into actual ciphertext and authentication
// tag components.
ByteSpan authenticationTag = ciphertext.Slice(ciphertext.Length - TagSize);
ciphertext = ciphertext.Slice(0, ciphertext.Length - TagSize);
// Create the initial counter block
nonce.CopyTo(this.blockJ_);
// Verify the tags match
GenerateAuthenticationTag(this.blockScratch_, ciphertext, associatedData);
if (0 == Const.ConstantCompareSpans(this.blockScratch_, authenticationTag))
{
return false;
}
// Decrypt the cipher text to output
GCTR(output, this.blockJ_, 2, ciphertext);
return true;
}
///
/// Release resources acquired by the cipher
///
public void Dispose()
{
this.encryptor_.Dispose();
}
// Generate the authentication tag for a ciphertext+associated data
void GenerateAuthenticationTag(ByteSpan output, ByteSpan ciphertext, ByteSpan associatedData)
{
Debug.Assert(output.Length >= 16);
// Hash `Associated data || Ciphertext || len(AssociatedD data) || len(Ciphertext)`
// into `blockS`
{
// Clear hash output block
SetSpanToZeros(this.blockS_);
// Write associated data blocks to hash
int fullBlocks = associatedData.Length / 16;
GHASH(this.blockS_, associatedData, fullBlocks);
if (fullBlocks * 16 < associatedData.Length)
{
SetSpanToZeros(this.blockScratch_);
associatedData.Slice(fullBlocks * 16).CopyTo(this.blockScratch_);
GHASH(this.blockS_, this.blockScratch_, 1);
}
// Write ciphertext blocks to hash
fullBlocks = ciphertext.Length / 16;
GHASH(this.blockS_, ciphertext, fullBlocks);
if (fullBlocks * 16 < ciphertext.Length)
{
SetSpanToZeros(this.blockScratch_);
ciphertext.Slice(fullBlocks * 16).CopyTo(this.blockScratch_);
GHASH(this.blockS_, this.blockScratch_, 1);
}
// Write bit sizes to hash
ulong associatedDataLengthInBits = (ulong)(8 * associatedData.Length);
ulong ciphertextDataLengthInBits = (ulong)(8 * ciphertext.Length);
this.blockScratch_.WriteBigEndian64(associatedDataLengthInBits);
this.blockScratch_.WriteBigEndian64(ciphertextDataLengthInBits, 8);
GHASH(this.blockS_, this.blockScratch_, 1);
}
// Encrypt the tag. GCM requires this because `GASH` is not
// cryptographically secure. An attacker could derive our hash
// subkey `hashSubkey_` from an unencrypted tag.
GCTR(output, this.blockJ_, 1, this.blockS_);
}
// Run the GCTR cipher
void GCTR(ByteSpan output, ByteSpan counterBlock, uint counter, ByteSpan data)
{
Debug.Assert(counterBlock.Length == 16);
Debug.Assert(output.Length >= data.Length);
// Loop through plaintext blocks
int writeIndex = 0;
int numBlocks = (data.Length + 15) / 16;
for (int ii = 0; ii != numBlocks; ++ii)
{
// Encode counter into block
// CB[1] = J0
// CB[i] = inc[32](CB[i-1])
counterBlock.WriteBigEndian32(counter, 12);
++counter;
// CIPH[k](CB[i])
this.encryptor_.EncryptBlock(counterBlock.Slice(0, 16), this.blockScratch_);
// Y[i] = X[i] xor CIPH[k](CB[i])
for (int jj = 0; jj != 16 && writeIndex < data.Length; ++jj, ++writeIndex)
{
output[writeIndex] = (byte)(data[writeIndex] ^ this.blockScratch_[jj]);
}
}
}
// Run the GHASH function
void GHASH(ByteSpan output, ByteSpan data, int numBlocks)
{
///TODO(mendsley): See Ref[6] for opitmizations of GHASH on both hardware and software
///
///[6] D. McGrew, J. Viega, The Galois/Counter Mode of Operation (GCM), Natl. Inst. Stand.
///Technol. [Web page], http://www.csrc.nist.gov/groups/ST/toolkit/BCM/documents/
///proposedmodes / gcm / gcm - revised - spec.pdf, May 31, 2005.
Debug.Assert(output.Length == 16);
Debug.Assert(data.Length >= numBlocks * 16);
int readIndex = 0;
for (int ii = 0; ii != numBlocks; ++ii)
{
for (int jj = 0; jj != 16; ++jj, ++readIndex)
{
// Y[ii-1] xor X[ii]
output[jj] ^= data[readIndex];
}
// Y[ii] = (Y[ii-1] xor X[ii]) · H
MultiplyGF128Elements(output, this.hashSubkey_, this.blockZ_, this.blockV_);
}
}
// Multiply two Galois field elements `X` and `Y` together and store
// the result in `X` such that at the end of the function:
// X = X·Y
static void MultiplyGF128Elements(ByteSpan X, ByteSpan Y, ByteSpan scratchZ, ByteSpan scratchV)
{
Debug.Assert(X.Length == 16);
Debug.Assert(Y.Length == 16);
Debug.Assert(scratchZ.Length == 16);
Debug.Assert(scratchV.Length == 16);
// Galois (finite) fields represented by GF(p) define a set of
// closed algebraic operations. For AES128_GCM we'll be dealing
// with the GF(2^128) field.
//
// We treat each incoming 16 byte block as a polynomial in field
// and define multiplication between two polynomials as the
// polynomial product reduced by (mod) the field polynomial:
// 1 + x + x^2 + x^7 + x^128
//
// Field polynomials are represented by a 128 bit string. Bit n is
// the coefficient of the x^n term. We use little-endian bit
// ordering (not to be confused with byte ordering) for these
// coefficients. E.g. X[0] & 0x00000001 represents the 7th bit in
// the bit string defined by X, _not_ the 0th bit.
//
// What follows is a modified version of the "peasant's algorithm"
// to multiply two numbers:
//
// Z contains the accumulated product
// V is a copy of Y (so we can modify it via shifting).
//
// We calculate Z = X·V as follows
// We loop through each of the 128 bits in X maintaining the
// following loop invariant: X·V + Z = the final product
//
// On each iteration `ii`:
//
// If the `ii`th bit of `X` is set, add the add the polynomial
// in `V` to `X`: `X[n] = X[n] ^ V[n]`
//
// Double V (Shift one bit right since we're storing little
// endian bit). This has the effect of multiplying V by the
// polynomial `x`. We track the unrepresentable coefficient
// of `x^128` by storing the most significant bit before the
// shift `V[15] >> 7` as `carry`
//
// Check if we've overflowed our multiplication. If overflow
// occurred, there will be a non-zero coefficient for the
// `x^128` term in the step above `carry`
//
// If we have overflowed, our polynomial is exactly of degree
// 129 (since we're only multiplying by `x`). We reduce the
// polynomial back into degree 128 by adding our field's
// irreducible polynomial: 1 + x + x^2 + x^7 + x^128. This
// reduction cancels out the x^128 term (x^128 + x^128 in GF(2)
// is zero). Therefore this modulo can be achieved by simply
// adding the irreducible polynomial to the new value of `V`. The
// irreducible polynomial is represented by the bit string:
// `11100001` followed by 120 `0`s. We can add this value to `V`
// by: `V[0] = V[0] ^ 0xE1`.
SetSpanToZeros(scratchZ);
X.CopyTo(scratchV);
for (int ii = 0; ii != 128; ++ii)
{
int bitIndex = 7 - (ii % 8);
if ((Y[ii / 8] & (1 << bitIndex)) != 0)
{
for (int jj = 0; jj != 16; ++jj)
{
scratchZ[jj] ^= scratchV[jj];
}
}
bool carry = false;
for (int jj = 0; jj != 16; ++jj)
{
bool newCarry = (scratchV[jj] & 0x01) != 0;
scratchV[jj] >>= 1;
if (carry)
{
scratchV[jj] |= 0x80;
}
carry = newCarry;
}
if (carry)
{
scratchV[0] ^= 0xE1;
}
}
scratchZ.CopyTo(X);
}
// Set the contents of a span to all zero
static void SetSpanToZeros(ByteSpan span)
{
for (int ii = 0, nn = span.Length; ii != nn; ++ii)
{
span[ii] = 0;
}
}
}
}