openpgpjs/src/packet/sym_encrypted_integrity_protected_data.js
larabr db15f6d6a1
Import legacy ciphers (CAST5, TwoFish, BlowFish, DES) only on demand (#1723)
This primarily affects the lightweight build, which will not include these
(fairly large) modules in the main bundle file.
2024-02-26 15:37:50 +01:00

316 lines
13 KiB
JavaScript

// GPG4Browsers - An OpenPGP implementation in javascript
// Copyright (C) 2011 Recurity Labs GmbH
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 3.0 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import * as stream from '@openpgp/web-stream-tools';
import crypto from '../crypto';
import computeHKDF from '../crypto/hkdf';
import enums from '../enums';
import util from '../util';
import defaultConfig from '../config';
import LiteralDataPacket from './literal_data';
import CompressedDataPacket from './compressed_data';
import OnePassSignaturePacket from './one_pass_signature';
import SignaturePacket from './signature';
import PacketList from './packetlist';
import { UnsupportedError } from './packet';
// A SEIP packet can contain the following packet types
const allowedPackets = /*#__PURE__*/ util.constructAllowedPackets([
LiteralDataPacket,
CompressedDataPacket,
OnePassSignaturePacket,
SignaturePacket
]);
/**
* Implementation of the Sym. Encrypted Integrity Protected Data Packet (Tag 18)
*
* {@link https://tools.ietf.org/html/rfc4880#section-5.13|RFC4880 5.13}:
* The Symmetrically Encrypted Integrity Protected Data packet is
* a variant of the Symmetrically Encrypted Data packet. It is a new feature
* created for OpenPGP that addresses the problem of detecting a modification to
* encrypted data. It is used in combination with a Modification Detection Code
* packet.
*/
class SymEncryptedIntegrityProtectedDataPacket {
static get tag() {
return enums.packet.symEncryptedIntegrityProtectedData;
}
static fromObject({ version, aeadAlgorithm }) {
if (version !== 1 && version !== 2) {
throw new Error('Unsupported SEIPD version');
}
const seip = new SymEncryptedIntegrityProtectedDataPacket();
seip.version = version;
if (version === 2) {
seip.aeadAlgorithm = aeadAlgorithm;
}
return seip;
}
constructor() {
this.version = null;
// The following 4 fields are for V2 only.
/** @type {enums.symmetric} */
this.cipherAlgorithm = null;
/** @type {enums.aead} */
this.aeadAlgorithm = null;
this.chunkSizeByte = null;
this.salt = null;
this.encrypted = null;
this.packets = null;
}
async read(bytes) {
await stream.parse(bytes, async reader => {
this.version = await reader.readByte();
// - A one-octet version number with value 1 or 2.
if (this.version !== 1 && this.version !== 2) {
throw new UnsupportedError(`Version ${this.version} of the SEIP packet is unsupported.`);
}
if (this.version === 2) {
// - A one-octet cipher algorithm.
this.cipherAlgorithm = await reader.readByte();
// - A one-octet AEAD algorithm.
this.aeadAlgorithm = await reader.readByte();
// - A one-octet chunk size.
this.chunkSizeByte = await reader.readByte();
// - Thirty-two octets of salt. The salt is used to derive the message key and must be unique.
this.salt = await reader.readBytes(32);
}
// For V1:
// - Encrypted data, the output of the selected symmetric-key cipher
// operating in Cipher Feedback mode with shift amount equal to the
// block size of the cipher (CFB-n where n is the block size).
// For V2:
// - Encrypted data, the output of the selected symmetric-key cipher operating in the given AEAD mode.
// - A final, summary authentication tag for the AEAD mode.
this.encrypted = reader.remainder();
});
}
write() {
if (this.version === 2) {
return util.concat([new Uint8Array([this.version, this.cipherAlgorithm, this.aeadAlgorithm, this.chunkSizeByte]), this.salt, this.encrypted]);
}
return util.concat([new Uint8Array([this.version]), this.encrypted]);
}
/**
* Encrypt the payload in the packet.
* @param {enums.symmetric} sessionKeyAlgorithm - The symmetric encryption algorithm to use
* @param {Uint8Array} key - The key of cipher blocksize length to be used
* @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {Promise<Boolean>}
* @throws {Error} on encryption failure
* @async
*/
async encrypt(sessionKeyAlgorithm, key, config = defaultConfig) {
let bytes = this.packets.write();
if (stream.isArrayStream(bytes)) bytes = await stream.readToEnd(bytes);
if (this.version === 2) {
this.cipherAlgorithm = sessionKeyAlgorithm;
this.salt = crypto.random.getRandomBytes(32);
this.chunkSizeByte = config.aeadChunkSizeByte;
this.encrypted = await runAEAD(this, 'encrypt', key, bytes);
} else {
const { blockSize } = crypto.getCipherParams(sessionKeyAlgorithm);
const prefix = await crypto.getPrefixRandom(sessionKeyAlgorithm);
const mdc = new Uint8Array([0xD3, 0x14]); // modification detection code packet
const tohash = util.concat([prefix, bytes, mdc]);
const hash = await crypto.hash.sha1(stream.passiveClone(tohash));
const plaintext = util.concat([tohash, hash]);
this.encrypted = await crypto.mode.cfb.encrypt(sessionKeyAlgorithm, key, plaintext, new Uint8Array(blockSize), config);
}
return true;
}
/**
* Decrypts the encrypted data contained in the packet.
* @param {enums.symmetric} sessionKeyAlgorithm - The selected symmetric encryption algorithm to be used
* @param {Uint8Array} key - The key of cipher blocksize length to be used
* @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {Promise<Boolean>}
* @throws {Error} on decryption failure
* @async
*/
async decrypt(sessionKeyAlgorithm, key, config = defaultConfig) {
let encrypted = stream.clone(this.encrypted);
if (stream.isArrayStream(encrypted)) encrypted = await stream.readToEnd(encrypted);
let packetbytes;
if (this.version === 2) {
packetbytes = await runAEAD(this, 'decrypt', key, encrypted);
} else {
const { blockSize } = crypto.getCipherParams(sessionKeyAlgorithm);
const decrypted = await crypto.mode.cfb.decrypt(sessionKeyAlgorithm, key, encrypted, new Uint8Array(blockSize));
// there must be a modification detection code packet as the
// last packet and everything gets hashed except the hash itself
const realHash = stream.slice(stream.passiveClone(decrypted), -20);
const tohash = stream.slice(decrypted, 0, -20);
const verifyHash = Promise.all([
stream.readToEnd(await crypto.hash.sha1(stream.passiveClone(tohash))),
stream.readToEnd(realHash)
]).then(([hash, mdc]) => {
if (!util.equalsUint8Array(hash, mdc)) {
throw new Error('Modification detected.');
}
return new Uint8Array();
});
const bytes = stream.slice(tohash, blockSize + 2); // Remove random prefix
packetbytes = stream.slice(bytes, 0, -2); // Remove MDC packet
packetbytes = stream.concat([packetbytes, stream.fromAsync(() => verifyHash)]);
if (!util.isStream(encrypted) || !config.allowUnauthenticatedStream) {
packetbytes = await stream.readToEnd(packetbytes);
}
}
this.packets = await PacketList.fromBinary(packetbytes, allowedPackets, config);
return true;
}
}
export default SymEncryptedIntegrityProtectedDataPacket;
/**
* En/decrypt the payload.
* @param {encrypt|decrypt} fn - Whether to encrypt or decrypt
* @param {Uint8Array} key - The session key used to en/decrypt the payload
* @param {Uint8Array | ReadableStream<Uint8Array>} data - The data to en/decrypt
* @returns {Promise<Uint8Array | ReadableStream<Uint8Array>>}
* @async
*/
export async function runAEAD(packet, fn, key, data) {
const isSEIPDv2 = packet instanceof SymEncryptedIntegrityProtectedDataPacket && packet.version === 2;
const isAEADP = !isSEIPDv2 && packet.constructor.tag === enums.packet.aeadEncryptedData; // no `instanceof` to avoid importing the corresponding class (circular import)
if (!isSEIPDv2 && !isAEADP) throw new Error('Unexpected packet type');
const mode = crypto.getAEADMode(packet.aeadAlgorithm);
const tagLengthIfDecrypting = fn === 'decrypt' ? mode.tagLength : 0;
const tagLengthIfEncrypting = fn === 'encrypt' ? mode.tagLength : 0;
const chunkSize = 2 ** (packet.chunkSizeByte + 6) + tagLengthIfDecrypting; // ((uint64_t)1 << (c + 6))
const chunkIndexSizeIfAEADEP = isAEADP ? 8 : 0;
const adataBuffer = new ArrayBuffer(13 + chunkIndexSizeIfAEADEP);
const adataArray = new Uint8Array(adataBuffer, 0, 5 + chunkIndexSizeIfAEADEP);
const adataTagArray = new Uint8Array(adataBuffer);
const adataView = new DataView(adataBuffer);
const chunkIndexArray = new Uint8Array(adataBuffer, 5, 8);
adataArray.set([0xC0 | packet.constructor.tag, packet.version, packet.cipherAlgorithm, packet.aeadAlgorithm, packet.chunkSizeByte], 0);
let chunkIndex = 0;
let latestPromise = Promise.resolve();
let cryptedBytes = 0;
let queuedBytes = 0;
let iv;
let ivView;
if (isSEIPDv2) {
const { keySize } = crypto.getCipherParams(packet.cipherAlgorithm);
const { ivLength } = mode;
const info = new Uint8Array(adataBuffer, 0, 5);
const derived = await computeHKDF(enums.hash.sha256, key, packet.salt, info, keySize + ivLength);
key = derived.subarray(0, keySize);
iv = derived.subarray(keySize); // The last 8 bytes of HKDF output are unneeded, but this avoids one copy.
iv.fill(0, iv.length - 8);
ivView = new DataView(iv.buffer, iv.byteOffset, iv.byteLength);
} else { // AEADEncryptedDataPacket
iv = packet.iv;
// ivView is unused in this case
}
const modeInstance = await mode(packet.cipherAlgorithm, key);
return stream.transformPair(data, async (readable, writable) => {
if (util.isStream(readable) !== 'array') {
const buffer = new TransformStream({}, {
highWaterMark: util.getHardwareConcurrency() * 2 ** (packet.chunkSizeByte + 6),
size: array => array.length
});
stream.pipe(buffer.readable, writable);
writable = buffer.writable;
}
const reader = stream.getReader(readable);
const writer = stream.getWriter(writable);
try {
while (true) {
let chunk = await reader.readBytes(chunkSize + tagLengthIfDecrypting) || new Uint8Array();
const finalChunk = chunk.subarray(chunk.length - tagLengthIfDecrypting);
chunk = chunk.subarray(0, chunk.length - tagLengthIfDecrypting);
let cryptedPromise;
let done;
let nonce;
if (isSEIPDv2) { // SEIPD V2
nonce = iv;
} else { // AEADEncryptedDataPacket
nonce = iv.slice();
for (let i = 0; i < 8; i++) {
nonce[iv.length - 8 + i] ^= chunkIndexArray[i];
}
}
if (!chunkIndex || chunk.length) {
reader.unshift(finalChunk);
cryptedPromise = modeInstance[fn](chunk, nonce, adataArray);
cryptedPromise.catch(() => {});
queuedBytes += chunk.length - tagLengthIfDecrypting + tagLengthIfEncrypting;
} else {
// After the last chunk, we either encrypt a final, empty
// data chunk to get the final authentication tag or
// validate that final authentication tag.
adataView.setInt32(5 + chunkIndexSizeIfAEADEP + 4, cryptedBytes); // Should be setInt64(5 + chunkIndexSizeIfAEADEP, ...)
cryptedPromise = modeInstance[fn](finalChunk, nonce, adataTagArray);
cryptedPromise.catch(() => {});
queuedBytes += tagLengthIfEncrypting;
done = true;
}
cryptedBytes += chunk.length - tagLengthIfDecrypting;
// eslint-disable-next-line no-loop-func
latestPromise = latestPromise.then(() => cryptedPromise).then(async crypted => {
await writer.ready;
await writer.write(crypted);
queuedBytes -= crypted.length;
}).catch(err => writer.abort(err));
if (done || queuedBytes > writer.desiredSize) {
await latestPromise; // Respect backpressure
}
if (!done) {
if (isSEIPDv2) { // SEIPD V2
ivView.setInt32(iv.length - 4, ++chunkIndex); // Should be setInt64(iv.length - 8, ...)
} else { // AEADEncryptedDataPacket
adataView.setInt32(5 + 4, ++chunkIndex); // Should be setInt64(5, ...)
}
} else {
await writer.close();
break;
}
}
} catch (e) {
await writer.ready.catch(() => {});
await writer.abort(e);
}
});
}