Mar 21 2010

Hybrid Cryptosystem

Category: CryptographyMike Lovell @ 12:11 pm

When using ‘asymmetric’ (public-private key) encryption, there are a few limitations:

  • Slow Speed - Symmetric (key) encryption is far quicker
  • Single Recipient – You can only encrypt to a single public key

However we can combine ‘asymmetric’ and ‘symmetric’ cryposystems to create a hybrid system which will allow us to harness the speed of ‘symmetric’encryption and allow multiple recipients by simply encrypting the shared ‘symmetric key’ to each public ‘asymmetric key’ we want to use.

So lets create a simple format for a ‘hybrid cryptosystem‘.  We’re going to be using ‘RSA‘ for our ‘asymmetric encryption‘, and AES (Rijndael) for our ‘symmetric encryption‘.


1 Byte    Recepient count: 1-255 recepients allowed
4 Bytes   Recepient 1 data: Size (Int32)
20 Bytes  Recepient 1 data: SHA1 hash of public key
n Bytes   Recepient 1 data: Shared key/password (encrypted to public key)
...
4 Bytes   Recepient n data: Size (Int32)
20 Bytes  Recepient n data: SHA1 hash of public key
n Bytes   Recepient n data: Shared key/password (encrypted to public key)

n Bytes   Encrypted data: Encrypted using AES (Rijndael) and shared key/password

 

