import * as stream from '@openpgp/web-stream-tools';import { armor, unarmor } from './encoding/armor';import { Argon2OutOfMemoryError } from './type/s2k';import defaultConfig from './config';import crypto from './crypto';import enums from './enums';import util from './util';import { Signature } from './signature';import { getPreferredCipherSuite, createSignaturePacket } from './key';import { PacketList, LiteralDataPacket, CompressedDataPacket, AEADEncryptedDataPacket, SymEncryptedIntegrityProtectedDataPacket, SymmetricallyEncryptedDataPacket, PublicKeyEncryptedSessionKeyPacket, SymEncryptedSessionKeyPacket, OnePassSignaturePacket, SignaturePacket} from './packet';
const allowedMessagePackets = util.constructAllowedPackets([ LiteralDataPacket, CompressedDataPacket, AEADEncryptedDataPacket, SymEncryptedIntegrityProtectedDataPacket, SymmetricallyEncryptedDataPacket, PublicKeyEncryptedSessionKeyPacket, SymEncryptedSessionKeyPacket, OnePassSignaturePacket, SignaturePacket]);const allowedSymSessionKeyPackets = util.constructAllowedPackets([SymEncryptedSessionKeyPacket]);const allowedDetachedSignaturePackets = util.constructAllowedPackets([SignaturePacket]);
export class Message { constructor(packetlist) { this.packets = packetlist || new PacketList(); }
getEncryptionKeyIDs() { const keyIDs = []; const pkESKeyPacketlist = this.packets.filterByTag(enums.packet.publicKeyEncryptedSessionKey); pkESKeyPacketlist.forEach(function(packet) { keyIDs.push(packet.publicKeyID); }); return keyIDs; }
getSigningKeyIDs() { const msg = this.unwrapCompressed(); const onePassSigList = msg.packets.filterByTag(enums.packet.onePassSignature); if (onePassSigList.length > 0) { return onePassSigList.map(packet => packet.issuerKeyID); } const signatureList = msg.packets.filterByTag(enums.packet.signature); return signatureList.map(packet => packet.issuerKeyID); }
async decrypt(decryptionKeys, passwords, sessionKeys, date = new Date(), config = defaultConfig) { const symEncryptedPacketlist = this.packets.filterByTag( enums.packet.symmetricallyEncryptedData, enums.packet.symEncryptedIntegrityProtectedData, enums.packet.aeadEncryptedData );
if (symEncryptedPacketlist.length === 0) { throw new Error('No encrypted data found'); }
const symEncryptedPacket = symEncryptedPacketlist[0]; const expectedSymmetricAlgorithm = symEncryptedPacket.cipherAlgorithm;
const sessionKeyObjects = sessionKeys || await this.decryptSessionKeys(decryptionKeys, passwords, expectedSymmetricAlgorithm, date, config);
let exception = null; const decryptedPromise = Promise.all(sessionKeyObjects.map(async ({ algorithm: algorithmName, data }) => { if (!util.isUint8Array(data) || (!symEncryptedPacket.cipherAlgorithm && !util.isString(algorithmName))) { throw new Error('Invalid session key for decryption.'); }
try { const algo = symEncryptedPacket.cipherAlgorithm || enums.write(enums.symmetric, algorithmName); await symEncryptedPacket.decrypt(algo, data, config); } catch (e) { util.printDebugError(e); exception = e; } })); stream.cancel(symEncryptedPacket.encrypted); symEncryptedPacket.encrypted = null; await decryptedPromise;
if (!symEncryptedPacket.packets || !symEncryptedPacket.packets.length) { throw exception || new Error('Decryption failed.'); }
const resultMsg = new Message(symEncryptedPacket.packets); symEncryptedPacket.packets = new PacketList();
return resultMsg; }
async decryptSessionKeys(decryptionKeys, passwords, expectedSymmetricAlgorithm, date = new Date(), config = defaultConfig) { let decryptedSessionKeyPackets = [];
let exception; if (passwords) { const skeskPackets = this.packets.filterByTag(enums.packet.symEncryptedSessionKey); if (skeskPackets.length === 0) { throw new Error('No symmetrically encrypted session key packet found.'); } await Promise.all(passwords.map(async function(password, i) { let packets; if (i) { packets = await PacketList.fromBinary(skeskPackets.write(), allowedSymSessionKeyPackets, config); } else { packets = skeskPackets; } await Promise.all(packets.map(async function(skeskPacket) { try { await skeskPacket.decrypt(password); decryptedSessionKeyPackets.push(skeskPacket); } catch (err) { util.printDebugError(err); if (err instanceof Argon2OutOfMemoryError) { exception = err; } } })); })); } else if (decryptionKeys) { const pkeskPackets = this.packets.filterByTag(enums.packet.publicKeyEncryptedSessionKey); if (pkeskPackets.length === 0) { throw new Error('No public key encrypted session key packet found.'); } await Promise.all(pkeskPackets.map(async function(pkeskPacket) { await Promise.all(decryptionKeys.map(async function(decryptionKey) { let algos = [ enums.symmetric.aes256, enums.symmetric.aes128, enums.symmetric.tripledes, enums.symmetric.cast5 ]; try { const selfCertification = await decryptionKey.getPrimarySelfSignature(date, undefined, config); if (selfCertification.preferredSymmetricAlgorithms) { algos = algos.concat(selfCertification.preferredSymmetricAlgorithms); } } catch (e) {}
const decryptionKeyPackets = (await decryptionKey.getDecryptionKeys(pkeskPacket.publicKeyID, null, undefined, config)).map(key => key.keyPacket); await Promise.all(decryptionKeyPackets.map(async function(decryptionKeyPacket) { if (!decryptionKeyPacket || decryptionKeyPacket.isDummy()) { return; } if (!decryptionKeyPacket.isDecrypted()) { throw new Error('Decryption key is not decrypted.'); }
const doConstantTimeDecryption = config.constantTimePKCS1Decryption && ( pkeskPacket.publicKeyAlgorithm === enums.publicKey.rsaEncrypt || pkeskPacket.publicKeyAlgorithm === enums.publicKey.rsaEncryptSign || pkeskPacket.publicKeyAlgorithm === enums.publicKey.rsaSign || pkeskPacket.publicKeyAlgorithm === enums.publicKey.elgamal );
if (doConstantTimeDecryption) {
const serialisedPKESK = pkeskPacket.write(); await Promise.all(( expectedSymmetricAlgorithm ? [expectedSymmetricAlgorithm] : Array.from(config.constantTimePKCS1DecryptionSupportedSymmetricAlgorithms) ).map(async sessionKeyAlgorithm => { const pkeskPacketCopy = new PublicKeyEncryptedSessionKeyPacket(); pkeskPacketCopy.read(serialisedPKESK); const randomSessionKey = { sessionKeyAlgorithm, sessionKey: crypto.generateSessionKey(sessionKeyAlgorithm) }; try { await pkeskPacketCopy.decrypt(decryptionKeyPacket, randomSessionKey); decryptedSessionKeyPackets.push(pkeskPacketCopy); } catch (err) { util.printDebugError(err); exception = err; } }));
} else { try { await pkeskPacket.decrypt(decryptionKeyPacket); const symmetricAlgorithm = expectedSymmetricAlgorithm || pkeskPacket.sessionKeyAlgorithm; if (symmetricAlgorithm && !algos.includes(enums.write(enums.symmetric, symmetricAlgorithm))) { throw new Error('A non-preferred symmetric algorithm was used.'); } decryptedSessionKeyPackets.push(pkeskPacket); } catch (err) { util.printDebugError(err); exception = err; } } })); })); stream.cancel(pkeskPacket.encrypted); pkeskPacket.encrypted = null; })); } else { throw new Error('No key or password specified.'); }
if (decryptedSessionKeyPackets.length > 0) { if (decryptedSessionKeyPackets.length > 1) { const seen = new Set(); decryptedSessionKeyPackets = decryptedSessionKeyPackets.filter(item => { const k = item.sessionKeyAlgorithm + util.uint8ArrayToString(item.sessionKey); if (seen.has(k)) { return false; } seen.add(k); return true; }); }
return decryptedSessionKeyPackets.map(packet => ({ data: packet.sessionKey, algorithm: packet.sessionKeyAlgorithm && enums.read(enums.symmetric, packet.sessionKeyAlgorithm) })); } throw exception || new Error('Session key decryption failed.'); }
getLiteralData() { const msg = this.unwrapCompressed(); const literal = msg.packets.findPacket(enums.packet.literalData); return (literal && literal.getBytes()) || null; }
getFilename() { const msg = this.unwrapCompressed(); const literal = msg.packets.findPacket(enums.packet.literalData); return (literal && literal.getFilename()) || null; }
getText() { const msg = this.unwrapCompressed(); const literal = msg.packets.findPacket(enums.packet.literalData); if (literal) { return literal.getText(); } return null; }
static async generateSessionKey(encryptionKeys = [], date = new Date(), userIDs = [], config = defaultConfig) { const { symmetricAlgo, aeadAlgo } = await getPreferredCipherSuite(encryptionKeys, date, userIDs, config); const symmetricAlgoName = enums.read(enums.symmetric, symmetricAlgo); const aeadAlgoName = aeadAlgo ? enums.read(enums.aead, aeadAlgo) : undefined;
await Promise.all(encryptionKeys.map(key => key.getEncryptionKey() .catch(() => null) .then(maybeKey => { if (maybeKey && (maybeKey.keyPacket.algorithm === enums.publicKey.x25519 || maybeKey.keyPacket.algorithm === enums.publicKey.x448) && !aeadAlgoName && !util.isAES(symmetricAlgo)) { throw new Error('Could not generate a session key compatible with the given `encryptionKeys`: X22519 and X448 keys can only be used to encrypt AES session keys; change `config.preferredSymmetricAlgorithm` accordingly.'); } }) ));
const sessionKeyData = crypto.generateSessionKey(symmetricAlgo); return { data: sessionKeyData, algorithm: symmetricAlgoName, aeadAlgorithm: aeadAlgoName }; }
async encrypt(encryptionKeys, passwords, sessionKey, wildcard = false, encryptionKeyIDs = [], date = new Date(), userIDs = [], config = defaultConfig) { if (sessionKey) { if (!util.isUint8Array(sessionKey.data) || !util.isString(sessionKey.algorithm)) { throw new Error('Invalid session key for encryption.'); } } else if (encryptionKeys && encryptionKeys.length) { sessionKey = await Message.generateSessionKey(encryptionKeys, date, userIDs, config); } else if (passwords && passwords.length) { sessionKey = await Message.generateSessionKey(undefined, undefined, undefined, config); } else { throw new Error('No keys, passwords, or session key provided.'); }
const { data: sessionKeyData, algorithm: algorithmName, aeadAlgorithm: aeadAlgorithmName } = sessionKey;
const msg = await Message.encryptSessionKey(sessionKeyData, algorithmName, aeadAlgorithmName, encryptionKeys, passwords, wildcard, encryptionKeyIDs, date, userIDs, config);
const symEncryptedPacket = SymEncryptedIntegrityProtectedDataPacket.fromObject({ version: aeadAlgorithmName ? 2 : 1, aeadAlgorithm: aeadAlgorithmName ? enums.write(enums.aead, aeadAlgorithmName) : null }); symEncryptedPacket.packets = this.packets;
const algorithm = enums.write(enums.symmetric, algorithmName); await symEncryptedPacket.encrypt(algorithm, sessionKeyData, config);
msg.packets.push(symEncryptedPacket); symEncryptedPacket.packets = new PacketList(); return msg; }
static async encryptSessionKey(sessionKey, algorithmName, aeadAlgorithmName, encryptionKeys, passwords, wildcard = false, encryptionKeyIDs = [], date = new Date(), userIDs = [], config = defaultConfig) { const packetlist = new PacketList(); const symmetricAlgorithm = enums.write(enums.symmetric, algorithmName); const aeadAlgorithm = aeadAlgorithmName && enums.write(enums.aead, aeadAlgorithmName);
if (encryptionKeys) { const results = await Promise.all(encryptionKeys.map(async function(primaryKey, i) { const encryptionKey = await primaryKey.getEncryptionKey(encryptionKeyIDs[i], date, userIDs, config);
const pkESKeyPacket = PublicKeyEncryptedSessionKeyPacket.fromObject({ version: aeadAlgorithm ? 6 : 3, encryptionKeyPacket: encryptionKey.keyPacket, anonymousRecipient: wildcard, sessionKey, sessionKeyAlgorithm: symmetricAlgorithm });
await pkESKeyPacket.encrypt(encryptionKey.keyPacket); delete pkESKeyPacket.sessionKey; return pkESKeyPacket; })); packetlist.push(...results); } if (passwords) { const testDecrypt = async function(keyPacket, password) { try { await keyPacket.decrypt(password); return 1; } catch (e) { return 0; } };
const sum = (accumulator, currentValue) => accumulator + currentValue;
const encryptPassword = async function(sessionKey, algorithm, aeadAlgorithm, password) { const symEncryptedSessionKeyPacket = new SymEncryptedSessionKeyPacket(config); symEncryptedSessionKeyPacket.sessionKey = sessionKey; symEncryptedSessionKeyPacket.sessionKeyAlgorithm = algorithm; if (aeadAlgorithm) { symEncryptedSessionKeyPacket.aeadAlgorithm = aeadAlgorithm; } await symEncryptedSessionKeyPacket.encrypt(password, config);
if (config.passwordCollisionCheck) { const results = await Promise.all(passwords.map(pwd => testDecrypt(symEncryptedSessionKeyPacket, pwd))); if (results.reduce(sum) !== 1) { return encryptPassword(sessionKey, algorithm, password); } }
delete symEncryptedSessionKeyPacket.sessionKey; return symEncryptedSessionKeyPacket; };
const results = await Promise.all(passwords.map(pwd => encryptPassword(sessionKey, symmetricAlgorithm, aeadAlgorithm, pwd))); packetlist.push(...results); }
return new Message(packetlist); }
async sign(signingKeys = [], signature = null, signingKeyIDs = [], date = new Date(), userIDs = [], notations = [], config = defaultConfig) { const packetlist = new PacketList();
const literalDataPacket = this.packets.findPacket(enums.packet.literalData); if (!literalDataPacket) { throw new Error('No literal data packet to sign.'); }
const signaturePackets = await createSignaturePackets(literalDataPacket, signingKeys, signature, signingKeyIDs, date, userIDs, notations, false, config); const onePassSignaturePackets = signaturePackets.map( (signaturePacket, i) => OnePassSignaturePacket.fromSignaturePacket(signaturePacket, i === 0)) .reverse();
packetlist.push(...onePassSignaturePackets); packetlist.push(literalDataPacket); packetlist.push(...signaturePackets);
return new Message(packetlist); }
compress(algo, config = defaultConfig) { if (algo === enums.compression.uncompressed) { return this; }
const compressed = new CompressedDataPacket(config); compressed.algorithm = algo; compressed.packets = this.packets;
const packetList = new PacketList(); packetList.push(compressed);
return new Message(packetList); }
async signDetached(signingKeys = [], signature = null, signingKeyIDs = [], date = new Date(), userIDs = [], notations = [], config = defaultConfig) { const literalDataPacket = this.packets.findPacket(enums.packet.literalData); if (!literalDataPacket) { throw new Error('No literal data packet to sign.'); } return new Signature(await createSignaturePackets(literalDataPacket, signingKeys, signature, signingKeyIDs, date, userIDs, notations, true, config)); }
async verify(verificationKeys, date = new Date(), config = defaultConfig) { const msg = this.unwrapCompressed(); const literalDataList = msg.packets.filterByTag(enums.packet.literalData); if (literalDataList.length !== 1) { throw new Error('Can only verify message with one literal data packet.'); } if (stream.isArrayStream(msg.packets.stream)) { msg.packets.push(...await stream.readToEnd(msg.packets.stream, _ => _ || [])); } const onePassSigList = msg.packets.filterByTag(enums.packet.onePassSignature).reverse(); const signatureList = msg.packets.filterByTag(enums.packet.signature); if (onePassSigList.length && !signatureList.length && util.isStream(msg.packets.stream) && !stream.isArrayStream(msg.packets.stream)) { await Promise.all(onePassSigList.map(async onePassSig => { onePassSig.correspondingSig = new Promise((resolve, reject) => { onePassSig.correspondingSigResolve = resolve; onePassSig.correspondingSigReject = reject; }); onePassSig.signatureData = stream.fromAsync(async () => (await onePassSig.correspondingSig).signatureData); onePassSig.hashed = stream.readToEnd(await onePassSig.hash(onePassSig.signatureType, literalDataList[0], undefined, false)); onePassSig.hashed.catch(() => {}); })); msg.packets.stream = stream.transformPair(msg.packets.stream, async (readable, writable) => { const reader = stream.getReader(readable); const writer = stream.getWriter(writable); try { for (let i = 0; i < onePassSigList.length; i++) { const { value: signature } = await reader.read(); onePassSigList[i].correspondingSigResolve(signature); } await reader.readToEnd(); await writer.ready; await writer.close(); } catch (e) { onePassSigList.forEach(onePassSig => { onePassSig.correspondingSigReject(e); }); await writer.abort(e); } }); return createVerificationObjects(onePassSigList, literalDataList, verificationKeys, date, false, config); } return createVerificationObjects(signatureList, literalDataList, verificationKeys, date, false, config); }
verifyDetached(signature, verificationKeys, date = new Date(), config = defaultConfig) { const msg = this.unwrapCompressed(); const literalDataList = msg.packets.filterByTag(enums.packet.literalData); if (literalDataList.length !== 1) { throw new Error('Can only verify message with one literal data packet.'); } const signatureList = signature.packets.filterByTag(enums.packet.signature); return createVerificationObjects(signatureList, literalDataList, verificationKeys, date, true, config); }
unwrapCompressed() { const compressed = this.packets.filterByTag(enums.packet.compressedData); if (compressed.length) { return new Message(compressed[0].packets); } return this; }
async appendSignature(detachedSignature, config = defaultConfig) { await this.packets.read( util.isUint8Array(detachedSignature) ? detachedSignature : (await unarmor(detachedSignature)).data, allowedDetachedSignaturePackets, config ); }
write() { return this.packets.write(); }
armor(config = defaultConfig) { const trailingPacket = this.packets[this.packets.length - 1]; const emitChecksum = trailingPacket.constructor.tag === SymEncryptedIntegrityProtectedDataPacket.tag ? trailingPacket.version !== 2 : this.packets.some(packet => packet.constructor.tag === SignaturePacket.tag && packet.version !== 6); return armor(enums.armor.message, this.write(), null, null, null, emitChecksum, config); }}
export async function createSignaturePackets(literalDataPacket, signingKeys, signature = null, signingKeyIDs = [], date = new Date(), userIDs = [], notations = [], detached = false, config = defaultConfig) { const packetlist = new PacketList();
const signatureType = literalDataPacket.text === null ? enums.signature.binary : enums.signature.text;
await Promise.all(signingKeys.map(async (primaryKey, i) => { const userID = userIDs[i]; if (!primaryKey.isPrivate()) { throw new Error('Need private key for signing'); } const signingKey = await primaryKey.getSigningKey(signingKeyIDs[i], date, userID, config); return createSignaturePacket(literalDataPacket, primaryKey, signingKey.keyPacket, { signatureType }, date, userID, notations, detached, config); })).then(signatureList => { packetlist.push(...signatureList); });
if (signature) { const existingSigPacketlist = signature.packets.filterByTag(enums.packet.signature); packetlist.push(...existingSigPacketlist); } return packetlist;}
async function createVerificationObject(signature, literalDataList, verificationKeys, date = new Date(), detached = false, config = defaultConfig) { let primaryKey; let unverifiedSigningKey;
for (const key of verificationKeys) { const issuerKeys = key.getKeys(signature.issuerKeyID); if (issuerKeys.length > 0) { primaryKey = key; unverifiedSigningKey = issuerKeys[0]; break; } }
const isOnePassSignature = signature instanceof OnePassSignaturePacket; const signaturePacketPromise = isOnePassSignature ? signature.correspondingSig : signature;
const verifiedSig = { keyID: signature.issuerKeyID, verified: (async () => { if (!unverifiedSigningKey) { throw new Error(`Could not find signing key with key ID ${signature.issuerKeyID.toHex()}`); }
await signature.verify(unverifiedSigningKey.keyPacket, signature.signatureType, literalDataList[0], date, detached, config); const signaturePacket = await signaturePacketPromise; if (unverifiedSigningKey.getCreationTime() > signaturePacket.created) { throw new Error('Key is newer than the signature'); } try { await primaryKey.getSigningKey(unverifiedSigningKey.getKeyID(), signaturePacket.created, undefined, config); } catch (e) { if (config.allowInsecureVerificationWithReformattedKeys && e.message.match(/Signature creation time is in the future/)) { await primaryKey.getSigningKey(unverifiedSigningKey.getKeyID(), date, undefined, config); } else { throw e; } } return true; })(), signature: (async () => { const signaturePacket = await signaturePacketPromise; const packetlist = new PacketList(); signaturePacket && packetlist.push(signaturePacket); return new Signature(packetlist); })() };
verifiedSig.signature.catch(() => {}); verifiedSig.verified.catch(() => {});
return verifiedSig;}
export async function createVerificationObjects(signatureList, literalDataList, verificationKeys, date = new Date(), detached = false, config = defaultConfig) { return Promise.all(signatureList.filter(function(signature) { return ['text', 'binary'].includes(enums.read(enums.signature, signature.signatureType)); }).map(async function(signature) { return createVerificationObject(signature, literalDataList, verificationKeys, date, detached, config); }));}
export async function readMessage({ armoredMessage, binaryMessage, config, ...rest }) { config = { ...defaultConfig, ...config }; let input = armoredMessage || binaryMessage; if (!input) { throw new Error('readMessage: must pass options object containing `armoredMessage` or `binaryMessage`'); } if (armoredMessage && !util.isString(armoredMessage) && !util.isStream(armoredMessage)) { throw new Error('readMessage: options.armoredMessage must be a string or stream'); } if (binaryMessage && !util.isUint8Array(binaryMessage) && !util.isStream(binaryMessage)) { throw new Error('readMessage: options.binaryMessage must be a Uint8Array or stream'); } const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw new Error(`Unknown option: ${unknownOptions.join(', ')}`);
const streamType = util.isStream(input); if (armoredMessage) { const { type, data } = await unarmor(input, config); if (type !== enums.armor.message) { throw new Error('Armored text not of type message'); } input = data; } const packetlist = await PacketList.fromBinary(input, allowedMessagePackets, config); const message = new Message(packetlist); message.fromStream = streamType; return message;}
export async function createMessage({ text, binary, filename, date = new Date(), format = text !== undefined ? 'utf8' : 'binary', ...rest }) { const input = text !== undefined ? text : binary; if (input === undefined) { throw new Error('createMessage: must pass options object containing `text` or `binary`'); } if (text && !util.isString(text) && !util.isStream(text)) { throw new Error('createMessage: options.text must be a string or stream'); } if (binary && !util.isUint8Array(binary) && !util.isStream(binary)) { throw new Error('createMessage: options.binary must be a Uint8Array or stream'); } const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw new Error(`Unknown option: ${unknownOptions.join(', ')}`);
const streamType = util.isStream(input); const literalDataPacket = new LiteralDataPacket(date); if (text !== undefined) { literalDataPacket.setText(input, enums.write(enums.literal, format)); } else { literalDataPacket.setBytes(input, enums.write(enums.literal, format)); } if (filename !== undefined) { literalDataPacket.setFilename(filename); } const literalDataPacketlist = new PacketList(); literalDataPacketlist.push(literalDataPacket); const message = new Message(literalDataPacketlist); message.fromStream = streamType; return message;}