/** * Counter with CBC-MAC (CCM) * * Due to JS limitations (52 bits of Number precision) maximum encrypted message length * is limited to ~4 PiB ( 2^52 - 16 ) per `nonce`-`key` pair. * That also limits `lengthSize` parameter maximum value to 7 (not 8 as described in RFC3610). * * Additional authenticated data `adata` maximum length is chosen to be no more than 65279 bytes ( 2^16 - 2^8 ), * which is considered enough for the most of use-cases. * * And one more important thing: in case of progressive ciphering of a data stream (in other * words when data can't be held in-memory at a whole and are ciphered chunk-by-chunk) * you have to know the `dataLength` in advance and pass that value to the cipher options. */ import { AES_asm } from './aes.asm'; import { AES } from './aes'; import { _heap_write } from '../other/utils'; import { IllegalArgumentError, IllegalStateError, SecurityError } from '../other/errors'; const _AES_CCM_adata_maxLength = 65279; // 2^16 - 2^8 const _AES_CCM_data_maxLength = 4503599627370480; // 2^52 - 2^4 export class AES_CCM extends AES { constructor(key, nonce, adata, tagSize = 16, dataLength) { super(key, undefined, undefined, 'CCM'); this.counter = 1; this.dataLength = -1; // Tag size if (tagSize < 4 || tagSize > 16 || tagSize & 1) throw new IllegalArgumentError('illegal tagSize value'); this.tagSize = tagSize; // Nonce this.nonce = nonce; if (nonce.length < 8 || nonce.length > 13) throw new IllegalArgumentError('illegal nonce length'); this.lengthSize = 15 - nonce.length; nonce = new Uint8Array(nonce.length + 1); nonce[0] = this.lengthSize - 1; nonce.set(this.nonce, 1); if (dataLength < 0 || dataLength > _AES_CCM_data_maxLength || dataLength > Math.pow(2, 8 * this.lengthSize) - 16) throw new IllegalArgumentError('illegal dataLength value'); if (adata !== undefined) { if (adata.length > _AES_CCM_adata_maxLength) throw new IllegalArgumentError('illegal adata length'); this.adata = adata.length ? adata : undefined; } this.dataLength = dataLength; this.counter = 1; this.AES_CCM_calculate_iv(); this.AES_CTR_set_options(nonce, this.counter, 8 * this.lengthSize); } static encrypt(clear, key, nonce, adata, tagsize = 16) { return new AES_CCM(key, nonce, adata, tagsize, clear.length).encrypt(clear); } static decrypt(cipher, key, nonce, adata, tagsize = 16) { return new AES_CCM(key, nonce, adata, tagsize, cipher.length - tagsize).decrypt(cipher); } encrypt(data) { this.dataLength = data.length || 0; const result1 = this.AES_CCM_Encrypt_process(data); const result2 = this.AES_CCM_Encrypt_finish(); const result = new Uint8Array(result1.length + result2.length); if (result1.length) result.set(result1); if (result2.length) result.set(result2, result1.length); return result; } decrypt(data) { this.dataLength = data.length || 0; const result1 = this.AES_CCM_Decrypt_process(data); const result2 = this.AES_CCM_Decrypt_finish(); const result = new Uint8Array(result1.length + result2.length); if (result1.length) result.set(result1); if (result2.length) result.set(result2, result1.length); return result; } AES_CCM_calculate_iv() { const nonce = this.nonce; const adata = this.adata; const tagSize = this.tagSize; const lengthSize = this.lengthSize; const dataLength = this.dataLength; const data = new Uint8Array(16 + (adata ? 2 + adata.length : 0)); // B0: flags(adata?, M', L'), nonce, len(data) data[0] = (adata ? 64 : 0) | ((tagSize - 2) << 2) | (lengthSize - 1); data.set(nonce, 1); if (lengthSize > 6) data[9] = ((dataLength / 0x100000000) >>> 16) & 15; if (lengthSize > 5) data[10] = ((dataLength / 0x100000000) >>> 8) & 255; if (lengthSize > 4) data[11] = (dataLength / 0x100000000) & 255; if (lengthSize > 3) data[12] = dataLength >>> 24; if (lengthSize > 2) data[13] = (dataLength >>> 16) & 255; data[14] = (dataLength >>> 8) & 255; data[15] = dataLength & 255; // B*: len(adata), adata if (adata) { data[16] = (adata.length >>> 8) & 255; data[17] = adata.length & 255; data.set(adata, 18); } this._cbc_mac_process(data); this.asm.get_state(AES_asm.HEAP_DATA); const iv = new Uint8Array(this.heap.subarray(0, 16)); const ivview = new DataView(iv.buffer, iv.byteOffset, iv.byteLength); this.asm.set_iv(ivview.getUint32(0), ivview.getUint32(4), ivview.getUint32(8), ivview.getUint32(12)); } _cbc_mac_process(data) { const heap = this.heap; const asm = this.asm; let dpos = 0; let dlen = data.length || 0; let wlen = 0; while (dlen > 0) { wlen = _heap_write(heap, 0, data, dpos, dlen); while (wlen & 15) heap[wlen++] = 0; dpos += wlen; dlen -= wlen; asm.mac(AES_asm.MAC.CBC, AES_asm.HEAP_DATA, wlen); } } AES_CCM_Encrypt_process(data) { const asm = this.asm; const heap = this.heap; let dpos = 0; let dlen = data.length || 0; let counter = this.counter; let pos = this.pos; let len = this.len; const rlen = (len + dlen) & -16; let rpos = 0; let wlen = 0; if (((counter - 1) << 4) + len + dlen > _AES_CCM_data_maxLength) // ??? should check against lengthSize throw new RangeError('counter overflow'); const result = new Uint8Array(rlen); while (dlen > 0) { wlen = _heap_write(heap, pos + len, data, dpos, dlen); len += wlen; dpos += wlen; dlen -= wlen; wlen = asm.mac(AES_asm.MAC.CBC, AES_asm.HEAP_DATA + pos, len); wlen = asm.cipher(AES_asm.ENC.CTR, AES_asm.HEAP_DATA + pos, wlen); if (wlen) result.set(heap.subarray(pos, pos + wlen), rpos); counter += wlen >>> 4; rpos += wlen; if (wlen < len) { pos += wlen; len -= wlen; } else { pos = 0; len = 0; } } this.counter = counter; this.pos = pos; this.len = len; return result; } AES_CCM_Encrypt_finish() { const asm = this.asm; const heap = this.heap; const tagSize = this.tagSize; const pos = this.pos; const len = this.len; const result = new Uint8Array(len + tagSize); let i = len; for (; i & 15; i++) heap[pos + i] = 0; asm.mac(AES_asm.MAC.CBC, AES_asm.HEAP_DATA + pos, i); asm.cipher(AES_asm.ENC.CTR, AES_asm.HEAP_DATA + pos, i); if (len) result.set(heap.subarray(pos, pos + len)); asm.set_counter(0, 0, 0, 0); asm.get_iv(AES_asm.HEAP_DATA); asm.cipher(AES_asm.ENC.CTR, AES_asm.HEAP_DATA, 16); result.set(heap.subarray(0, tagSize), len); this.counter = 1; this.pos = 0; this.len = 0; return result; } AES_CCM_Decrypt_process(data) { let dpos = 0; let dlen = data.length || 0; const asm = this.asm; const heap = this.heap; let counter = this.counter; const tagSize = this.tagSize; let pos = this.pos; let len = this.len; let rpos = 0; const rlen = len + dlen > tagSize ? (len + dlen - tagSize) & -16 : 0; const tlen = len + dlen - rlen; let wlen = 0; if (((counter - 1) << 4) + len + dlen > _AES_CCM_data_maxLength) throw new RangeError('counter overflow'); const result = new Uint8Array(rlen); while (dlen > tlen) { wlen = _heap_write(heap, pos + len, data, dpos, dlen - tlen); len += wlen; dpos += wlen; dlen -= wlen; wlen = asm.cipher(AES_asm.DEC.CTR, AES_asm.HEAP_DATA + pos, wlen); wlen = asm.mac(AES_asm.MAC.CBC, AES_asm.HEAP_DATA + pos, wlen); if (wlen) result.set(heap.subarray(pos, pos + wlen), rpos); counter += wlen >>> 4; rpos += wlen; pos = 0; len = 0; } if (dlen > 0) { len += _heap_write(heap, 0, data, dpos, dlen); } this.counter = counter; this.pos = pos; this.len = len; return result; } AES_CCM_Decrypt_finish() { const asm = this.asm; const heap = this.heap; const tagSize = this.tagSize; const pos = this.pos; const len = this.len; const rlen = len - tagSize; if (len < tagSize) throw new IllegalStateError('authentication tag not found'); const result = new Uint8Array(rlen); const atag = new Uint8Array(heap.subarray(pos + rlen, pos + len)); asm.cipher(AES_asm.DEC.CTR, AES_asm.HEAP_DATA + pos, (rlen + 15) & -16); result.set(heap.subarray(pos, pos + rlen)); let i = rlen; for (; i & 15; i++) heap[pos + i] = 0; asm.mac(AES_asm.MAC.CBC, AES_asm.HEAP_DATA + pos, i); asm.set_counter(0, 0, 0, 0); asm.get_iv(AES_asm.HEAP_DATA); asm.cipher(AES_asm.ENC.CTR, AES_asm.HEAP_DATA, 16); let acheck = 0; for (let j = 0; j < tagSize; ++j) acheck |= atag[j] ^ heap[j]; if (acheck) throw new SecurityError('data integrity check failed'); this.counter = 1; this.pos = 0; this.len = 0; return result; } AES_CTR_set_options(nonce, counter, size) { if (size < 8 || size > 48) throw new IllegalArgumentError('illegal counter size'); const mask = Math.pow(2, size) - 1; this.asm.set_mask(0, 0, (mask / 0x100000000) | 0, mask | 0); const len = nonce.length; if (!len || len > 16) throw new IllegalArgumentError('illegal nonce size'); this.nonce = nonce; const view = new DataView(new ArrayBuffer(16)); new Uint8Array(view.buffer).set(nonce); this.asm.set_nonce(view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)); if (counter < 0 || counter >= Math.pow(2, size)) throw new IllegalArgumentError('illegal counter value'); this.counter = counter; this.asm.set_counter(0, 0, (counter / 0x100000000) | 0, counter | 0); } }