Skip to content

Commit

Permalink
Use Web Crypto for encrypting and decrypting on web
Browse files Browse the repository at this point in the history
WIP

TODO test if this works

Resolves #1292.
  • Loading branch information
lawrence-forooghian committed May 24, 2023
1 parent e5921fa commit 90ea5e8
Showing 1 changed file with 53 additions and 115 deletions.
168 changes: 53 additions & 115 deletions src/platform/web/lib/util/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import WordArray from 'crypto-js/build/lib-typedarrays';
import CryptoJS from 'crypto-js/build';
import Logger from '../../../../common/lib/util/logger';
import ErrorInfo from 'common/lib/types/errorinfo';
import * as API from '../../../../../ably';
Expand All @@ -13,6 +11,7 @@ import { IPlatformConfig } from 'common/types/IPlatformConfig';
type MessagePackBinaryType = ArrayBuffer;

type IV = CryptoDataTypes.IV<BufferUtilsOutput>;
// TODO should Bufferlike be https://www.w3.org/TR/WebIDL-1/#common-BufferSource ?
type InputPlaintext = CryptoDataTypes.InputPlaintext<Bufferlike, BufferUtilsOutput>;
type OutputCiphertext = ArrayBuffer;
type InputCiphertext = CryptoDataTypes.InputCiphertext<MessagePackBinaryType, BufferUtilsOutput>;
Expand All @@ -25,28 +24,23 @@ var CryptoFactory = function (config: IPlatformConfig, bufferUtils: typeof Buffe
var DEFAULT_BLOCKLENGTH = 16; // bytes
var DEFAULT_BLOCKLENGTH_WORDS = 4; // 32-bit words
var UINT32_SUP = 0x100000000;
var INT32_SUP = 0x80000000;

/**
* Internal: generate an array of secure random words corresponding to the given length of bytes
* Internal: generate an array of secure random data corresponding to the given length of bytes
* @param bytes
* @param callback
*/
var generateRandom: (byteLength: number, callback: (error: Error | null, result: WordArray | null) => void) => void;
var generateRandom: (byteLength: number, callback: (error: Error | null, result: ArrayBuffer | null) => void) => void;
if (config.getRandomArrayBuffer) {
generateRandom = (byteLength, callback) => {
config.getRandomArrayBuffer!(byteLength, (error, result) => {
callback(error, result ? bufferUtils.toWordArray(result) : null);
});
};
generateRandom = config.getRandomArrayBuffer;
} else if (typeof Uint32Array !== 'undefined' && config.getRandomValues) {
var blockRandomArray = new Uint32Array(DEFAULT_BLOCKLENGTH_WORDS);
generateRandom = function (bytes, callback) {
var words = bytes / 4,
nativeArray = words == DEFAULT_BLOCKLENGTH_WORDS ? blockRandomArray : new Uint32Array(words);
config.getRandomValues!(nativeArray, function (err) {
if (typeof callback !== 'undefined') {
callback(err, bufferUtils.toWordArray(nativeArray));
callback(err, bufferUtils.toArrayBuffer(nativeArray));
}
});
};
Expand All @@ -58,29 +52,15 @@ var CryptoFactory = function (config: IPlatformConfig, bufferUtils: typeof Buffe
'Warning: the browser you are using does not support secure cryptographically secure randomness generation; falling back to insecure Math.random()'
);
var words = bytes / 4,
array = new Array(words);
array = new Uint32Array(words);
for (var i = 0; i < words; i++) {
/* cryptojs wordarrays use signed ints. When WordArray.create is fed a
* Uint32Array unsigned are converted to signed automatically, but when
* fed a normal array they aren't, so need to do so ourselves by
* subtracting INT32_SUP */
array[i] = Math.floor(Math.random() * UINT32_SUP) - INT32_SUP;
array[i] = Math.random() * UINT32_SUP;
}

callback(null, WordArray.create(array));
callback(null, bufferUtils.toArrayBuffer(array));
};
}

/**
* Internal: calculate the padded length of a given plaintext
* using PKCS5.
* @param plaintextLength
* @return
*/
function getPaddedLength(plaintextLength: number) {
return (plaintextLength + DEFAULT_BLOCKLENGTH) & -DEFAULT_BLOCKLENGTH;
}

