13

is there a way that you can specify a separate encryption and validation key. Currently, there is just one master key that does both validation and encryption. However, we have several applications in a web farm and only one of them run on ASP.NET CORE and this is hosted on IIS. The rest of the application (Running on ASP.NET *Not core) use the same machine key. The machine key has, of course, the decryption and validation keys and all the other applications use this same machine key to synchronize data between them. I would also like to have the CORE app synchronized with the same keys. Currently, the core app has this. The IDataProtector uses the master to validate and encrypt/decrypt.

  <?xml version="1.0" encoding="utf-8"?>
    <key id="6015093e-8571-4244-8824-17157f248d13" version="1">
      <creationDate>2017-10-03T12:13:26.6902857Z</creationDate>
      <activationDate>2017-10-03T13:13:26.6897307+01:00</activationDate>
      <expirationDate>2017-11-03T13:13:26.6898152+01:00</expirationDate>
      <descriptor>
        <descriptor>
          <encryption algorithm="AES_256_CBC" />
          <validation algorithm="HMACSHA256" />
          <masterKey p4:requiresEncryption="true" xmlns:p4="http://schemas.asp.net/2015/03/dataProtection">
            <value>**This is the key**</value>
          </masterKey>
        </descriptor>
      </descriptor>
    </key>

I would like to have something like this

    <descriptor>
      <encryption algorithm="AES_256_CBC" />
      <validation algorithm="HMACSHA256" />
      <encryptionKey p4:requiresEncryption="true" xmlns:p4="http://schemas.asp.net/2015/03/dataProtection">
        <!-- Warning: the key below is in an unencrypted form. -->
        <value>encrypt key</value>
      </encryptionKey>
      <decryptionKey p4:requiresEncryption="true" xmlns:p4="http://schemas.asp.net/2015/03/dataProtection">
        <!-- Warning: the key below is in an unencrypted form. -->
        <value>validation key</value>
      </decryptionKey>
    </descriptor>
  </descriptor>

Specifying the separate validation and encryption keys. Is something like this possible?

Umar Karimabadi
  • 960
  • 8
  • 20

1 Answers1

9

I only needed the MachineKey.UnProtect function. I could not get anything to work with the APIs from ASP.NET CORE so I had no choice but to stitch up the source code from the .net Framework. The following code ended up working for me to unprotect something.

