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) {
|
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
|
if (util.getNodeCrypto()) { // Node crypto library
|
||||||
return async function(pt) {
|
return async function(pt) {
|
||||||
const en = new nodeCrypto.createCipheriv('aes-' + (key.length * 8) + '-cbc', key, zeroBlock);
|
const en = new nodeCrypto.createCipheriv('aes-' + (key.length * 8) + '-cbc', key, zeroBlock);
|
||||||
@ -86,6 +79,24 @@ async function CBC(key) {
|
|||||||
return new Uint8Array(ct);
|
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
|
// asm.js fallback
|
||||||
return async function(pt) {
|
return async function(pt) {
|
||||||
return AES_CBC.encrypt(pt, key, false, zeroBlock);
|
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) {
|
export async function decrypt(algo, key, ciphertext, iv) {
|
||||||
const algoName = enums.read(enums.symmetric, algo);
|
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);
|
return nodeDecrypt(algo, key, ciphertext, iv);
|
||||||
}
|
}
|
||||||
if (util.isAES(algo)) {
|
if (util.isAES(algo)) {
|
||||||
@ -129,19 +129,123 @@ export async function decrypt(algo, key, ciphertext, iv) {
|
|||||||
return stream.transform(ciphertext, process, process);
|
return stream.transform(ciphertext, process, process);
|
||||||
}
|
}
|
||||||
|
|
||||||
function aesEncrypt(algo, key, pt, iv, config) {
|
class WebCryptoEncryptor {
|
||||||
if (
|
constructor(algo, key, iv) {
|
||||||
util.getWebCrypto() &&
|
const { blockSize } = getCipher(algo);
|
||||||
key.length !== 24 && // Chrome doesn't support 192 bit keys, see https://www.chromium.org/blink/webcrypto#TOC-AES-support
|
this.key = key;
|
||||||
!util.isStream(pt) &&
|
this.prevBlock = iv;
|
||||||
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
|
this.nextBlock = new Uint8Array(blockSize);
|
||||||
) { // Web Crypto
|
this.i = 0; // pointer inside next block
|
||||||
return webEncrypt(algo, key, pt, iv);
|
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);
|
const cfb = new AES_CFB(key, iv);
|
||||||
return stream.transform(pt, value => cfb.aes.AES_Encrypt_process(value), () => cfb.aes.AES_Encrypt_finish());
|
return stream.transform(pt, value => cfb.aes.AES_Encrypt_process(value), () => cfb.aes.AES_Encrypt_finish());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function aesDecrypt(algo, key, ct, iv) {
|
function aesDecrypt(algo, key, ct, iv) {
|
||||||
if (util.isStream(ct)) {
|
if (util.isStream(ct)) {
|
||||||
@ -152,21 +256,12 @@ function aesDecrypt(algo, key, ct, iv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function xorMut(a, b) {
|
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];
|
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) {
|
function nodeEncrypt(algo, key, pt, iv) {
|
||||||
const algoName = enums.read(enums.symmetric, algo);
|
const algoName = enums.read(enums.symmetric, algo);
|
||||||
const cipherObj = new nodeCrypto.createCipheriv(nodeAlgos[algoName], key, iv);
|
const cipherObj = new nodeCrypto.createCipheriv(nodeAlgos[algoName], key, iv);
|
||||||
|
@ -47,16 +47,6 @@ async function OMAC(key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function CTR(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
|
if (util.getNodeCrypto()) { // Node crypto library
|
||||||
return async function(pt, iv) {
|
return async function(pt, iv) {
|
||||||
const en = new nodeCrypto.createCipheriv('aes-' + (key.length * 8) + '-ctr', key, 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);
|
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
|
// asm.js fallback
|
||||||
return async function(pt, iv) {
|
return async function(pt, iv) {
|
||||||
return AES_CTR.encrypt(pt, key, 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']);
|
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 {
|
return {
|
||||||
encrypt: async function(pt, iv, adata = new Uint8Array()) {
|
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);
|
return AES_GCM.encrypt(pt, key, iv, adata);
|
||||||
}
|
}
|
||||||
const ct = await webCrypto.encrypt({ name: ALGO, iv, additionalData: adata, tagLength: tagLength * 8 }, _key, pt);
|
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()) {
|
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);
|
return AES_GCM.decrypt(ct, key, iv, adata);
|
||||||
}
|
}
|
||||||
const pt = await webCrypto.decrypt({ name: ALGO, iv, additionalData: adata, tagLength: tagLength * 8 }, _key, ct);
|
const pt = await webCrypto.decrypt({ name: ALGO, iv, additionalData: adata, tagLength: tagLength * 8 }, _key, ct);
|
||||||
return new Uint8Array(pt);
|
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 {
|
return {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user