Skip to content

Instantly share code, notes, and snippets.

@ekalchev
Created December 12, 2020 10:43
Show Gist options
  • Save ekalchev/d1286bae6a90ef005576c1ef993898f6 to your computer and use it in GitHub Desktop.
Save ekalchev/d1286bae6a90ef005576c1ef993898f6 to your computer and use it in GitHub Desktop.
class Cryptography2
{
private ECPrivateKeyParameters privateKey;
private ECPublicKeyParameters publicKey;
private ECDomainParameters ecDomainParameters;
private ECCurve ecurve;
private const byte INFO_PARAMETER_DELIMITER = 0;
// CEK_INFO = "Content-Encoding: aesgcm" || 0x00
private static readonly byte[] keyInfoParameter = Encoding.ASCII.GetBytes("Content-Encoding: aesgcm\0");
// NONCE_INFO = "Content-Encoding: nonce" || 0x00
private static readonly byte[] nonceInfoParameter = Encoding.ASCII.GetBytes("Content-Encoding: nonce\0");
private static readonly byte[] authInfoParameter = Encoding.ASCII.GetBytes("Content-Encoding: auth\0");
private static readonly byte[] keyLabel = Encoding.ASCII.GetBytes("P-256");
private const int NonceBitSize = 128;
private const int MacBitSize = 128;
private const int SHA_256_LENGTH = 32;
private const int KEY_LENGTH = 16;
private const int NONCE_LENGTH = 12;
private const int HEADER_RS = 4096;
private const int TAG_LENGTH = 16;
private const int CHUNK_SIZE = HEADER_RS + TAG_LENGTH;
public Cryptography2()
{
Test();
}
public byte[] AuthSecret { get; }
public byte[] PublicKey { get; }
public byte[] DecryptMessage(KeyParameter sharedKey, byte[] encryptedMessage, out byte[] nonSecretPayloadBytes)
{
using (var cipherStream = new MemoryStream(encryptedMessage))
using (var cipherReader = new BinaryReader(cipherStream))
{
//Grab Payload
int nonSecretLength = (int)cipherReader.ReadByte();
nonSecretPayloadBytes = cipherReader.ReadBytes(nonSecretLength);
//Grab Nonce
var nonce = cipherReader.ReadBytes(NonceBitSize / 8);
var cipher = new GcmBlockCipher(new AesEngine());
var parameters = new AeadParameters(sharedKey, MacBitSize, nonce, nonSecretPayloadBytes);
cipher.Init(false, parameters);
//Decrypt Cipher Text
var cipherText = cipherReader.ReadBytes(encryptedMessage.Length - nonSecretLength - nonce.Length);
var plainText = new byte[cipher.GetOutputSize(cipherText.Length)];
try
{
var len = cipher.ProcessBytes(cipherText, 0, cipherText.Length, plainText, 0);
cipher.DoFinal(plainText, len);
}
catch (InvalidCipherTextException)
{
//Return null if it doesn't authenticate
return null;
}
return plainText;
}
}
private (byte[], byte[]) ExtractDH(byte[] senderKey, byte[] receiverPrivateKey)
{
ECPoint pt = ecurve.DecodePoint(senderKey);
ECPublicKeyParameters publicKeyParams = new ECPublicKeyParameters(pt, ecDomainParameters);
IBasicAgreement aKeyAgree = new ECDHBasicAgreement();
aKeyAgree.Init(privateKey);
byte[] sharedSecret = aKeyAgree.CalculateAgreement(publicKeyParams).ToByteArrayUnsigned();
byte[] receiverKey = AddLengthPrefix(PublicKey);
senderKey = AddLengthPrefix(senderKey);
byte[] context = new byte[keyLabel.Length + 1 + receiverKey.Length + senderKey.Length];
int destinationOffset = 0;
Array.Copy(keyLabel, 0, context, destinationOffset, keyLabel.Length);
destinationOffset += keyLabel.Length + 1;
Array.Copy(receiverKey, 0, context, destinationOffset, receiverKey.Length);
destinationOffset += receiverKey.Length;
Array.Copy(senderKey, 0, context, destinationOffset, senderKey.Length);
return (sharedSecret, context);
}
private byte[] AddLengthPrefix(byte[] buffer)
{
byte[] newBuffer = new byte[buffer.Length + 2];
Array.Copy(buffer, 0, newBuffer, 2, buffer.Length);
byte[] intBytes = BitConverter.GetBytes((short)buffer.Length);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(intBytes);
}
Debug.Assert(intBytes.Length <= 2);
Array.Copy(intBytes, 0, newBuffer, 0, intBytes.Length);
return newBuffer;
}
public void Test()
{
////// generated on the client - ECDH with curve prime256v1//////
// random bytes
var authSecret = new byte[] { 5, 47, 48, 155, 244, 31, 204, 235, 11, 247, 67, 120, 24, 137, 25, 153 };
// public key sent to the server
var receiverPublicKeyBytes = new byte[] { 4, 234, 243, 178, 1, 91, 224, 122, 211, 185, 63, 90, 135, 90, 206, 224, 43, 63, 63, 131, 227, 22, 157, 108, 31, 176, 83, 27, 70, 246, 89, 112, 7, 102, 79, 42, 205, 17, 100, 100, 149, 198, 135, 95, 241, 189, 182, 61, 103, 161, 4, 244, 127, 185, 128, 18, 139, 78, 3, 169, 111, 218, 80, 73, 55 };
// private key kept on the client
var privateKey = new byte[] { 250, 117, 42, 156, 20, 153, 20, 193, 233, 136, 185, 246, 56, 52, 250, 150, 120, 250, 72, 147, 182, 144, 120, 103, 76, 11, 175, 143, 92, 1, 177, 59 };
// received from the server
var salt = new byte[] { 248, 70, 134, 75, 160, 188, 58, 83, 105, 238, 59, 171, 27, 115, 224, 200 };
// server public key
var senderPublicKeyBytes = new byte[] { 4, 26, 9, 166, 16, 222, 177, 154, 230, 15, 231, 11, 89, 108, 66, 97, 247, 3, 158, 199, 93, 98, 187, 162, 175, 76, 127, 2, 149, 67, 13, 195, 26, 145, 46, 223, 4, 34, 46, 70, 57, 0, 98, 139, 79, 25, 84, 187, 176, 126, 50, 108, 192, 61, 207, 83, 248, 189, 14, 10, 182, 18, 141, 52, 92 };
// actual data that needs decoding
var rawData = new byte[] { 127, 5, 92, 210, 222, 94, 48, 180, 122, 71, 186, 120, 91, 171, 10, 6, 14, 182, 145, 108, 136, 161, 172, 8, 67, 27, 136, 55, 6, 224, 180, 181, 141, 242, 21, 101, 235, 6, 125, 162, 97, 236, 49, 150, 61, 225, 130, 58, 57, 93, 37, 79, 208, 21, 8, 139, 72, 235, 12, 173, 50 };
////Extract DH
var ecP = NistNamedCurves.GetByName("P-256");
ECDomainParameters eCDomainParameters = new ECDomainParameters(ecP.Curve, ecP.G, ecP.N);
var receiverPrivateKey = new ECPrivateKeyParameters(new BigInteger(1, privateKey), eCDomainParameters);
ECPoint pt1 = ecP.Curve.DecodePoint(senderPublicKeyBytes);
ECPublicKeyParameters senderPublicKey = new ECPublicKeyParameters(pt1, eCDomainParameters);
ECPoint pt2 = ecP.Curve.DecodePoint(receiverPublicKeyBytes);
ECPublicKeyParameters receiverPublicKey = new ECPublicKeyParameters(pt2, eCDomainParameters);
var (key, nonce) = DeriveKeyAndNonce(salt, authSecret, senderPublicKey, receiverPublicKey, receiverPrivateKey);
byte[] buffer = rawData;
byte[] result = new byte[0];
var start = 0;
for (var i = 0; start < buffer.Length; ++i)
{
var end = start + CHUNK_SIZE;
if (end == buffer.Length)
{
throw new InvalidOperationException("Truncated payload");
}
end = Math.Min(end, buffer.Length);
if (end - start <= TAG_LENGTH)
{
throw new InvalidOperationException("Invalid block: too small at " + i);
}
byte[] block = DecryptRecord(key, i, Slice(buffer, start, end), end >= buffer.Length);
result = Concat(result, block);
start = end;
}
}
private byte[] DecryptRecord(byte[] key, int counter, byte[] buffer, bool last)
{
return null; // TODO
}
private (byte[], byte[]) DeriveKeyAndNonce(byte[] salt, byte[] authSecret, ECPublicKeyParameters senderPublicKey, ECPublicKeyParameters receiverPublicKey, ECPrivateKeyParameters receiverPrivateKey)
{
var (secret, context) = ExtractSecretAndContext(senderPublicKey, receiverPublicKey, receiverPrivateKey);
secret = HKDF.GetBytes(authSecret, secret, authInfoParameter, SHA_256_LENGTH);
byte[] keyInfo = Concat(keyInfoParameter, context);
byte[] nonceInfo = Concat(nonceInfoParameter, context);
byte[] prk = HKDF.Extract(salt, secret);
return (HKDF.Expand(prk, keyInfo, KEY_LENGTH), HKDF.Expand(prk, nonceInfo, NONCE_LENGTH));
}
private (byte[], byte[]) ExtractSecretAndContext(ECPublicKeyParameters senderPublicKey, ECPublicKeyParameters receiverPublicKey, ECPrivateKeyParameters receiverPrivateKey)
{
IBasicAgreement aKeyAgree = new ECDHBasicAgreement();
aKeyAgree.Init(receiverPrivateKey);
byte[] sharedSecret = aKeyAgree.CalculateAgreement(senderPublicKey).ToByteArrayUnsigned();
byte[] receiverKeyBytes = AddLengthPrefix(receiverPublicKey.Q.GetEncoded());
byte[] senderPublicKeyBytes = AddLengthPrefix(senderPublicKey.Q.GetEncoded());
byte[] context = new byte[keyLabel.Length + 1 + receiverKeyBytes.Length + senderPublicKeyBytes.Length];
int destinationOffset = 0;
Array.Copy(keyLabel, 0, context, destinationOffset, keyLabel.Length);
destinationOffset += keyLabel.Length + 1;
Array.Copy(receiverKeyBytes, 0, context, destinationOffset, receiverKeyBytes.Length);
destinationOffset += receiverKeyBytes.Length;
Array.Copy(senderPublicKeyBytes, 0, context, destinationOffset, senderPublicKeyBytes.Length);
return (sharedSecret, context);
}
private static byte[] Slice(byte[] source, int startIndex, int endIndex)
{
Debug.Assert(startIndex < endIndex);
int length = endIndex - startIndex;
byte[] result = new byte[length];
Array.Copy(source, startIndex, result, 0, length);
return result;
}
private byte[] Concat(byte[] first, byte[] second)
{
byte[] ret = new byte[first.Length + second.Length];
Buffer.BlockCopy(first, 0, ret, 0, first.Length);
Buffer.BlockCopy(second, 0, ret, first.Length, second.Length);
return ret;
}
private (ECPrivateKeyParameters, ECPublicKeyParameters) GenerateKeys()
{
ECKeyPairGenerator gen = new ECKeyPairGenerator("ECDH");
SecureRandom secureRandom = new SecureRandom();
X9ECParameters ecp = NistNamedCurves.GetByName("P-256");
ecurve = ecp.Curve;
ECDomainParameters ecSpec = new ECDomainParameters(ecurve, ecp.G, ecp.N, ecp.H, ecp.GetSeed());
ECKeyGenerationParameters eckgparameters = new ECKeyGenerationParameters(ecSpec, secureRandom);
ecDomainParameters = eckgparameters.DomainParameters;
gen.Init(eckgparameters);
AsymmetricCipherKeyPair eckp = gen.GenerateKeyPair();
ECPublicKeyParameters ecPub = (ECPublicKeyParameters)eckp.Public;
ECPrivateKeyParameters ecPri = (ECPrivateKeyParameters)eckp.Private;
return (ecPri, ecPub);
}
public class HKDF
{
/// <summary>
/// Returns a 32 byte psuedorandom number that can be used with the Expand method if
/// a cryptographically secure pseudorandom number is not already available.
/// </summary>
/// <param name="salt">(Optional, but you should use it) Non-secret random value.
/// If less than 64 bytes it is padded with zeros. Can be reused but output is
/// stronger if not reused. (And of course output is much stronger with salt than
/// without it)</param>
/// <param name="inputKeyMaterial">Material that is not necessarily random that
/// will be used with the HMACSHA256 hash function and the salt to produce
/// a 32 byte psuedorandom number.</param>
/// <returns></returns>
public static byte[] Extract(byte[] salt, byte[] inputKeyMaterial)
{
//For algorithm docs, see section 2.2: https://tools.ietf.org/html/rfc5869
using (System.Security.Cryptography.HMACSHA256 hmac = new System.Security.Cryptography.HMACSHA256(salt))
{
return hmac.ComputeHash(inputKeyMaterial, offset: 0, count: inputKeyMaterial.Length);
}
}
/// <summary>
/// Returns a secure pseudorandom key of the desired length. Useful as a key derivation
/// function to derive one cryptograpically secure pseudorandom key from another
/// cryptograpically secure pseudorandom key. This can be useful, for example,
/// when needing to create a subKey from a master key.
/// </summary>
/// <param name="key">A cryptograpically secure pseudorandom number. Can be obtained
/// via the Extract method or elsewhere. Must be 32 bytes or greater. 64 bytes is
/// the prefered size. Shorter keys are padded to 64 bytes, longer ones are hashed
/// to 64 bytes.</param>
/// <param name="info">(Optional) Context and application specific information.
/// Allows the output to be bound to application context related information.</param>
/// <param name="length">Length of output in bytes.</param>
/// <returns></returns>
public static byte[] Expand(byte[] key, byte[] info, int length)
{
//For algorithm docs, see section 2.3: https://tools.ietf.org/html/rfc5869
//Also note:
// SHA256 has a block size of 64 bytes
// SHA256 has an output length of 32 bytes (but can be truncated to less)
const int hashLength = 32;
//Min recommended length for Key is the size of the hash output (32 bytes in this case)
//See section 2: https://tools.ietf.org/html/rfc2104#section-3
//Also see: http://security.stackexchange.com/questions/95972/what-are-requirements-for-hmac-secret-key
if (key == null || key.Length < 32)
{
throw new ArgumentOutOfRangeException("Key should be 32 bytes or greater.");
}
if (length > 255 * hashLength)
{
throw new ArgumentOutOfRangeException("Output length must 8160 bytes or less which is 255 * the SHA256 block site of 32 bytes.");
}
int outputIndex = 0;
byte[] buffer;
byte[] hash = new byte[0];
byte[] output = new byte[length];
int count = 1;
int bytesToCopy;
using (System.Security.Cryptography.HMACSHA256 hmac = new System.Security.Cryptography.HMACSHA256(key))
{
while (outputIndex < length)
{
//Setup buffer to hash
buffer = new byte[hash.Length + info.Length + 1];
Buffer.BlockCopy(hash, 0, buffer, 0, hash.Length);
Buffer.BlockCopy(info, 0, buffer, hash.Length, info.Length);
buffer[buffer.Length - 1] = (byte)count++;
//Hash the buffer and return a 32 byte hash
hash = hmac.ComputeHash(buffer, offset: 0, count: buffer.Length);
//Copy as much of the hash as we need to the final output
bytesToCopy = Math.Min(length - outputIndex, hash.Length);
Buffer.BlockCopy(hash, 0, output, outputIndex, bytesToCopy);
outputIndex += bytesToCopy;
}
}
return output;
}
/// <summary>
/// Generates a psuedorandom number of the length specified. This number is suitable
/// for use as an encryption key, HMAC validation key or other uses of a cryptographically
/// secure psuedorandom number.
/// </summary>
/// <param name="salt">non-secret random value. If less than 64 bytes it is
/// padded with zeros. Can be reused but output is stronger if not reused.</param>
/// <param name="inputKeyMaterial">Material that is not necessarily random that
/// will be used with the HMACSHA256 hash function and the salt to produce
/// a 32 byte psuedorandom number.</param>
/// <param name="info">(Optional) context and application specific information.
/// Allows the output to be bound to application context related information. Pass 0 length
/// byte array to omit.</param>
/// <param name="length">Length of output in bytes.</param>
public static byte[] GetBytes(byte[] salt, byte[] inputKeyMaterial, byte[] info, int length)
{
byte[] key = Extract(salt, inputKeyMaterial);
return Expand(key, info, length);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment