• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.bouncycastle.crypto.modes;
2 
3 import java.io.ByteArrayOutputStream;
4 
5 import org.bouncycastle.crypto.BlockCipher;
6 import org.bouncycastle.crypto.CipherParameters;
7 import org.bouncycastle.crypto.DataLengthException;
8 import org.bouncycastle.crypto.InvalidCipherTextException;
9 import org.bouncycastle.crypto.Mac;
10 import org.bouncycastle.crypto.OutputLengthException;
11 import org.bouncycastle.crypto.macs.CBCBlockCipherMac;
12 import org.bouncycastle.crypto.params.AEADParameters;
13 import org.bouncycastle.crypto.params.ParametersWithIV;
14 import org.bouncycastle.util.Arrays;
15 
16 /**
17  * Implements the Counter with Cipher Block Chaining mode (CCM) detailed in
18  * NIST Special Publication 800-38C.
19  * <p>
20  * <b>Note</b>: this mode is a packet mode - it needs all the data up front.
21  */
22 public class CCMBlockCipher
23     implements AEADBlockCipher
24 {
25     private BlockCipher           cipher;
26     private int                   blockSize;
27     private boolean               forEncryption;
28     private byte[]                nonce;
29     private byte[]                initialAssociatedText;
30     private int                   macSize;
31     private CipherParameters      keyParam;
32     private byte[]                macBlock;
33     private ExposedByteArrayOutputStream associatedText = new ExposedByteArrayOutputStream();
34     private ExposedByteArrayOutputStream data = new ExposedByteArrayOutputStream();
35 
36     /**
37      * Basic constructor.
38      *
39      * @param c the block cipher to be used.
40      */
CCMBlockCipher(BlockCipher c)41     public CCMBlockCipher(BlockCipher c)
42     {
43         this.cipher = c;
44         this.blockSize = c.getBlockSize();
45         this.macBlock = new byte[blockSize];
46 
47         if (blockSize != 16)
48         {
49             throw new IllegalArgumentException("cipher required with a block size of 16.");
50         }
51     }
52 
53     /**
54      * return the underlying block cipher that we are wrapping.
55      *
56      * @return the underlying block cipher that we are wrapping.
57      */
getUnderlyingCipher()58     public BlockCipher getUnderlyingCipher()
59     {
60         return cipher;
61     }
62 
63 
init(boolean forEncryption, CipherParameters params)64     public void init(boolean forEncryption, CipherParameters params)
65           throws IllegalArgumentException
66     {
67         this.forEncryption = forEncryption;
68 
69         CipherParameters cipherParameters;
70         if (params instanceof AEADParameters)
71         {
72             AEADParameters param = (AEADParameters)params;
73 
74             nonce = param.getNonce();
75             initialAssociatedText = param.getAssociatedText();
76             macSize = param.getMacSize() / 8;
77             cipherParameters = param.getKey();
78         }
79         else if (params instanceof ParametersWithIV)
80         {
81             ParametersWithIV param = (ParametersWithIV)params;
82 
83             nonce = param.getIV();
84             initialAssociatedText = null;
85             macSize = macBlock.length / 2;
86             cipherParameters = param.getParameters();
87         }
88         else
89         {
90             throw new IllegalArgumentException("invalid parameters passed to CCM: " + params.getClass().getName());
91         }
92 
93         // NOTE: Very basic support for key re-use, but no performance gain from it
94         if (cipherParameters != null)
95         {
96             keyParam = cipherParameters;
97         }
98 
99         if (nonce == null || nonce.length < 7 || nonce.length > 13)
100         {
101             throw new IllegalArgumentException("nonce must have length from 7 to 13 octets");
102         }
103 
104         reset();
105     }
106 
getAlgorithmName()107     public String getAlgorithmName()
108     {
109         return cipher.getAlgorithmName() + "/CCM";
110     }
111 
processAADByte(byte in)112     public void processAADByte(byte in)
113     {
114         associatedText.write(in);
115     }
116 
processAADBytes(byte[] in, int inOff, int len)117     public void processAADBytes(byte[] in, int inOff, int len)
118     {
119         // TODO: Process AAD online
120         associatedText.write(in, inOff, len);
121     }
122 
processByte(byte in, byte[] out, int outOff)123     public int processByte(byte in, byte[] out, int outOff)
124         throws DataLengthException, IllegalStateException
125     {
126         data.write(in);
127 
128         return 0;
129     }
130 
processBytes(byte[] in, int inOff, int inLen, byte[] out, int outOff)131     public int processBytes(byte[] in, int inOff, int inLen, byte[] out, int outOff)
132         throws DataLengthException, IllegalStateException
133     {
134         if (in.length < (inOff + inLen))
135         {
136             throw new DataLengthException("Input buffer too short");
137         }
138         data.write(in, inOff, inLen);
139 
140         return 0;
141     }
142 
doFinal(byte[] out, int outOff)143     public int doFinal(byte[] out, int outOff)
144         throws IllegalStateException, InvalidCipherTextException
145     {
146         int len = processPacket(data.getBuffer(), 0, data.size(), out, outOff);
147 
148         reset();
149 
150         return len;
151     }
152 
reset()153     public void reset()
154     {
155         cipher.reset();
156         associatedText.reset();
157         data.reset();
158     }
159 
160     /**
161      * Returns a byte array containing the mac calculated as part of the
162      * last encrypt or decrypt operation.
163      *
164      * @return the last mac calculated.
165      */
getMac()166     public byte[] getMac()
167     {
168         byte[] mac = new byte[macSize];
169 
170         System.arraycopy(macBlock, 0, mac, 0, mac.length);
171 
172         return mac;
173     }
174 
getUpdateOutputSize(int len)175     public int getUpdateOutputSize(int len)
176     {
177         return 0;
178     }
179 
getOutputSize(int len)180     public int getOutputSize(int len)
181     {
182         int totalData = len + data.size();
183 
184         if (forEncryption)
185         {
186              return totalData + macSize;
187         }
188 
189         return totalData < macSize ? 0 : totalData - macSize;
190     }
191 
192     /**
193      * Process a packet of data for either CCM decryption or encryption.
194      *
195      * @param in data for processing.
196      * @param inOff offset at which data starts in the input array.
197      * @param inLen length of the data in the input array.
198      * @return a byte array containing the processed input..
199      * @throws IllegalStateException if the cipher is not appropriately set up.
200      * @throws InvalidCipherTextException if the input data is truncated or the mac check fails.
201      */
processPacket(byte[] in, int inOff, int inLen)202     public byte[] processPacket(byte[] in, int inOff, int inLen)
203         throws IllegalStateException, InvalidCipherTextException
204     {
205         byte[] output;
206 
207         if (forEncryption)
208         {
209             output = new byte[inLen + macSize];
210         }
211         else
212         {
213             if (inLen < macSize)
214             {
215                 throw new InvalidCipherTextException("data too short");
216             }
217             output = new byte[inLen - macSize];
218         }
219 
220         processPacket(in, inOff, inLen, output, 0);
221 
222         return output;
223     }
224 
225     /**
226      * Process a packet of data for either CCM decryption or encryption.
227      *
228      * @param in data for processing.
229      * @param inOff offset at which data starts in the input array.
230      * @param inLen length of the data in the input array.
231      * @param output output array.
232      * @param outOff offset into output array to start putting processed bytes.
233      * @return the number of bytes added to output.
234      * @throws IllegalStateException if the cipher is not appropriately set up.
235      * @throws InvalidCipherTextException if the input data is truncated or the mac check fails.
236      * @throws DataLengthException if output buffer too short.
237      */
processPacket(byte[] in, int inOff, int inLen, byte[] output, int outOff)238     public int processPacket(byte[] in, int inOff, int inLen, byte[] output, int outOff)
239         throws IllegalStateException, InvalidCipherTextException, DataLengthException
240     {
241         // TODO: handle null keyParam (e.g. via RepeatedKeySpec)
242         // Need to keep the CTR and CBC Mac parts around and reset
243         if (keyParam == null)
244         {
245             throw new IllegalStateException("CCM cipher unitialized.");
246         }
247 
248         int n = nonce.length;
249         int q = 15 - n;
250         if (q < 4)
251         {
252             int limitLen = 1 << (8 * q);
253             if (inLen >= limitLen)
254             {
255                 throw new IllegalStateException("CCM packet too large for choice of q.");
256             }
257         }
258 
259         byte[] iv = new byte[blockSize];
260         iv[0] = (byte)((q - 1) & 0x7);
261         System.arraycopy(nonce, 0, iv, 1, nonce.length);
262 
263         BlockCipher ctrCipher = new SICBlockCipher(cipher);
264         ctrCipher.init(forEncryption, new ParametersWithIV(keyParam, iv));
265 
266         int outputLen;
267         int inIndex = inOff;
268         int outIndex = outOff;
269 
270         if (forEncryption)
271         {
272             outputLen = inLen + macSize;
273             if (output.length < (outputLen + outOff))
274             {
275                 throw new OutputLengthException("Output buffer too short.");
276             }
277 
278             calculateMac(in, inOff, inLen, macBlock);
279 
280             byte[] encMac = new byte[blockSize];
281 
282             ctrCipher.processBlock(macBlock, 0, encMac, 0);   // S0
283 
284             while (inIndex < (inOff + inLen - blockSize))                 // S1...
285             {
286                 ctrCipher.processBlock(in, inIndex, output, outIndex);
287                 outIndex += blockSize;
288                 inIndex += blockSize;
289             }
290 
291             byte[] block = new byte[blockSize];
292 
293             System.arraycopy(in, inIndex, block, 0, inLen + inOff - inIndex);
294 
295             ctrCipher.processBlock(block, 0, block, 0);
296 
297             System.arraycopy(block, 0, output, outIndex, inLen + inOff - inIndex);
298 
299             System.arraycopy(encMac, 0, output, outOff + inLen, macSize);
300         }
301         else
302         {
303             if (inLen < macSize)
304             {
305                 throw new InvalidCipherTextException("data too short");
306             }
307             outputLen = inLen - macSize;
308             if (output.length < (outputLen + outOff))
309             {
310                 throw new OutputLengthException("Output buffer too short.");
311             }
312 
313             System.arraycopy(in, inOff + outputLen, macBlock, 0, macSize);
314 
315             ctrCipher.processBlock(macBlock, 0, macBlock, 0);
316 
317             for (int i = macSize; i != macBlock.length; i++)
318             {
319                 macBlock[i] = 0;
320             }
321 
322             while (inIndex < (inOff + outputLen - blockSize))
323             {
324                 ctrCipher.processBlock(in, inIndex, output, outIndex);
325                 outIndex += blockSize;
326                 inIndex += blockSize;
327             }
328 
329             byte[] block = new byte[blockSize];
330 
331             System.arraycopy(in, inIndex, block, 0, outputLen - (inIndex - inOff));
332 
333             ctrCipher.processBlock(block, 0, block, 0);
334 
335             System.arraycopy(block, 0, output, outIndex, outputLen - (inIndex - inOff));
336 
337             byte[] calculatedMacBlock = new byte[blockSize];
338 
339             calculateMac(output, outOff, outputLen, calculatedMacBlock);
340 
341             if (!Arrays.constantTimeAreEqual(macBlock, calculatedMacBlock))
342             {
343                 throw new InvalidCipherTextException("mac check in CCM failed");
344             }
345         }
346 
347         return outputLen;
348     }
349 
calculateMac(byte[] data, int dataOff, int dataLen, byte[] macBlock)350     private int calculateMac(byte[] data, int dataOff, int dataLen, byte[] macBlock)
351     {
352         Mac cMac = new CBCBlockCipherMac(cipher, macSize * 8);
353 
354         cMac.init(keyParam);
355 
356         //
357         // build b0
358         //
359         byte[] b0 = new byte[16];
360 
361         if (hasAssociatedText())
362         {
363             b0[0] |= 0x40;
364         }
365 
366         b0[0] |= (((cMac.getMacSize() - 2) / 2) & 0x7) << 3;
367 
368         b0[0] |= ((15 - nonce.length) - 1) & 0x7;
369 
370         System.arraycopy(nonce, 0, b0, 1, nonce.length);
371 
372         int q = dataLen;
373         int count = 1;
374         while (q > 0)
375         {
376             b0[b0.length - count] = (byte)(q & 0xff);
377             q >>>= 8;
378             count++;
379         }
380 
381         cMac.update(b0, 0, b0.length);
382 
383         //
384         // process associated text
385         //
386         if (hasAssociatedText())
387         {
388             int extra;
389 
390             int textLength = getAssociatedTextLength();
391             if (textLength < ((1 << 16) - (1 << 8)))
392             {
393                 cMac.update((byte)(textLength >> 8));
394                 cMac.update((byte)textLength);
395 
396                 extra = 2;
397             }
398             else // can't go any higher than 2^32
399             {
400                 cMac.update((byte)0xff);
401                 cMac.update((byte)0xfe);
402                 cMac.update((byte)(textLength >> 24));
403                 cMac.update((byte)(textLength >> 16));
404                 cMac.update((byte)(textLength >> 8));
405                 cMac.update((byte)textLength);
406 
407                 extra = 6;
408             }
409 
410             if (initialAssociatedText != null)
411             {
412                 cMac.update(initialAssociatedText, 0, initialAssociatedText.length);
413             }
414             if (associatedText.size() > 0)
415             {
416                 cMac.update(associatedText.getBuffer(), 0, associatedText.size());
417             }
418 
419             extra = (extra + textLength) % 16;
420             if (extra != 0)
421             {
422                 for (int i = extra; i != 16; i++)
423                 {
424                     cMac.update((byte)0x00);
425                 }
426             }
427         }
428 
429         //
430         // add the text
431         //
432         cMac.update(data, dataOff, dataLen);
433 
434         return cMac.doFinal(macBlock, 0);
435     }
436 
getAssociatedTextLength()437     private int getAssociatedTextLength()
438     {
439         return associatedText.size() + ((initialAssociatedText == null) ? 0 : initialAssociatedText.length);
440     }
441 
hasAssociatedText()442     private boolean hasAssociatedText()
443     {
444         return getAssociatedTextLength() > 0;
445     }
446 
447     private class ExposedByteArrayOutputStream
448         extends ByteArrayOutputStream
449     {
ExposedByteArrayOutputStream()450         public ExposedByteArrayOutputStream()
451         {
452         }
453 
getBuffer()454         public byte[] getBuffer()
455         {
456             return this.buf;
457         }
458     }
459 }
460