mirror of
https://github.com/openpgpjs/openpgpjs.git
synced 2025-03-30 15:08:32 +00:00

The module was barely used, and its presence confusing, since WebCrypto or asmcrypto are often directly used and usable instead. Also, use AES_CBC instead of AES_ECB for single-block encryption, so that we can drop support for the latter in the asmcrypto lib.
276 lines
9.2 KiB
JavaScript
276 lines
9.2 KiB
JavaScript
// Modified by ProtonTech AG
|
|
|
|
// Modified by Recurity Labs GmbH
|
|
|
|
// modified version of https://www.hanewin.net/encrypt/PGdecode.js:
|
|
|
|
/* OpenPGP encryption using RSA/AES
|
|
* Copyright 2005-2006 Herbert Hanewinkel, www.haneWIN.de
|
|
* version 2.0, check www.haneWIN.de for the latest version
|
|
|
|
* This software is provided as-is, without express or implied warranty.
|
|
* Permission to use, copy, modify, distribute or sell this software, with or
|
|
* without fee, for any purpose and by any individual or organization, is hereby
|
|
* granted, provided that the above copyright notice and this paragraph appear
|
|
* in all copies. Distribution as a part of an application or binary must
|
|
* include the above copyright notice in the documentation and/or other
|
|
* materials provided with the application or distribution.
|
|
*/
|
|
|
|
/**
|
|
* @module crypto/mode/cfb
|
|
*/
|
|
|
|
import { AES_CFB } from '@openpgp/asmcrypto.js/aes/cfb.js';
|
|
import * as stream from '@openpgp/web-stream-tools';
|
|
import util from '../../util';
|
|
import enums from '../../enums';
|
|
import { getLegacyCipher, getCipherParams } from '../cipher';
|
|
|
|
const webCrypto = util.getWebCrypto();
|
|
const nodeCrypto = util.getNodeCrypto();
|
|
|
|
const knownAlgos = nodeCrypto ? nodeCrypto.getCiphers() : [];
|
|
const nodeAlgos = {
|
|
idea: knownAlgos.includes('idea-cfb') ? 'idea-cfb' : undefined, /* Unused, not implemented */
|
|
tripledes: knownAlgos.includes('des-ede3-cfb') ? 'des-ede3-cfb' : undefined,
|
|
cast5: knownAlgos.includes('cast5-cfb') ? 'cast5-cfb' : undefined,
|
|
blowfish: knownAlgos.includes('bf-cfb') ? 'bf-cfb' : undefined,
|
|
aes128: knownAlgos.includes('aes-128-cfb') ? 'aes-128-cfb' : undefined,
|
|
aes192: knownAlgos.includes('aes-192-cfb') ? 'aes-192-cfb' : undefined,
|
|
aes256: knownAlgos.includes('aes-256-cfb') ? 'aes-256-cfb' : undefined
|
|
/* twofish is not implemented in OpenSSL */
|
|
};
|
|
|
|
/**
|
|
* CFB encryption
|
|
* @param {enums.symmetric} algo - block cipher algorithm
|
|
* @param {Uint8Array} key
|
|
* @param {MaybeStream<Uint8Array>} plaintext
|
|
* @param {Uint8Array} iv
|
|
* @param {Object} config - full configuration, defaults to openpgp.config
|
|
* @returns MaybeStream<Uint8Array>
|
|
*/
|
|
export async function encrypt(algo, key, plaintext, iv, config) {
|
|
const algoName = enums.read(enums.symmetric, algo);
|
|
if (util.getNodeCrypto() && nodeAlgos[algoName]) { // Node crypto library.
|
|
return nodeEncrypt(algo, key, plaintext, iv);
|
|
}
|
|
if (util.isAES(algo)) {
|
|
return aesEncrypt(algo, key, plaintext, iv, config);
|
|
}
|
|
|
|
const LegacyCipher = await getLegacyCipher(algo);
|
|
const cipherfn = new LegacyCipher(key);
|
|
const block_size = cipherfn.blockSize;
|
|
|
|
const blockc = iv.slice();
|
|
let pt = new Uint8Array();
|
|
const process = chunk => {
|
|
if (chunk) {
|
|
pt = util.concatUint8Array([pt, chunk]);
|
|
}
|
|
const ciphertext = new Uint8Array(pt.length);
|
|
let i;
|
|
let j = 0;
|
|
while (chunk ? pt.length >= block_size : pt.length) {
|
|
const encblock = cipherfn.encrypt(blockc);
|
|
for (i = 0; i < block_size; i++) {
|
|
blockc[i] = pt[i] ^ encblock[i];
|
|
ciphertext[j++] = blockc[i];
|
|
}
|
|
pt = pt.subarray(block_size);
|
|
}
|
|
return ciphertext.subarray(0, j);
|
|
};
|
|
return stream.transform(plaintext, process, process);
|
|
}
|
|
|
|
/**
|
|
* CFB decryption
|
|
* @param {enums.symmetric} algo - block cipher algorithm
|
|
* @param {Uint8Array} key
|
|
* @param {MaybeStream<Uint8Array>} ciphertext
|
|
* @param {Uint8Array} iv
|
|
* @returns MaybeStream<Uint8Array>
|
|
*/
|
|
export async function decrypt(algo, key, ciphertext, iv) {
|
|
const algoName = enums.read(enums.symmetric, algo);
|
|
if (nodeCrypto && nodeAlgos[algoName]) { // Node crypto library.
|
|
return nodeDecrypt(algo, key, ciphertext, iv);
|
|
}
|
|
if (util.isAES(algo)) {
|
|
return aesDecrypt(algo, key, ciphertext, iv);
|
|
}
|
|
|
|
const LegacyCipher = await getLegacyCipher(algo);
|
|
const cipherfn = new LegacyCipher(key);
|
|
const block_size = cipherfn.blockSize;
|
|
|
|
let blockp = iv;
|
|
let ct = new Uint8Array();
|
|
const process = chunk => {
|
|
if (chunk) {
|
|
ct = util.concatUint8Array([ct, chunk]);
|
|
}
|
|
const plaintext = new Uint8Array(ct.length);
|
|
let i;
|
|
let j = 0;
|
|
while (chunk ? ct.length >= block_size : ct.length) {
|
|
const decblock = cipherfn.encrypt(blockp);
|
|
blockp = ct.subarray(0, block_size);
|
|
for (i = 0; i < block_size; i++) {
|
|
plaintext[j++] = blockp[i] ^ decblock[i];
|
|
}
|
|
ct = ct.subarray(block_size);
|
|
}
|
|
return plaintext.subarray(0, j);
|
|
};
|
|
return stream.transform(ciphertext, process, process);
|
|
}
|
|
|
|
class WebCryptoEncryptor {
|
|
constructor(algo, key, iv) {
|
|
const { blockSize } = getCipherParams(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);
|
|
}
|
|
|
|
static async isSupported(algo) {
|
|
const { keySize } = getCipherParams(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) {
|
|
if (util.isStream(ct)) {
|
|
const cfb = new AES_CFB(key, iv);
|
|
return stream.transform(ct, value => cfb.aes.AES_Decrypt_process(value), () => cfb.aes.AES_Decrypt_finish());
|
|
}
|
|
return AES_CFB.decrypt(ct, key, iv);
|
|
}
|
|
|
|
function xorMut(a, b) {
|
|
const aLength = Math.min(a.length, b.length);
|
|
for (let i = 0; i < aLength; i++) {
|
|
a[i] = a[i] ^ b[i];
|
|
}
|
|
}
|
|
|
|
function nodeEncrypt(algo, key, pt, iv) {
|
|
const algoName = enums.read(enums.symmetric, algo);
|
|
const cipherObj = new nodeCrypto.createCipheriv(nodeAlgos[algoName], key, iv);
|
|
return stream.transform(pt, value => new Uint8Array(cipherObj.update(value)));
|
|
}
|
|
|
|
function nodeDecrypt(algo, key, ct, iv) {
|
|
const algoName = enums.read(enums.symmetric, algo);
|
|
const decipherObj = new nodeCrypto.createDecipheriv(nodeAlgos[algoName], key, iv);
|
|
return stream.transform(ct, value => new Uint8Array(decipherObj.update(value)));
|
|
}
|