Astroport.ONE/www/AESBox/aes-ctr.js

296 lines
13 KiB
JavaScript
Raw Normal View History

2023-03-09 19:42:41 +01:00
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* 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;