Now we have our format defined, we can implement a simple C# application that follows this.  First we’re going to implement a ‘CryptoProvider‘ class.  This is going to handle both ‘asymmetric‘ and ‘symmetric‘ encryption and decryption, key generation, hash generation and random byte generation.  Our key/password is going to be randomly generated and 48 bytes long.  The first 16 will be the ‘IV‘ vector, the last 32 will be the ‘key‘.

   1:  // Author: Mike Lovell (mike.lovell@gotinker.com)
   2:   
   3:  class CryptoProvider
   4:  {
   5:      private    RSACryptoServiceProvider    rsaProvider;
   6:   
   7:   
   8:      public CryptoProvider(int keySize)
   9:      {
  10:          rsaProvider = new RSACryptoServiceProvider(keySize);    // Generate new key
  11:      }
  12:   
  13:   
  14:      public CryptoProvider(string xmlKey)
  15:      {
  16:          rsaProvider = new RSACryptoServiceProvider();
  17:   
  18:          rsaProvider.FromXmlString(xmlKey);    // Import XML Key
  19:      }
  20:   
  21:   
  22:      public string ToXmlString(bool includePrivateKey)
  23:      {
  24:          return rsaProvider.ToXmlString(includePrivateKey);    // Export XML Key
  25:      }
  26:   
  27:   
  28:      public byte[] Hash()
  29:      {
  30:          var sha1 = new SHA1Managed();
  31:   
  32:          // Hash from the modulus
  33:          return sha1.ComputeHash(rsaProvider.ExportParameters(false).Modulus);
  34:      }
  35:   
  36:   
  37:      public byte[] Encrypt(byte[] data)
  38:      {
  39:          return rsaProvider.Encrypt(data, false);
  40:      }
  41:   
  42:   
  43:      public byte[] Decrypt(byte[] data)
  44:      {
  45:          return rsaProvider.Decrypt(data, false);
  46:      }
  47:   
  48:   
  49:      public static byte[] RandomSequence(int size)
  50:      {
  51:          var random        = new byte[size];
  52:          var rngProvider    = new RNGCryptoServiceProvider();
  53:  
  54:          rngProvider.GetBytes(random);
  55:   
  56:          return random;
  57:      }
  58:   
  59:   
  60:      public static byte[] Encrypt(byte[] data, byte[] password)
  61:      {
  62:          var rijndael    = Rijndael.Create();
  63:   
  64:          rijndael.KeySize    = 256;    // Maximum key size
  65:          rijndael.IV            = SubBytes(password, 0, 16);    // First 16 bytes is vector
  66:          rijndael.Key        = TrimBytes(SubBytes(password, 16, password.Length - 16), 32);    // Last 32 is key
  67:          rijndael.Mode        = CipherMode.CBC;
  68:   
  69:          var stream            = new MemoryStream();
  70:          var cryptoStream    = new CryptoStream(stream, rijndael.CreateEncryptor(), CryptoStreamMode.Write);
  71:   
  72:          cryptoStream.Write(data, 0, data.Length);
  73:          cryptoStream.FlushFinalBlock();
  74:   
  75:          cryptoStream.Close();
  76:   
  77:          var encryptedData = stream.ToArray();
  78:   
  79:          stream.Close();
  80:   
  81:          return encryptedData;
  82:      }
  83:   
  84:   
  85:      public static byte[] Decrypt(byte[] data, byte[] password)
  86:      {
  87:          var rijndael    = Rijndael.Create();
  88:   
  89:          rijndael.KeySize    = 256;    // Maximum key size
  90:          rijndael.IV            = SubBytes(password, 0, 16);    // First 16 bytes is vector
  91:          rijndael.Key        = TrimBytes(SubBytes(password, 16, password.Length - 16), 32);    // Last 32 is key
  92:          rijndael.Mode        = CipherMode.CBC;
  93:   
  94:          var stream            = new MemoryStream(data);
  95:          var cryptoStream    = new CryptoStream(stream, rijndael.CreateDecryptor(), CryptoStreamMode.Read);
  96:   
  97:          var    length            = cryptoStream.Read(data, 0, data.Length);
  98:          var    decryptedData    = new byte[length];
  99:   
 100:          stream.Read(decryptedData, 0, length);
 101:   
 102:          cryptoStream.Close();
 103:          stream.Close();
 104:   
 105:          Buffer.BlockCopy(data, 0, decryptedData, 0, length);
 106:   
 107:          return decryptedData;
 108:      }
 109:   
 110:   
 111:      public byte[] DecryptPasswordAndData(byte[] data)
 112:      {
 113:          var    buffer            = new MemoryStream(data);
 114:          var    keyCount        = buffer.ReadByte();    // First byte is key count
 115:          var    thisHash        = Convert.ToBase64String(this.Hash());
 116:          var    password        = (byte[])null;
 117:   
 118:          if (keyCount < 1) throw new Exception("No keys found");
 119:  
 120:          var    keyLengthBuffer    = new byte[4];
 121:  
 122:          for (int i=0; i < keyCount; i++)
 123:          {
 124:              // First 4 bytes are Int32 of encrypted hash/password size
 125:              buffer.Read(keyLengthBuffer, 0, 4);
 126:   
 127:              var    keyLength    = BitConverter.ToInt32(keyLengthBuffer, 0);
 128:              var    keyHash        = new byte[20];    // First 20 bytes is SHA1 hash
 129:              var    keyPassword    = new byte[keyLength - 20];    // Remaining byes are encrypted password
 130:   
 131:              buffer.Read(keyHash, 0, 20);    // Read first 20 bytes (SHA1 Hash)
 132:              buffer.Read(keyPassword, 0, keyPassword.Length);    // Read remaining bytes (password)
 133:   
 134:              if (Convert.ToBase64String(keyHash) == thisHash)
 135:              {
 136:                  password = Decrypt(keyPassword);    // Decrypt password
 137:              }
 138:          }
 139:   
 140:          if (password == null) throw new Exception("Data not encrypted to your key");
 141:   
 142:          // remaining bytes are encrypted data
 143:          var encryptedData     = new byte[buffer.Length - buffer.Position];
 144:   
 145:          // Read remaining bytes
 146:          buffer.Read(encryptedData, 0, encryptedData.Length);
 147:   
 148:          buffer.Close();
 149:   
 150:          return CryptoProvider.Decrypt(encryptedData, password);    // Decrypt data
 151:      }
 152:   
 153:   
 154:      private static byte[] SubBytes(byte[] source, int offset, int length)
 155:      {
 156:          var    subData    = new byte[length - offset];
 157:   
 158:          Buffer.BlockCopy(source, offset, subData, 0, length - offset);
 159:   
 160:          return subData;
 161:      }
 162:   
 163:   
 164:      private static byte[] TrimBytes(byte[] source, int maxLength)
 165:      {
 166:          if (source.Length > maxLength)
 167:          {
 168:              var trimmedData = new byte[maxLength];
 169:   
 170:              Buffer.BlockCopy(source, 0, trimmedData, 0, maxLength);
 171:   
 172:              return trimmedData;
 173:          }
 174:   
 175:          return source;
 176:      }
 177:  }
 178:   

