4

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?

ataravati
  • 8,891
  • 9
  • 57
  • 89
  • Can I ask why you are encrypting a GET url parameter? – Tommy Aug 08 '14 at 18:32
  • 1
    Because the GET url parameter is a confidential id. – ataravati Aug 08 '14 at 18:44
  • Your code above "should" work. I have code and a webconfig that looks almost exactly like this except I'm using UTF8 and I have some string compression. Are you sure nothing is happening to the strings in between? Does your "encodedValue" look different everytime with the input string? Are you sure your machinekey element is in the right place in the webconfig? – JuhaKangas Aug 13 '14 at 13:38
  • @JuhaKangas, this code works. The only problem is that the encrypted string looks different every time with the input string. But, it still gets decrypted correctly. Is that how it's supposed to work? By the way, my `MachineKey` element is inside the `system.web` element in the `web.config` file. I updated the code in my question. – ataravati Aug 13 '14 at 14:10
  • 1
    Don't take my word for this, but I think that this how it works with this encryption yes. There's a seed value that is padded onto the encrypted data that is used during the encryption/decryption. You can probably read up on it somewhere. – JuhaKangas Aug 13 '14 at 15:21
  • @JuhaKangas, so you are getting the same results with your code? – ataravati Aug 13 '14 at 15:29
  • Why do you require the encryption to return the same value every time?What is the problem with it returning a new value each time? – Lasse V. Karlsen Aug 16 '14 at 21:02
  • @LasseV.Karlsen, because what I'm encrypting is a confidential id that is passed as a parameter in a query string. I want the url to be always the same for a given id. – ataravati Aug 17 '14 at 04:54
  • @ataravati you don't want reversable encryption then. You want a trap door function. But, why aren't you just decrypting the url? – Aron Aug 17 '14 at 05:25

2 Answers2

9

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.

ataravati
  • 8,891
  • 9
  • 57
  • 89
sga101
  • 1,904
  • 13
  • 12
  • 1
    Thank you! So, it looks like there's no way you can have the encrypted expression always the same when using `MachineKey.Protect()`. It would be a good idea for Microsoft to add a boolean parameter to the method for that purpose. – ataravati Aug 17 '14 at 22:29
0

Try adding a purpose argument to the Machine.Key.Proctect method! Like so.

public static class Key
{       
    public static string EncryptWithAPurpose(this string expression, string[] purpose)
    {
        if (string.IsNullOrEmpty(expression))
            return string.Empty;

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

    public static string DecryptWithAPurpose(this string expression, string[] purpose)
    {
        if (string.IsNullOrEmpty(expression))
            return string.Empty;

        byte[] stream = HttpServerUtility.UrlTokenDecode(expression);

        byte[] decodedValue = MachineKey.Unprotect(stream,purpose);
        return Encoding.Unicode.GetString(decodedValue);
    }

}

For testing:

  1. Add a Link -> @Html.ActionLink("Get new key", "GetKey", new { id = Guid.NewGuid()})
  2. Add the controller method
  3. Add the ViewModel
  4. Add the View.

{

public ActionResult GetKey(Guid id)  
{
            var vm = new vmGetKey();

            vm.Guid = id;

            //create a purpose
            var purpose = new string[] { "Test", "WithAPurpose" };

            //encrypt  key1
            vm.key1 = Key.EncryptWithAPurpose(id.ToString(),  purpose);

            //encrypt Key2
            vm.key2 = Key.EncryptWithAPurpose(id.ToString(), purpose);

            //decrypt key1
            vm.key1_DecryptResult = Key.DecryptWithAPurpose(vm.key1, purpose);

            //decrypt key2
            vm.key2_DecryptResult = Key.DecryptWithAPurpose(vm.key2, purpose);

            return View(vm);
}


public class vmGetKey
{
    public Guid Guid { get; set; }
    public string key1 { get; set; }
    public string key2 { get; set; }
    public string key1_DecryptResult { get; set; }
    public string key2_DecryptResult { get; set; }        
}        




    }

enter image description here

Igor
  • 298
  • 3
  • 8
  • I had tried it with a purpose before. It didn't work. I'll try again tomorrow and let you know. Thanks! – ataravati Aug 12 '14 at 23:16
  • I tired this again. And, as I expected, it didn't work. The `purpose` argument just adds one more level of security to the encryption. It does not have any impact on the behaviour I explained in my question. – ataravati Aug 13 '14 at 18:48