/**
* Internal: checks that the cipherParams are a valid combination. Currently
* just checks that the calculated keyLength is a valid one for aes-cbc
Expand All @@ -103,29 +83,6 @@ var CryptoFactory = function (config: IPlatformConfig, bufferUtils: typeof Buffe
return string.replace('_', '/').replace('-', '+');
}

/**
* Internal: obtain the pkcs5 padding string for a given padded length;
*/
var pkcs5Padding = [
WordArray.create([0x10101010, 0x10101010, 0x10101010, 0x10101010], 16),
WordArray.create([0x01000000], 1),
WordArray.create([0x02020000], 2),
WordArray.create([0x03030300], 3),
WordArray.create([0x04040404], 4),
WordArray.create([0x05050505, 0x05000000], 5),
WordArray.create([0x06060606, 0x06060000], 6),
WordArray.create([0x07070707, 0x07070700], 7),
WordArray.create([0x08080808, 0x08080808], 8),
WordArray.create([0x09090909, 0x09090909, 0x09000000], 9),
WordArray.create([0x0a0a0a0a, 0x0a0a0a0a, 0x0a0a0000], 10),
WordArray.create([0x0b0b0b0b, 0x0b0b0b0b, 0x0b0b0b00], 11),
WordArray.create([0x0c0c0c0c, 0x0c0c0c0c, 0x0c0c0c0c], 12),
WordArray.create([0x0d0d0d0d, 0x0d0d0d0d, 0x0d0d0d0d, 0x0d000000], 13),
WordArray.create([0x0e0e0e0e, 0x0e0e0e0e, 0x0e0e0e0e, 0x0e0e0000], 14),
WordArray.create([0x0f0f0f0f, 0x0f0f0f0f, 0x0f0f0f0f, 0x0f0f0f0f], 15),
WordArray.create([0x10101010, 0x10101010, 0x10101010, 0x10101010], 16),
];

