forked from STI/Astroport.ONE
296 lines
13 KiB
JavaScript
296 lines
13 KiB
JavaScript
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
||
/* 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<nBytes; i++) { // use 1st 16/24/32 chars of password for key
|
||
pwBytes[i] = i<password.length ? password.charCodeAt(i) : 0;
|
||
}
|
||
let key = Aes.cipher(pwBytes, Aes.keyExpansion(pwBytes)); // gives us 16-byte key
|
||
key = key.concat(key.slice(0, nBytes-16)); // expand key to 16/24/32 bytes long
|
||
|
||
// initialise 1st 8 bytes of counter block with nonce (NIST SP 800-38A §B.2): [0-1] = millisec,
|
||
// [2-3] = random, [4-7] = seconds, together giving full sub-millisec uniqueness up to Feb 2106
|
||
const timestamp = (new Date()).getTime(); // milliseconds since 1-Jan-1970
|
||
const nonceMs = timestamp%1000;
|
||
const nonceSec = Math.floor(timestamp/1000);
|
||
const nonceRnd = Math.floor(Math.random()*0xffff);
|
||
// for debugging: const [ nonceMs, nonceSec, nonceRnd ] = [ 0, 0, 0 ];
|
||
const counterBlock = [ // 16-byte array; blocksize is fixed at 16 for AES
|
||
nonceMs & 0xff, nonceMs >>>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<blockCount; b++) {
|
||
// ---- encrypt counter block; Oⱼ = CIPHₖ(Tⱼ) ----
|
||
const cipherCntr = Aes.cipher(counterBlock, keySchedule);
|
||
|
||
// block size is reduced on final block
|
||
const blockLength = b<blockCount-1 ? blockSize : (plaintext.length-1)%blockSize + 1;
|
||
|
||
// ---- xor plaintext with ciphered counter byte-by-byte; Cⱼ = Pⱼ ⊕ Oⱼ ----
|
||
for (let i=0; i<blockLength; i++) {
|
||
ciphertext[b*blockSize + i] = cipherCntr[i] ^ plaintext[b*blockSize + i];
|
||
}
|
||
|
||
// increment counter block (counter in 2nd 8 bytes of counter block, big-endian)
|
||
counterBlock[blockSize-1]++;
|
||
// and propagate carry digits
|
||
for (let i=blockSize-1; i>=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<nBytes; i++) { // use 1st nBytes chars of password for key
|
||
pwBytes[i] = i<password.length ? password.charCodeAt(i) : 0;
|
||
}
|
||
let key = Aes.cipher(pwBytes, Aes.keyExpansion(pwBytes));
|
||
key = key.concat(key.slice(0, nBytes-16)); // expand key to 16/24/32 bytes long
|
||
|
||
// recover nonce from 1st 8 bytes of ciphertext into 1st 8 bytes of counter block
|
||
const counterBlock = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
|
||
for (let i=0; i<8; i++) counterBlock[i] = ciphertext.charCodeAt(i);
|
||
|
||
// convert ciphertext to byte array (skipping past initial 8 bytes)
|
||
const ciphertextBytes = new Array(ciphertext.length-8);
|
||
for (let i=8; i<ciphertext.length; i++) ciphertextBytes[i-8] = ciphertext.charCodeAt(i);
|
||
|
||
// ------------ perform decryption ------------
|
||
const plaintextBytes = AesCtr.nistDecryption(ciphertextBytes, key, counterBlock);
|
||
|
||
// convert byte array to (utf-8) plaintext string
|
||
const plaintextUtf8 = plaintextBytes.map(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<blockCount; b++) {
|
||
// ---- decrypt counter block; Oⱼ = CIPHₖ(Tⱼ) ----
|
||
const cipherCntr = Aes.cipher(counterBlock, keySchedule);
|
||
|
||
// block size is reduced on final block
|
||
const blockLength = b<blockCount-1 ? blockSize : (ciphertext.length-1)%blockSize + 1;
|
||
|
||
// ---- xor ciphertext with ciphered counter byte-by-byte; Pⱼ = Cⱼ ⊕ Oⱼ ----
|
||
for (let i=0; i<blockLength; i++) {
|
||
plaintext[b*blockSize + i] = cipherCntr[i] ^ ciphertext[b*blockSize + i];
|
||
}
|
||
|
||
// increment counter block (counter in 2nd 8 bytes of counter block, big-endian)
|
||
counterBlock[blockSize-1]++;
|
||
// and propagate carry digits
|
||
for (let i=blockSize-1; i>=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;
|