Then lets create a collection of ‘CryptoProvider‘ which will allow us to encrypt our data to multiple public keys.

 179:   
 180:  class CryptoProviderCollection : List<CryptoProvider>
 181:  {
 182:      public byte[] EncryptPasswordAndData(byte[] data)
 183:      {
 184:          if (this.Count < 1 || this.Count > 255) throw new Exception("Too few or too many recepients");
 185:   
 186:          var password    = CryptoProvider.RandomSequence(16 + 32);    // Random password
 187:                                                                      // First 16 = Vector
 188:                                                                      // Last 32 = Key
 189:          var stream        = new MemoryStream();
 190:  
 191:          stream.WriteByte((byte)this.Count); // First byte is recepient count
 192:   
 193:          var encryptedPasswords    =
 194:              (
 195:                  from    item
 196:                  in        this
 197:                  select    CombineBytes
 198:                      (
 199:                          item.Hash(),            // Public key hash
 200:                          item.Encrypt(password)    // Password encrypted for recipient
 201:                      )
 202:              );
 203:   
 204:   
 205:          foreach (var encryptedPassword in encryptedPasswords)
 206:          {    // For each recepient
 207:   
 208:              // Store encrypted password length as Int32 (first 4 bytes)
 209:              stream.Write(BitConverter.GetBytes(encryptedPassword.Length), 0, 4);
 210:              // Store encrypted password
 211:              stream.Write(encryptedPassword, 0,  encryptedPassword.Length);
 212:          }
 213:   
 214:          var encryptedData = CryptoProvider.Encrypt(data, password);    // Encrypt data
 215:   
 216:          stream.Write(encryptedData, 0, encryptedData.Length);        // Store
 217:   
 218:          var encryptedBytes = stream.ToArray();
 219:   
 220:          stream.Close();
 221:   
 222:          return encryptedBytes;
 223:      }
 224:   
 225:   
 226:      private byte[] CombineBytes(byte[] dataA, byte[] dataB)
 227:      {
 228:          var combinedData = new byte[dataA.Length + dataB.Length];
 229:   
 230:          Buffer.BlockCopy(dataA, 0, combinedData, 0, dataA.Length);
 231:          Buffer.BlockCopy(dataB, 0, combinedData, dataA.Length, dataB.Length);
 232:   
 233:          return combinedData;
 234:      }
 235:  }
 236:   

Now lets see if it works.  We’ll create 3 ‘RSA‘ keys then encrypt the data for the first two recipients ONLY, just to see if the 3rd cannot decrypt the data (which should be the case).

 237:   
 238:  class Program
 239:  {
 240:      static void Main(string[] args)
 241:      {
 242:          var    key1    = new CryptoProvider(1024);    // Make a new key
 243:          var    key2    = new CryptoProvider(2048);    // ..
 244:          var    key3    = new CryptoProvider(1024);    // ..
 245:   
 246:          var col    = new CryptoProviderCollection();
 247:  
 248:          col.Add(key1);    // Add only key1 and key2
 249:          col.Add(key2);    // to the collection
 250:   
 251:          // Encrypt data and random password for key1 and key2
 252:          var encryptedData    = col.EncryptPasswordAndData(Encoding.UTF8.GetBytes("Test Encrypted Data"));
 253:  
 254:          // Decrypt using key1
 255:          Console.WriteLine(Encoding.UTF8.GetString(key1.DecryptPasswordAndData(encryptedData)));
 256:          // Decrypt using key2
 257:          Console.WriteLine(Encoding.UTF8.GetString(key2.DecryptPasswordAndData(encryptedData)));
 258:  
 259:          try
 260:          {
 261:              // Decrypt using key3, that we DIDN'T use for encryption, this will fail
 262:              Console.WriteLine(Encoding.UTF8.GetString(key3.DecryptPasswordAndData(encryptedData)));
 263:          }
 264:          catch (Exception e)
 265:          {
 266:              Console.WriteLine("ERROR: {0}",  e.Message.ToString());
 267:          }
 268:   
 269:          Console.ReadLine();
 270:      }
 271:  }

And the result you should see is the following:

Test Encrypted Data
Test Encrypted Data
ERROR: Data not encrypted to your key

You can expand the classes to carry out ‘exception handling‘, and build a robust ‘hybrid cryptosystem‘ from this basic implementation.

Download Visual Studio 2010 Project (8.33k)

Tags: , , , , , , , , , ,

4 Responses to “Hybrid Cryptosystem”

  1. Hanna Moorhead says:

    Clear blog. Questioning if you happen to ever trade visitor articles? I am working a web site on my latest obsession water filters and seeking to trade some articles with good pages. I checked out your blog and you have got some good articles and I feel our guests would each find value. Thanks!

  2. Bill says:

    Thank you for the wonderful example.

    I was hoping to be able to use this as a base for a larger encryption project, potentially requiring more than 255 recipients for any given message. From looking at the code it looks like a simple change in the number of bytes read/written (when adding recipient count to the MemoryStream) would make more recipients possible, but I don’t know enough about cryptography to know if 255 is an algorithm limitation, bad practice/not recommended, etc.

    Can you explain a little more as to why you chose this value and whether or not more recipients would be possible/recommended?

    Thanks again!

  3. Mike Lovell says:

    255 was completely arbitrary, just as it happened to fit inside a byte – It seemed to pass the “three bears” test (not to little, not too much!). There shouldn’t be a limit on how many recipients you can add providing you allow for this in your schema (and keep the value as “always” the same data type unless you add a mechanism to allow it to change).

    It should be as simple as changing the 1st item in the schema:

    1 Byte Recepient count: 1-255 recepients allowed

    To 4 bytes and take it to an unsigned int32 (as of course you cannot have a negative amount of recipients). Then use the BitConverter to retrieve the int (rather than just taking the byte directly).

    So in short, no, it’s not an algorithm limitation or bad practice. Use whatever value your project requires (if you have to go to an Uint64 I’d really like to know what you’re working on though! :-) ).

Leave a Reply