// 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} * @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} * @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} data - The data to en/decrypt * @returns {Promise>} * @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); } }); }