Search code examples
c#asp.netasp.net-mvcencryptionmachinekey

Encryption with MachineKey is not persistent


I use the MachineKey.Protect() method to encrypt the id passed as a query string in my asp.net MVC application.

Here's the code I use to encrypt/decrypt:

public static string Encrypt(this string expression)
{
    if (string.IsNullOrEmpty(expression))
        return string.Empty;

    byte[] stream = Encoding.Unicode.GetBytes(expression);
    byte[] encodedValue = MachineKey.Protect(stream);            
    return HttpServerUtility.UrlTokenEncode(encodedValue);            
}

public static string Decrypt(this string expression)
{
    if (string.IsNullOrEmpty(expression))
        return string.Empty;

    byte[] stream = HttpServerUtility.UrlTokenDecode(expression);
    byte[] decodedValue = MachineKey.Unprotect(stream);
    return Encoding.Unicode.GetString(decodedValue);
}

And, here is the MachineKey element in my web.config file:

<system.web>
    .
    .
    .
    <machineKey validationKey="xxx" decryptionKey="xxx" validation="SHA1" decryption="AES" />
</system.web>

The problem is the encrypted id is not persistent. Every time I call the method, I get a new encrypted expression. How do I make it persistent?


Solution

  • Summary:

    If you want to get the same result every time, you need to use a different method to protect your data. MachineKey.Protect uses a different IV for each run resulting in a different result every time.

    Detail

    Microsoft makes the source code for a lot of the dot net framework freely viewable on the internet.

    Starting from the top: MachineKey

    The protect method uses the AspNetCryptoServiceProvider

    If you follow the code through AspNetCryptoServiceProvider.GetCryptoService into NetFXCryptoService, you will find this:

    public byte[] Protect(byte[] clearData) {
            // The entire operation is wrapped in a 'checked' block because any overflows should be treated as failures.
            checked {
    
                // These SymmetricAlgorithm instances are single-use; we wrap it in a 'using' block.
                using (SymmetricAlgorithm encryptionAlgorithm = _cryptoAlgorithmFactory.GetEncryptionAlgorithm()) {
                    // Initialize the algorithm with the specified key and an appropriate IV
                    encryptionAlgorithm.Key = _encryptionKey.GetKeyMaterial();
    
                    if (_predictableIV) {
                        // The caller wanted the output to be predictable (e.g. for caching), so we'll create an
                        // appropriate IV directly from the input buffer. The IV length is equal to the block size.
                        encryptionAlgorithm.IV = CryptoUtil.CreatePredictableIV(clearData, encryptionAlgorithm.BlockSize);
                    }
                    else {
                        // If the caller didn't ask for a predictable IV, just let the algorithm itself choose one.
                        encryptionAlgorithm.GenerateIV();
                    }
                    byte[] iv = encryptionAlgorithm.IV;
    
                    using (MemoryStream memStream = new MemoryStream()) {
                        memStream.Write(iv, 0, iv.Length);
    
                        // At this point:
                        // memStream := IV
    
                        // Write the encrypted payload to the memory stream.
                        using (ICryptoTransform encryptor = encryptionAlgorithm.CreateEncryptor()) {
                            using (CryptoStream cryptoStream = new CryptoStream(memStream, encryptor, CryptoStreamMode.Write)) {
                                cryptoStream.Write(clearData, 0, clearData.Length);
                                cryptoStream.FlushFinalBlock();
    
                                // At this point:
                                // memStream := IV || Enc(Kenc, IV, clearData)
    
                                // These KeyedHashAlgorithm instances are single-use; we wrap it in a 'using' block.
                                using (KeyedHashAlgorithm signingAlgorithm = _cryptoAlgorithmFactory.GetValidationAlgorithm()) {
                                    // Initialize the algorithm with the specified key
                                    signingAlgorithm.Key = _validationKey.GetKeyMaterial();
    
                                    // Compute the signature
                                    byte[] signature = signingAlgorithm.ComputeHash(memStream.GetBuffer(), 0, (int)memStream.Length);
    
                                    // At this point:
                                    // memStream := IV || Enc(Kenc, IV, clearData)
                                    // signature := Sign(Kval, IV || Enc(Kenc, IV, clearData))
    
                                    // Append the signature to the encrypted payload
                                    memStream.Write(signature, 0, signature.Length);
    
                                    // At this point:
                                    // memStream := IV || Enc(Kenc, IV, clearData) || Sign(Kval, IV || Enc(Kenc, IV, clearData))
    
                                    // Algorithm complete
                                    byte[] protectedData = memStream.ToArray();
                                    return protectedData;
                                }
                            }
                        }
                    }
                }
            }
        }
    

    The class was initialised with the default options, so _predictableIV is false.

    Therefore, it uses a new IV every time, which means the result will be different everytime, even with the same input.

    The IV is included in the result so the Unprotect method can reverse the encryption.