mirror of
https://github.com/openpgpjs/openpgpjs.git
synced 2025-06-24 07:02:30 +00:00
Use WebCrypto for streamed CFB encryption; for CFB/GCM/EAX, fallback to asmcrypto only if key size is not supported
CFB decryption is too slow using WebCrypto (CBC mode), since every block needs to be decrypted separately
This commit is contained in:
parent
21a6d83ec8
commit
2377b2958d
@ -72,13 +72,6 @@ export default async function CMAC(key) {
|
||||
}
|
||||
|
||||
async function CBC(key) {
|
||||
if (util.getWebCrypto() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support
|
||||
key = await webCrypto.importKey('raw', key, { name: 'AES-CBC', length: key.length * 8 }, false, ['encrypt']);
|
||||
return async function(pt) {
|
||||
const ct = await webCrypto.encrypt({ name: 'AES-CBC', iv: zeroBlock, length: blockLength * 8 }, key, pt);
|
||||
return new Uint8Array(ct).subarray(0, ct.byteLength - blockLength);
|
||||
};
|
||||
}
|
||||
if (util.getNodeCrypto()) { // Node crypto library
|
||||
return async function(pt) {
|
||||
const en = new nodeCrypto.createCipheriv('aes-' + (key.length * 8) + '-cbc', key, zeroBlock);
|
||||
@ -86,6 +79,24 @@ async function CBC(key) {
|
||||
return new Uint8Array(ct);
|
||||
};
|
||||
}
|
||||
|
||||
if (util.getWebCrypto()) {
|
||||
try {
|
||||
key = await webCrypto.importKey('raw', key, { name: 'AES-CBC', length: key.length * 8 }, false, ['encrypt']);
|
||||
return async function(pt) {
|
||||
const ct = await webCrypto.encrypt({ name: 'AES-CBC', iv: zeroBlock, length: blockLength * 8 }, key, pt);
|
||||
return new Uint8Array(ct).subarray(0, ct.byteLength - blockLength);
|
||||
};
|
||||
} catch (err) {
|
||||
// no 192 bit support in Chromium, which throws `OperationError`, see: https://www.chromium.org/blink/webcrypto#TOC-AES-support
|
||||
if (err.name !== 'NotSupportedError' &&
|
||||
!(key.length === 24 && err.name === 'OperationError')) {
|
||||
throw err;
|
||||
}
|
||||
util.printDebugError('Browser did not support operation: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// asm.js fallback
|
||||
return async function(pt) {
|
||||
return AES_CBC.encrypt(pt, key, false, zeroBlock);
|
||||
|
@ -96,7 +96,7 @@ export async function encrypt(algo, key, plaintext, iv, config) {
|
||||
*/
|
||||
export async function decrypt(algo, key, ciphertext, iv) {
|
||||
const algoName = enums.read(enums.symmetric, algo);
|
||||
if (util.getNodeCrypto() && nodeAlgos[algoName]) { // Node crypto library.
|
||||
if (nodeCrypto && nodeAlgos[algoName]) { // Node crypto library.
|
||||
return nodeDecrypt(algo, key, ciphertext, iv);
|
||||
}
|
||||
if (util.isAES(algo)) {
|
||||
@ -129,18 +129,122 @@ export async function decrypt(algo, key, ciphertext, iv) {
|
||||
return stream.transform(ciphertext, process, process);
|
||||
}
|
||||
|
||||
function aesEncrypt(algo, key, pt, iv, config) {
|
||||
if (
|
||||
util.getWebCrypto() &&
|
||||
key.length !== 24 && // Chrome doesn't support 192 bit keys, see https://www.chromium.org/blink/webcrypto#TOC-AES-support
|
||||
!util.isStream(pt) &&
|
||||
pt.length >= 3000 * config.minBytesForWebCrypto // Default to a 3MB minimum. Chrome is pretty slow for small messages, see: https://bugs.chromium.org/p/chromium/issues/detail?id=701188#c2
|
||||
) { // Web Crypto
|
||||
return webEncrypt(algo, key, pt, iv);
|
||||
class WebCryptoEncryptor {
|
||||
constructor(algo, key, iv) {
|
||||
const { blockSize } = getCipher(algo);
|
||||
this.key = key;
|
||||
this.prevBlock = iv;
|
||||
this.nextBlock = new Uint8Array(blockSize);
|
||||
this.i = 0; // pointer inside next block
|
||||
this.blockSize = blockSize;
|
||||
this.zeroBlock = new Uint8Array(this.blockSize);
|
||||
}
|
||||
// asm.js fallback
|
||||
|
||||
static async isSupported(algo) {
|
||||
const { keySize } = getCipher(algo);
|
||||
return webCrypto.importKey('raw', new Uint8Array(keySize), 'aes-cbc', false, ['encrypt'])
|
||||
.then(() => true, () => false);
|
||||
}
|
||||
|
||||
async _runCBC(plaintext, nonZeroIV) {
|
||||
const mode = 'AES-CBC';
|
||||
this.keyRef = this.keyRef || await webCrypto.importKey('raw', this.key, mode, false, ['encrypt']);
|
||||
const ciphertext = await webCrypto.encrypt(
|
||||
{ name: mode, iv: nonZeroIV || this.zeroBlock },
|
||||
this.keyRef,
|
||||
plaintext
|
||||
);
|
||||
return new Uint8Array(ciphertext).subarray(0, plaintext.length);
|
||||
}
|
||||
|
||||
async encryptChunk(value) {
|
||||
const missing = this.nextBlock.length - this.i;
|
||||
const added = value.subarray(0, missing);
|
||||
this.nextBlock.set(added, this.i);
|
||||
if ((this.i + value.length) >= (2 * this.blockSize)) {
|
||||
const leftover = (value.length - missing) % this.blockSize;
|
||||
const plaintext = util.concatUint8Array([
|
||||
this.nextBlock,
|
||||
value.subarray(missing, value.length - leftover)
|
||||
]);
|
||||
const toEncrypt = util.concatUint8Array([
|
||||
this.prevBlock,
|
||||
plaintext.subarray(0, plaintext.length - this.blockSize) // stop one block "early", since we only need to xor the plaintext and pass it over as prevBlock
|
||||
]);
|
||||
|
||||
const encryptedBlocks = await this._runCBC(toEncrypt);
|
||||
xorMut(encryptedBlocks, plaintext);
|
||||
this.prevBlock = encryptedBlocks.subarray(-this.blockSize).slice();
|
||||
|
||||
// take care of leftover data
|
||||
if (leftover > 0) this.nextBlock.set(value.subarray(-leftover).slice());
|
||||
this.i = leftover;
|
||||
|
||||
return encryptedBlocks;
|
||||
}
|
||||
|
||||
this.i += added.length;
|
||||
let encryptedBlock = new Uint8Array();
|
||||
if (this.i === this.nextBlock.length) { // block ready to be encrypted
|
||||
const curBlock = this.nextBlock;
|
||||
encryptedBlock = await this._runCBC(this.prevBlock);
|
||||
xorMut(encryptedBlock, curBlock);
|
||||
this.prevBlock = encryptedBlock.slice();
|
||||
this.i = 0;
|
||||
|
||||
const remaining = value.subarray(added.length);
|
||||
this.nextBlock.set(remaining, this.i);
|
||||
this.i += remaining.length;
|
||||
}
|
||||
|
||||
return encryptedBlock;
|
||||
}
|
||||
|
||||
async finish() {
|
||||
let result;
|
||||
if (this.i === 0) { // nothing more to encrypt
|
||||
result = new Uint8Array();
|
||||
} else {
|
||||
this.nextBlock = this.nextBlock.subarray(0, this.i);
|
||||
const curBlock = this.nextBlock;
|
||||
const encryptedBlock = await this._runCBC(this.prevBlock);
|
||||
xorMut(encryptedBlock, curBlock);
|
||||
result = encryptedBlock.subarray(0, curBlock.length);
|
||||
}
|
||||
|
||||
this.clearSensitiveData();
|
||||
return result;
|
||||
}
|
||||
|
||||
clearSensitiveData() {
|
||||
this.nextBlock.fill(0);
|
||||
this.prevBlock.fill(0);
|
||||
this.keyRef = null;
|
||||
this.key = null;
|
||||
}
|
||||
|
||||
async encrypt(plaintext) {
|
||||
// plaintext is internally padded to block length before encryption
|
||||
const encryptedWithPadding = await this._runCBC(
|
||||
util.concatUint8Array([new Uint8Array(this.blockSize), plaintext]),
|
||||
this.iv
|
||||
);
|
||||
// drop encrypted padding
|
||||
const ct = encryptedWithPadding.subarray(0, plaintext.length);
|
||||
xorMut(ct, plaintext);
|
||||
this.clearSensitiveData();
|
||||
return ct;
|
||||
}
|
||||
}
|
||||
|
||||
async function aesEncrypt(algo, key, pt, iv) {
|
||||
if (webCrypto && await WebCryptoEncryptor.isSupported(algo)) { // Chromium does not implement AES with 192-bit keys
|
||||
const cfb = new WebCryptoEncryptor(algo, key, iv);
|
||||
return util.isStream(pt) ? stream.transform(pt, value => cfb.encryptChunk(value), () => cfb.finish()) : cfb.encrypt(pt);
|
||||
} else {
|
||||
const cfb = new AES_CFB(key, iv);
|
||||
return stream.transform(pt, value => cfb.aes.AES_Encrypt_process(value), () => cfb.aes.AES_Encrypt_finish());
|
||||
}
|
||||
}
|
||||
|
||||
function aesDecrypt(algo, key, ct, iv) {
|
||||
@ -152,21 +256,12 @@ function aesDecrypt(algo, key, ct, iv) {
|
||||
}
|
||||
|
||||
function xorMut(a, b) {
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
const aLength = Math.min(a.length, b.length);
|
||||
for (let i = 0; i < aLength; i++) {
|
||||
a[i] = a[i] ^ b[i];
|
||||
}
|
||||
}
|
||||
|
||||
async function webEncrypt(algo, key, pt, iv) {
|
||||
const ALGO = 'AES-CBC';
|
||||
const _key = await webCrypto.importKey('raw', key, { name: ALGO }, false, ['encrypt']);
|
||||
const { blockSize } = getCipher(algo);
|
||||
const cbc_pt = util.concatUint8Array([new Uint8Array(blockSize), pt]);
|
||||
const ct = new Uint8Array(await webCrypto.encrypt({ name: ALGO, iv }, _key, cbc_pt)).subarray(0, pt.length);
|
||||
xorMut(ct, pt);
|
||||
return ct;
|
||||
}
|
||||
|
||||
function nodeEncrypt(algo, key, pt, iv) {
|
||||
const algoName = enums.read(enums.symmetric, algo);
|
||||
const cipherObj = new nodeCrypto.createCipheriv(nodeAlgos[algoName], key, iv);
|
||||
|
@ -47,16 +47,6 @@ async function OMAC(key) {
|
||||
}
|
||||
|
||||
async function CTR(key) {
|
||||
if (
|
||||
util.getWebCrypto() &&
|
||||
key.length !== 24 // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support
|
||||
) {
|
||||
key = await webCrypto.importKey('raw', key, { name: 'AES-CTR', length: key.length * 8 }, false, ['encrypt']);
|
||||
return async function(pt, iv) {
|
||||
const ct = await webCrypto.encrypt({ name: 'AES-CTR', counter: iv, length: blockLength * 8 }, key, pt);
|
||||
return new Uint8Array(ct);
|
||||
};
|
||||
}
|
||||
if (util.getNodeCrypto()) { // Node crypto library
|
||||
return async function(pt, iv) {
|
||||
const en = new nodeCrypto.createCipheriv('aes-' + (key.length * 8) + '-ctr', key, iv);
|
||||
@ -64,6 +54,24 @@ async function CTR(key) {
|
||||
return new Uint8Array(ct);
|
||||
};
|
||||
}
|
||||
|
||||
if (util.getWebCrypto()) {
|
||||
try {
|
||||
const keyRef = await webCrypto.importKey('raw', key, { name: 'AES-CTR', length: key.length * 8 }, false, ['encrypt']);
|
||||
return async function(pt, iv) {
|
||||
const ct = await webCrypto.encrypt({ name: 'AES-CTR', counter: iv, length: blockLength * 8 }, keyRef, pt);
|
||||
return new Uint8Array(ct);
|
||||
};
|
||||
} catch (err) {
|
||||
// no 192 bit support in Chromium, which throws `OperationError`, see: https://www.chromium.org/blink/webcrypto#TOC-AES-support
|
||||
if (err.name !== 'NotSupportedError' &&
|
||||
!(key.length === 24 && err.name === 'OperationError')) {
|
||||
throw err;
|
||||
}
|
||||
util.printDebugError('Browser did not support operation: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// asm.js fallback
|
||||
return async function(pt, iv) {
|
||||
return AES_CTR.encrypt(pt, key, iv);
|
||||
|
@ -65,12 +65,15 @@ async function GCM(cipher, key) {
|
||||
};
|
||||
}
|
||||
|
||||
if (util.getWebCrypto() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support
|
||||
if (util.getWebCrypto()) {
|
||||
try {
|
||||
const _key = await webCrypto.importKey('raw', key, { name: ALGO }, false, ['encrypt', 'decrypt']);
|
||||
|
||||
// Safari 13 and Safari iOS 14 does not support GCM-en/decrypting empty messages
|
||||
const webcryptoEmptyMessagesUnsupported = navigator.userAgent.match(/Version\/13\.\d(\.\d)* Safari/) ||
|
||||
navigator.userAgent.match(/Version\/(13|14)\.\d(\.\d)* Mobile\/\S* Safari/);
|
||||
return {
|
||||
encrypt: async function(pt, iv, adata = new Uint8Array()) {
|
||||
if (!pt.length) { // iOS does not support GCM-en/decrypting empty messages
|
||||
if (webcryptoEmptyMessagesUnsupported && !pt.length) {
|
||||
return AES_GCM.encrypt(pt, key, iv, adata);
|
||||
}
|
||||
const ct = await webCrypto.encrypt({ name: ALGO, iv, additionalData: adata, tagLength: tagLength * 8 }, _key, pt);
|
||||
@ -78,13 +81,21 @@ async function GCM(cipher, key) {
|
||||
},
|
||||
|
||||
decrypt: async function(ct, iv, adata = new Uint8Array()) {
|
||||
if (ct.length === tagLength) { // iOS does not support GCM-en/decrypting empty messages
|
||||
if (webcryptoEmptyMessagesUnsupported && ct.length === tagLength) {
|
||||
return AES_GCM.decrypt(ct, key, iv, adata);
|
||||
}
|
||||
const pt = await webCrypto.decrypt({ name: ALGO, iv, additionalData: adata, tagLength: tagLength * 8 }, _key, ct);
|
||||
return new Uint8Array(pt);
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
// no 192 bit support in Chromium, which throws `OperationError`, see: https://www.chromium.org/blink/webcrypto#TOC-AES-support
|
||||
if (err.name !== 'NotSupportedError' &&
|
||||
!(key.length === 24 && err.name === 'OperationError')) {
|
||||
throw err;
|
||||
}
|
||||
util.printDebugError('Browser did not support operation: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
Loading…
x
Reference in New Issue
Block a user