public static class MachineKey
    {
        private static readonly UTF8Encoding SecureUTF8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
        public static byte[] Unprotect(byte[] protectedData, string validationKey, string encKey, params string[] specificPurposes)
        {
            // The entire operation is wrapped in a 'checked' block because any overflows should be treated as failures.
            checked
            {
                using (SymmetricAlgorithm decryptionAlgorithm = new AesCryptoServiceProvider())
                {

                    decryptionAlgorithm.Key = SP800_108.DeriveKey(HexToBinary(encKey), "User.MachineKey.Protect", specificPurposes);

                    // These KeyedHashAlgorithm instances are single-use; we wrap it in a 'using' block.
                    using (KeyedHashAlgorithm validationAlgorithm = new HMACSHA256())
                    {
                        validationAlgorithm.Key = SP800_108.DeriveKey(HexToBinary(validationKey), "User.MachineKey.Protect", specificPurposes);

                        int ivByteCount = decryptionAlgorithm.BlockSize / 8; 
                        int signatureByteCount = validationAlgorithm.HashSize / 8;
                        int encryptedPayloadByteCount = protectedData.Length - ivByteCount - signatureByteCount;
                        if (encryptedPayloadByteCount <= 0)
                        {
                            return null;
                        }

                        byte[] computedSignature = validationAlgorithm.ComputeHash(protectedData, 0, ivByteCount + encryptedPayloadByteCount);

                        if (!BuffersAreEqual(
                            buffer1: protectedData, buffer1Offset: ivByteCount + encryptedPayloadByteCount, buffer1Count: signatureByteCount,
                            buffer2: computedSignature, buffer2Offset: 0, buffer2Count: computedSignature.Length))
                        {

                            return null;
                        }

                        byte[] iv = new byte[ivByteCount];
                        Buffer.BlockCopy(protectedData, 0, iv, 0, iv.Length);
                        decryptionAlgorithm.IV = iv;

                        using (MemoryStream memStream = new MemoryStream())
                        {
                            using (ICryptoTransform decryptor = decryptionAlgorithm.CreateDecryptor())
                            {
                                using (CryptoStream cryptoStream = new CryptoStream(memStream, decryptor, CryptoStreamMode.Write))
                                {
                                    cryptoStream.Write(protectedData, ivByteCount, encryptedPayloadByteCount);
                                    cryptoStream.FlushFinalBlock();

                                    byte[] clearData = memStream.ToArray();

                                    return clearData;
                                }
                            }
                        }
                    }
                }
            }
        }

        private static bool BuffersAreEqual(byte[] buffer1, int buffer1Offset, int buffer1Count, byte[] buffer2, int buffer2Offset, int buffer2Count)
        {
            bool success = (buffer1Count == buffer2Count); // can't possibly be successful if the buffers are of different lengths
            for (int i = 0; i < buffer1Count; i++)
            {
                success &= (buffer1[buffer1Offset + i] == buffer2[buffer2Offset + (i % buffer2Count)]);
            }
            return success;
        }

        private static class SP800_108
        {

            public static byte[] DeriveKey(byte[] keyDerivationKey, string primaryPurpose, params string[] specificPurposes)
            {
                using (HMACSHA512 hmac = new HMACSHA512(keyDerivationKey))
                {

                    GetKeyDerivationParameters(out byte[] label, out byte[] context, primaryPurpose, specificPurposes);

                    byte[] derivedKey = DeriveKeyImpl(hmac, label, context, keyDerivationKey.Length * 8);

                    return derivedKey;
                }
            }

            private static byte[] DeriveKeyImpl(HMAC hmac, byte[] label, byte[] context, int keyLengthInBits)
            {
                checked
                {
                    int labelLength = (label != null) ? label.Length : 0;
                    int contextLength = (context != null) ? context.Length : 0;
                    byte[] buffer = new byte[4 /* [i]_2 */ + labelLength /* label */ + 1 /* 0x00 */ + contextLength /* context */ + 4 /* [L]_2 */];

                    if (labelLength != 0)
                    {
                        Buffer.BlockCopy(label, 0, buffer, 4, labelLength); // the 4 accounts for the [i]_2 length
                    }
                    if (contextLength != 0)
                    {
                        Buffer.BlockCopy(context, 0, buffer, 5 + labelLength, contextLength); // the '5 +' accounts for the [i]_2 length, the label, and the 0x00 byte
                    }
                    WriteUInt32ToByteArrayBigEndian((uint)keyLengthInBits, buffer, 5 + labelLength + contextLength); // the '5 +' accounts for the [i]_2 length, the label, the 0x00 byte, and the context

                    int numBytesWritten = 0;
                    int numBytesRemaining = keyLengthInBits / 8;
                    byte[] output = new byte[numBytesRemaining];

                    for (uint i = 1; numBytesRemaining > 0; i++)
                    {
                        WriteUInt32ToByteArrayBigEndian(i, buffer, 0); // set the first 32 bits of the buffer to be the current iteration value
                        byte[] K_i = hmac.ComputeHash(buffer);

                        // copy the leftmost bits of K_i into the output buffer
                        int numBytesToCopy = Math.Min(numBytesRemaining, K_i.Length);
                        Buffer.BlockCopy(K_i, 0, output, numBytesWritten, numBytesToCopy);
                        numBytesWritten += numBytesToCopy;
                        numBytesRemaining -= numBytesToCopy;
                    }

                    // finished
                    return output;
                }
            }

            private static void WriteUInt32ToByteArrayBigEndian(uint value, byte[] buffer, int offset)
            {
                buffer[offset + 0] = (byte)(value >> 24);
                buffer[offset + 1] = (byte)(value >> 16);
                buffer[offset + 2] = (byte)(value >> 8);
                buffer[offset + 3] = (byte)(value);
            }

        }

        private static void GetKeyDerivationParameters(out byte[] label, out byte[] context, string primaryPurpose, params string[] specificPurposes)
        {
            label = SecureUTF8Encoding.GetBytes(primaryPurpose);

                using (MemoryStream stream = new MemoryStream())
                using (BinaryWriter writer = new BinaryWriter(stream, SecureUTF8Encoding))
                {
                    foreach (string specificPurpose in specificPurposes)
                    {
                        writer.Write(specificPurpose);
                    }
                    context = stream.ToArray();
                }
        }

        private static byte[] HexToBinary(string data)
        {
            if (data == null || data.Length % 2 != 0)
            {
                // input string length is not evenly divisible by 2
                return null;
            }

            byte[] binary = new byte[data.Length / 2];

            for (int i = 0; i < binary.Length; i++)
            {
                int highNibble = HexToInt(data[2 * i]);
                int lowNibble = HexToInt(data[2 * i + 1]);

                if (highNibble == -1 || lowNibble == -1)
                {
                    return null; // bad hex data
                }
                binary[i] = (byte)((highNibble << 4) | lowNibble);
            }

            int HexToInt(char h)
            {
                return (h >= '0' && h <= '9') ? h - '0' :
                (h >= 'a' && h <= 'f') ? h - 'a' + 10 :
                (h >= 'A' && h <= 'F') ? h - 'A' + 10 :
                -1;
            }
            return binary;
        }

    } 

