Created
December 12, 2020 10:43
-
-
Save ekalchev/d1286bae6a90ef005576c1ef993898f6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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