/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* AES counter-mode (CTR) implementation in JavaScript (c) Chris Veness 2005-2019 */ /* MIT Licence */ /* www.movable-type.co.uk/scripts/aes.html */ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* global WorkerGlobalScope */ import Aes from './aes.js'; /** * AesCtr: Counter-mode (CTR) wrapper for AES. * * This encrypts a Unicode string to produces a base64 ciphertext using 128/192/256-bit AES, * and the converse to decrypt an encrypted ciphertext. * * See csrc.nist.gov/publications/detail/sp/800-38a/final */ class AesCtr extends Aes { /** * Encrypt a text using AES encryption in Counter mode of operation. * * Unicode multi-byte character safe. * * @param {string} plaintext - Source text to be encrypted. * @param {string} password - The password to use to generate a key for encryption. * @param {number} nBits - Number of bits to be used in the key; 128 / 192 / 256. * @returns {string} Encrypted text, base-64 encoded. * * @example * const encr = AesCtr.encrypt('big secret', 'pāşšŵōřđ', 256); // 'lwGl66VVwVObKIr6of8HVqJr' */ static encrypt(plaintext, password, nBits) { if (![ 128, 192, 256 ].includes(nBits)) throw new Error('Key size is not 128 / 192 / 256'); plaintext = AesCtr.utf8Encode(String(plaintext)); password = AesCtr.utf8Encode(String(password)); // use AES itself to encrypt password to get cipher key (using plain password as source for key // expansion) to give us well encrypted key (in real use hashed password could be used for key) const nBytes = nBits/8; // no bytes in key (16/24/32) const pwBytes = new Array(nBytes); for (let i=0; i>>8 & 0xff, nonceRnd & 0xff, nonceRnd>>>8 & 0xff, nonceSec & 0xff, nonceSec>>>8 & 0xff, nonceSec>>>16 & 0xff, nonceSec>>>24 & 0xff, 0, 0, 0, 0, 0, 0, 0, 0, ]; // and convert nonce to a string to go on the front of the ciphertext const nonceStr = counterBlock.slice(0, 8).map(i => String.fromCharCode(i)).join(''); // convert (utf-8) plaintext to byte array const plaintextBytes = plaintext.split('').map(ch => ch.charCodeAt(0)); // ------------ perform encryption ------------ const ciphertextBytes = AesCtr.nistEncryption(plaintextBytes, key, counterBlock); // convert byte array to (utf-8) ciphertext string const ciphertextUtf8 = ciphertextBytes.map(i => String.fromCharCode(i)).join(''); // base-64 encode ciphertext const ciphertextB64 = AesCtr.base64Encode(nonceStr+ciphertextUtf8); return ciphertextB64; } /** * NIST SP 800-38A sets out recommendations for block cipher modes of operation in terms of byte * operations. This implements the §6.5 Counter Mode (CTR). * * Oⱼ = CIPHₖ(Tⱼ) for j = 1, 2 … n * Cⱼ = Pⱼ ⊕ Oⱼ for j = 1, 2 … n-1 * C*ₙ = P* ⊕ MSBᵤ(Oₙ) final (partial?) block * where CIPHₖ is the forward cipher function, O output blocks, P plaintext blocks, C * ciphertext blocks * * @param {number[]} plaintext - Plaintext to be encrypted, as byte array. * @param {number[]} key - Key to be used to encrypt plaintext. * @param {number[]} counterBlock - Initial 16-byte CTR counter block (with nonce & 0 counter). * @returns {number[]} Ciphertext as byte array. * * @private */ static nistEncryption(plaintext, key, counterBlock) { const blockSize = 16; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES // generate key schedule - an expansion of the key into distinct Key Rounds for each round const keySchedule = Aes.keyExpansion(key); const blockCount = Math.ceil(plaintext.length/blockSize); const ciphertext = new Array(plaintext.length); for (let b=0; b=8; i--) { counterBlock[i-1] += counterBlock[i] >> 8; counterBlock[i] &= 0xff; } // if within web worker, announce progress every 1000 blocks (roughly every 50ms) if (typeof WorkerGlobalScope != 'undefined' && self instanceof WorkerGlobalScope) { if (b%1000 == 0) self.postMessage({ progress: b/blockCount }); } } return ciphertext; } /** * Decrypt a text encrypted by AES in counter mode of operation. * * @param {string} ciphertext - Cipher text to be decrypted. * @param {string} password - Password to use to generate a key for decryption. * @param {number} nBits - Number of bits to be used in the key; 128 / 192 / 256. * @returns {string} Decrypted text * * @example * const decr = AesCtr.decrypt('lwGl66VVwVObKIr6of8HVqJr', 'pāşšŵōřđ', 256); // 'big secret' */ static decrypt(ciphertext, password, nBits) { if (![ 128, 192, 256 ].includes(nBits)) throw new Error('Key size is not 128 / 192 / 256'); ciphertext = AesCtr.base64Decode(String(ciphertext)); password = AesCtr.utf8Encode(String(password)); // use AES to encrypt password (mirroring encrypt routine) const nBytes = nBits/8; // no bytes in key const pwBytes = new Array(nBytes); for (let i=0; i String.fromCharCode(i)).join(''); // decode from UTF8 back to Unicode multi-byte chars const plaintext = AesCtr.utf8Decode(plaintextUtf8); return plaintext; } /** * NIST SP 800-38A sets out recommendations for block cipher modes of operation in terms of byte * operations. This implements the §6.5 Counter Mode (CTR). * * Oⱼ = CIPHₖ(Tⱼ) for j = 1, 2 … n * Pⱼ = Cⱼ ⊕ Oⱼ for j = 1, 2 … n-1 * P*ₙ = C* ⊕ MSBᵤ(Oₙ) final (partial?) block * where CIPHₖ is the forward cipher function, O output blocks, C ciphertext blocks, P * plaintext blocks * * @param {number[]} ciphertext - Ciphertext to be decrypted, as byte array. * @param {number[]} key - Key to be used to decrypt ciphertext. * @param {number[]} counterBlock - Initial 16-byte CTR counter block (with nonce & 0 counter). * @returns {number[]} Plaintext as byte array. * * @private */ static nistDecryption(ciphertext, key, counterBlock) { const blockSize = 16; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES // generate key schedule - an expansion of the key into distinct Key Rounds for each round const keySchedule = Aes.keyExpansion(key); const blockCount = Math.ceil(ciphertext.length/blockSize); const plaintext = new Array(ciphertext.length); for (let b=0; b=8; i--) { counterBlock[i-1] += counterBlock[i] >> 8; counterBlock[i] &= 0xff; } // if within web worker, announce progress every 1000 blocks (roughly every 50ms) if (typeof WorkerGlobalScope != 'undefined' && self instanceof WorkerGlobalScope) { if (b%1000 == 0) self.postMessage({ progress: b/blockCount }); } } return plaintext; } /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /** * Encodes multi-byte string to utf8. * * Note utf8Encode is an identity function with 7-bit ascii strings, but not with 8-bit strings; * utf8Encode('x') = 'x', but utf8Encode('ça') = 'ça', and utf8Encode('ça') = 'ça'. */ static utf8Encode(str) { try { return new TextEncoder().encode(str, 'utf-8').reduce((prev, curr) => prev + String.fromCharCode(curr), ''); } catch (e) { // no TextEncoder available? return unescape(encodeURIComponent(str)); // monsur.hossa.in/2012/07/20/utf-8-in-javascript.html } } /** * Decodes utf8 string to multi-byte. */ static utf8Decode(str) { try { return new TextEncoder().decode(str, 'utf-8').reduce((prev, curr) => prev + String.fromCharCode(curr), ''); } catch (e) { // no TextEncoder available? return decodeURIComponent(escape(str)); // monsur.hossa.in/2012/07/20/utf-8-in-javascript.html } } /* * Encodes string as base-64. * * - developer.mozilla.org/en-US/docs/Web/API/window.btoa, nodejs.org/api/buffer.html * - note: btoa & Buffer/binary work on single-byte Unicode (C0/C1), so ok for utf8 strings, not for general Unicode... * - note: if btoa()/atob() are not available (eg IE9-), try github.com/davidchambers/Base64.js */ static base64Encode(str) { if (typeof btoa != 'undefined') return btoa(str); // browser if (typeof Buffer != 'undefined') return new Buffer(str, 'binary').toString('base64'); // Node.js throw new Error('No Base64 Encode'); } /* * Decodes base-64 encoded string. */ static base64Decode(str) { if (typeof atob != 'undefined') return atob(str); // browser if (typeof Buffer != 'undefined') return new Buffer(str, 'base64').toString('binary'); // Node.js throw new Error('No Base64 Decode'); } } /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ export default AesCtr;