[EXAMPLE]

var message = "My secret message";

var encodedMessage = Encoding.ASCII.GetBytes(message);

var protectedMessage = MachineKey.Protect(encodedMessage, "My Purpose");

var protectedMessageAsBase64 = Convert.ToBase64String(protectedMessage);

// Now make sure you reverse the process 

var convertFromBase64 = Convert.FromBase64String(protectedMessageAsBase64);

var unProtectedMessage = MachineKey.Unprotect(convertFromBase64, "Your validation key", "Your encryption key", "My Purpose");

var decodedMessage = Encoding.ASCII.GetString(unProtectedMessage);

This is just a simple example. First, make sure you have the correct validation and encryption keys from IIS. This may seem like an obvious point but it drove me mad because I was using the wrong keys. Next, make sure you know what purpose the message was enrypted with. In my Example, the purpose is "My purpose". If the message was encrypted without a purpose, just leave the purpose paramter out when you unprotect something. Finally, you have to know how your encrypted message has been presented to you. Is it base64 encoded, for example, you need to know this so you can do the reverse.

Umar Karimabadi
  • 960
  • 8
  • 20
  • Can you share how you used this, please? I'm trying to decrypt an AuthenticationTicket and am using what you provided above. However, my BuffersAreEqual is ending up being false and I suspect it's because I'm not specifying the correct "specificPurposes" that the original was protected with. – dmarlow Nov 04 '17 at 00:40
  • I posted an example, we have this working in production for one of our internal tools so I can tell you this works 100%. If you are having troubles please let me know. – Umar Karimabadi Nov 04 '17 at 20:38
  • 4
    I got it working, thanks Umar. My issue was that I was previously using HMACSHA1 (configured in my web.config/machineKey). Once I used that, it started working as expected. I've made a NuGet package in case others want to use it how I'm using it: https://github.com/dmarlow/BearerTokenBridge – dmarlow Nov 05 '17 at 21:33
  • 1
    Awesome, you made it into a package : ) I'll update my answer to mention how you can use the different encryption and validation algorithms. – Umar Karimabadi Nov 05 '17 at 21:43
  • I got it working as well. Big thanks to Umar, dmarlow. I used this nuget package AspNetTicketBridge (source https://github.com/dmarlow/AspNetTicketBridge). It is very important to have same keys as your IIS (see where to get that info for Azure AppService https://rimdev.io/find-auto-generated-aspnet-machine-key-in-azure-web-apps/). Another important thing was that the default validation algorithm is now HMACSHA256 instead of HMAC1) – Alex Sorokoletov Oct 07 '19 at 01:34
  • This works great, got me out of a big hole. Thank you! I had to change using (KeyedHashAlgorithm validationAlgorithm = new HMACSHA256()) to using (KeyedHashAlgorithm validationAlgorithm = new HMACSHA512()) But apart from that, it just worked! – Toby Simmerling Jun 02 '20 at 13:10
  • @dmarlow I tried your package but initially I was getting null when I unprotected my token so I downloaded the code and stepped through it and noticed my token's version was coming up as version 2. Can you elaborate on what the token version is about and how I might change that on my authorization server? – Scott Wilson Dec 04 '21 at 00:36
  • Can someone shed some light on this format version thing, it's completely unclear how this is set or how to change it and I can't find any information about it. – Scott Wilson Dec 09 '21 at 01:32
  • Since I couldn't get any information on this version format thing or find any examples of how to accomplish this with ASP.NET Core Data Protection I created yet another repository for this. https://github.com/m1is/TokenUnprotector – Scott Wilson Dec 10 '21 at 20:16