Skip to content

how it works hybrid encryption

Shaylen edited this page Apr 14, 2023 · 2 revisions

Hybrid Encryption

This is something I never thought I'd ever implement in this solution

Some things I saw made me a little nervous:

  • The customers api was wide-open
  • The solution [at the time] wasn't running over https

This prompted me to watch a course on cryptography, Cryptography in .NET 6 by Stephen Haunts

Even after that, I needed to figure out if the private key that IdentityServer4 has can be accessed so I checked the docs, and it could be

With the customers api sitting in the same application as IdentityServer4, the ISigningCredentialStore could be injected into the customers controller and handed over to the DecryptAsync method to extract the private key and decrypt the EncryptedAesKey

That choice of keeping it together allows this to work

IdentityServer4 publishes the public key of the RSA key pair usually in the form of a json web key [JWK] usually used for token validation, however, another advantage here is that asymmetric encryption can be utilized

Now, hybrid encryption is implemented using both symmetric and asymmetric encryption, one question that you may be wanting to ask is, "Why not directly encrypt the data with asymmetric encryption and avoid symmetric encryption altogether?"

That would be a valid question to ask

The problem, however, is that if you try to encrypt data that is larger than the size of the key, it breaks, and we have no idea how large the data to encrypt is going to be

So to solve this, encrypt the data with symmetric encryption, and since that key will always be smaller than the asymmetric encryption key, it wouldn't break and the key used for symmetric encryption can then be encrypted with asymmetric encryption

The algorithm used for symmetric encryption is AesGcm, and the advantage of using it is that manual integrity checking can be omitted as it fills an array [of 16 bytes size] with a tag it generated during encryption

That tag will be used to validate the integrity of the data during decryption

The Public Key

This can be accessed via the jwks url /.well-known/openid-configuration/jwks at the Identity Service

Here's the public key, and what's important to create the RSA public key is the e [exponent] and n [modulus] values

Also worth pointing out is that those values are base64url encoded

{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "e": "AQAB",
      "n": "5Xm3AZI8rQYx3pCp0XSWPAYZHgE-eXjz26a3kRH0FjjOYJTS-H3oq8UW4KBJqX2vVBLxWKQH-3XdPkyY4IOrdfwY0OTZSyBODP-YygYrt9tj88hzMcEz2kSfUYJnkQvS02kwI6aQpNBksL4gf3dDpzAhC4Uspj7aEeeyTUC7u7_pxmtxxeYsolasNtAQlWFyR1voOyyKGVEah77NeypfL0Cml0XKqIlSjW8q1l1_eP-g_-KpkQt9fDO9tzjm4gwPyqXjg2XArpnCm5j6AiYAcvOlla13fEvUKjZ6g6jLOt7aiFoq3KjYRprtnuOOGLZ0fwUWGLtDxZBl9JFJRZnthQ",
      "alg": "RS256"
    }
  ]
}

Though this endpoint exists, in the EncryptAsync method, the discovery document is retrieved and it's extracted from there

The EncryptedDataModel

This is the result of the encryption process

public class EncryptedDataModel
{
    public byte[] AesGcmNonce { get; set; } = RandomNumberGenerator.GetBytes(12);
    public byte[] EncryptedAesKey { get; set; } = Array.Empty<byte>();
    public byte[] AesGcmCipherText { get; set; } = Array.Empty<byte>();
    public byte[] AesGcmTag { get; set; } = new byte[16];
}

The AesGcmNonce has to be unique so it's created on the fly with the RandomNumberGenerator, and the others are set during the encryption process

The Encryption and Decryption Process

Since copying and pasting the code here will explain it well enough, I'll just link to it and provide a high level flowchart for each process

Also, these don't follow the Single-Responsibility Principal of SOLID, so as it's currently implemented, it cannot be extracted into a new library

A better way would be to handle the retrieval of the asymmetric keys outside of the EncryptAsync and DecryptAsync methods and pass it in as RSAParameters

Encryption

Its method signature

public static async Task<EncryptedDataModel> EncryptAsync<T>(
	this T model, 
	HttpClient client, 
	IConfiguration configuration,
	ILogger logger) where T : class
{
	// ...
}

The full implementation is here

flowchart TD
	start[Start]
	utf8json[Serialize Model to UTF8 JSON]
	aesKey[Randomly Generate Aes Key]
	cipherTextModelSize[Set AesGcmCipherText Size to Model Size]
	aesGcm[Create AesGcm Instance with Aes Key]
	aesGcmEncrypt[Encrypt Model with Symmetric Encryption]
	discoveryDocument[Retrieve Discovery Document]
	extractJwk[Extract JWK and Create RSA Parameters]
	rsa[Create RSA Instance with RSA Parameters]
	encryptAesKey[Encrypt Aes Key with Asymmetric Encryption]
	returnEncryptedDataModel[Return EncryptedDataModel]
	last[End]

	start --> utf8json
	utf8json --> aesKey
	aesKey --> cipherTextModelSize
	cipherTextModelSize --> aesGcm
	aesGcm --> aesGcmEncrypt
	aesGcmEncrypt --> discoveryDocument
	discoveryDocument --> extractJwk
	extractJwk --> rsa
	rsa --> encryptAesKey
	encryptAesKey --> returnEncryptedDataModel
	returnEncryptedDataModel --> last

Loading

Sample Usage

var response = await client.PostAsync("api/customers", JsonContent.Create(await registerModel.EncryptAsync(client, configuration, logger)));

Decryption

Its method signature

public static async Task<T?> DecryptAsync<T>(
	this EncryptedDataModel encryptedDataModel, 
	ISigningCredentialStore signingCredentialStore,
	ILogger logger) where T : class
{
	// ...
}

The full implementation is here

flowchart TD
	start[Start]
	extractRsaSecurityKey[Extract RSA Security Key from Credential Store]
	rsa[Create RSA Instance with RSA Parameters]
	aesKey[Decrypt Encrypted Aes Key]
	modelAsBytes[Create Empty Byte Array Matching the Size of the Cipher Text]
	aesGcm[Create AesGcm Instance with Aes Key]
	attemptDecryption[Attempt Decryption with AesGcm]
	succeeds{Decryption Succeeds?}
	returnModelOfT[Deserialize Model to T and Return]
	returnNull[Return Null]
	last[End]

	start --> extractRsaSecurityKey
	extractRsaSecurityKey --> rsa
	rsa --> aesKey
	aesKey --> modelAsBytes
	modelAsBytes --> aesGcm
	aesGcm --> attemptDecryption
	attemptDecryption --> succeeds
	succeeds --> |Yes| returnModelOfT
	succeeds --> |No| returnNull
	returnModelOfT --> last
	returnNull --> last

Loading

Sample Usage

var registerModel = await encryptedDataModel.DecryptAsync<RegisterModel>(signingCredentialStore, logger);

Additional Resources

  • How Encryption Works
    • John Savill did a video on encryption that includes this type of hybrid encryption, and its major flaw
Clone this wiki locally