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.

March 21st, 2010 12:31 pm
[...] GoTinker » Hybrid Cryptosystem [...]
January 17th, 2011 1:24 pm
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!
February 1st, 2011 11:56 am
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!
February 1st, 2011 1:28 pm
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!
).