function isCipherParams(
params: API.Types.CipherParams | API.Types.CipherParamOptions
): params is API.Types.CipherParams {
Expand All @@ -148,7 +105,7 @@ var CryptoFactory = function (config: IPlatformConfig, bufferUtils: typeof Buffe
algorithm: string;
keyLength: number;
mode: string;
key: Bufferlike;
key: Bufferlike; // TODO change to ArrayBuffer

constructor(algorithm: string, keyLength: number, mode: string, key: Bufferlike) {
this.algorithm = algorithm;
Expand Down Expand Up @@ -184,7 +141,7 @@ var CryptoFactory = function (config: IPlatformConfig, bufferUtils: typeof Buffe
* in any not provided with default values, calculating a keyLength from
* the supplied key, and validating the result.
* @param params an object containing at a minimum a `key` key with value the
* key, as either a binary (ArrayBuffer, Array, WordArray) or a
* key, as either a binary (ArrayBuffer, Array) or a
* base64-encoded string. May optionally also contain: algorithm (defaults to
* AES), mode (defaults to 'cbc')
*/
Expand Down Expand Up @@ -221,7 +178,7 @@ var CryptoFactory = function (config: IPlatformConfig, bufferUtils: typeof Buffe

/**
* Generate a random encryption key from the supplied keylength (or the
* default keyLength if none supplied) as a CryptoJS WordArray
* default keyLength if none supplied) as an ArrayBuffer
* @param keyLength (optional) the required keyLength in bits
* @param callback (optional) (err, key)
*/
Expand Down Expand Up @@ -255,80 +212,62 @@ var CryptoFactory = function (config: IPlatformConfig, bufferUtils: typeof Buffe

class CBCCipher implements ICipher<InputPlaintext, OutputCiphertext, InputCiphertext, OutputPlaintext> {
algorithm: string;
cjsAlgorithm: 'AES';
key: WordArray;
iv: WordArray | null;
encryptCipher: ReturnType<typeof CryptoJS.algo.AES.createEncryptor> | null;
key: ArrayBuffer;
iv: ArrayBuffer | null;

constructor(params: CipherParams, iv: IV | null) {
this.algorithm = params.algorithm + '-' + String(params.keyLength) + '-' + params.mode;
const cjsAlgorithm = params.algorithm.toUpperCase().replace(/-\d+$/, '');
if (cjsAlgorithm != 'AES') {
throw new Error('AES is the only supported encryption algorithm');
}
this.cjsAlgorithm = cjsAlgorithm;
this.key = bufferUtils.isWordArray(params.key) ? params.key : bufferUtils.toWordArray(params.key);
this.iv = iv ? bufferUtils.toWordArray(iv).clone() : null;
this.encryptCipher = null;
this.algorithm = params.algorithm.toUpperCase() + '-' + '-' + params.mode;
this.key = bufferUtils.toArrayBuffer(params.key).slice(0) /* i.e. create a copy */;
this.iv = iv
? bufferUtils
.toArrayBuffer(iv)
.slice(0) /* i.e. create a copy - TODO here and in the above we might not need to copy */
: null;
}

encrypt(plaintext: InputPlaintext, callback: (error: Error | null, data: OutputCiphertext | null) => void) {
Logger.logAction(Logger.LOG_MICRO, 'CBCCipher.encrypt()', '');
const plaintextWordArray = bufferUtils.toWordArray(plaintext);
var plaintextLength = plaintextWordArray.sigBytes,
paddedLength = getPaddedLength(plaintextLength),
self = this;

var then = function () {
self.getIv(function (err, iv) {
if (err) {
callback(err, null);
return;
}
var cipherOut = self.encryptCipher!.process(
plaintextWordArray.concat(pkcs5Padding[paddedLength - plaintextLength])
);
var ciphertext = iv!.concat(cipherOut);
callback(null, bufferUtils.toArrayBuffer(ciphertext));
});
};

if (!this.encryptCipher) {
if (this.iv) {
this.encryptCipher = CryptoJS.algo[this.cjsAlgorithm].createEncryptor(this.key, { iv: this.iv });
then();
} else {
generateRandom(DEFAULT_BLOCKLENGTH, function (err, iv) {
if (err) {
callback(err, null);
return;
const encryptAsync = async () => {
// TODO can we make this API async?
const iv = await new Promise((resolve: (iv: IV) => void, reject: (error: Error) => void) => {
this.getIv((error, iv) => {
if (error) {
reject(error);
} else {
resolve(iv!);
}
self.encryptCipher = CryptoJS.algo[self.cjsAlgorithm].createEncryptor(self.key, { iv: iv! });
self.iv = iv;
then();
});
}
} else {
then();
}
});

const cryptoKey = await crypto.subtle.importKey('raw', this.key, this.algorithm, false, ['encrypt']);
return crypto.subtle.decrypt({ name: this.algorithm, iv }, cryptoKey, plaintext);
};

encryptAsync()
.then((ciphertext) => {
callback(null, ciphertext);
})
.catch((error) => {
callback(error, null);
});
}

// TODO handle the fact that in a non-secure context (e.g. test in console on http://permission.site), globalThis.crypto has no `subtle` property — see https://webidl.spec.whatwg.org/#SecureContext

// according to https://www.w3.org/TR/WebCryptoAPI/#subtlecrypto-interface we can pass data of type BufferSource, which is defined in https://www.w3.org/TR/WebIDL-1/#common-BufferSource ("used to represent objects that are either themselves an ArrayBuffer or which provide a view on to an ArrayBuffer")
async decrypt(ciphertext: InputCiphertext): Promise<OutputPlaintext> {
Logger.logAction(Logger.LOG_MICRO, 'CBCCipher.decrypt()', '');
const ciphertextWordArray = bufferUtils.toWordArray(ciphertext);
var ciphertextWords = ciphertextWordArray.words,
iv = WordArray.create(ciphertextWords.slice(0, DEFAULT_BLOCKLENGTH_WORDS)),
ciphertextBody = WordArray.create(ciphertextWords.slice(DEFAULT_BLOCKLENGTH_WORDS));

var decryptCipher = CryptoJS.algo[this.cjsAlgorithm].createDecryptor(this.key, { iv: iv });
var plaintext = decryptCipher.process(ciphertextBody);
var epilogue = decryptCipher.finalize();
decryptCipher.reset();
if (epilogue && epilogue.sigBytes) plaintext.concat(epilogue);
return bufferUtils.toArrayBuffer(plaintext);

const ciphertextArrayBuffer = bufferUtils.toArrayBuffer(ciphertext);
const iv = ciphertextArrayBuffer.slice(0, DEFAULT_BLOCKLENGTH);
const ciphertextBody = ciphertextArrayBuffer.slice(DEFAULT_BLOCKLENGTH);

const cryptoKey = await crypto.subtle.importKey('raw', this.key, this.algorithm, false, ['decrypt']);
return crypto.subtle.decrypt({ name: this.algorithm, iv }, cryptoKey, ciphertextBody);
}

getIv(callback: (error: Error | null, iv: WordArray | null) => void) {
getIv(callback: (error: Error | null, iv: ArrayBuffer | null) => void) {
if (this.iv) {
var iv = this.iv;
this.iv = null;
Expand All @@ -339,13 +278,12 @@ var CryptoFactory = function (config: IPlatformConfig, bufferUtils: typeof Buffe
/* Since the iv for a new block is the ciphertext of the last, this
* sets a new iv (= aes(randomBlock XOR lastCipherText)) as well as
* returning it */
var self = this;
generateRandom(DEFAULT_BLOCKLENGTH, function (err, randomBlock) {
if (err) {
callback(err, null);
return;
}
callback(null, self.encryptCipher!.process(randomBlock!));
callback(null, bufferUtils.toArrayBuffer(randomBlock!));
});
}
}
Expand Down

0 comments on commit 90ea5e8

Please sign in to comment.