import * as stream from '@openpgp/web-stream-tools';import { armor, unarmor } from './encoding/armor';import KeyID from './type/keyid';import defaultConfig from './config';import crypto from './crypto';import enums from './enums';import util from './util';import { Signature } from './signature';import { getPreferredHashAlgo, getPreferredAlgo, isAEADSupported, 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 sessionKeyObjects = sessionKeys || await this.decryptSessionKeys(decryptionKeys, passwords, date, config);
const symEncryptedPacketlist = this.packets.filterByTag( enums.packet.symmetricallyEncryptedData, enums.packet.symEncryptedIntegrityProtectedData, enums.packet.aeadEncryptedData );
if (symEncryptedPacketlist.length === 0) { return this; }
const symEncryptedPacket = symEncryptedPacketlist[0]; let exception = null; const decryptedPromise = Promise.all(sessionKeyObjects.map(async ({ algorithm: algorithmName, data }) => { if (!util.isUint8Array(data) || !util.isString(algorithmName)) { throw new Error('Invalid session key for decryption.'); }
try { const algo = 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, 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); } })); })); } 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 primaryUser = await decryptionKey.getPrimaryUser(date, undefined, config); if (primaryUser.selfCertification.preferredSymmetricAlgorithms) { algos = algos.concat(primaryUser.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(Array.from(config.constantTimePKCS1DecryptionSupportedSymmetricAlgorithms).map(async sessionKeyAlgorithm => { const pkeskPacketCopy = new PublicKeyEncryptedSessionKeyPacket(); pkeskPacketCopy.read(serialisedPKESK); const randomSessionKey = { sessionKeyAlgorithm, sessionKey: await 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); if (!algos.includes(enums.write(enums.symmetric, pkeskPacket.sessionKeyAlgorithm))) { 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: 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 algo = await getPreferredAlgo('symmetric', encryptionKeys, date, userIDs, config); const algorithmName = enums.read(enums.symmetric, algo); const aeadAlgorithmName = config.aeadProtect && await isAEADSupported(encryptionKeys, date, userIDs, config) ? enums.read(enums.aead, await getPreferredAlgo('aead', encryptionKeys, date, userIDs, config)) : undefined;
const sessionKeyData = await crypto.generateSessionKey(algo); return { data: sessionKeyData, algorithm: algorithmName, aeadAlgorithm: aeadAlgorithmName }; }
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);
let symEncryptedPacket; if (aeadAlgorithmName) { symEncryptedPacket = new AEADEncryptedDataPacket(); symEncryptedPacket.aeadAlgorithm = enums.write(enums.aead, aeadAlgorithmName); } else { symEncryptedPacket = new SymEncryptedIntegrityProtectedDataPacket(); } 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 algorithm = 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 = new PublicKeyEncryptedSessionKeyPacket(); pkESKeyPacket.publicKeyID = wildcard ? KeyID.wildcard() : encryptionKey.getKeyID(); pkESKeyPacket.publicKeyAlgorithm = encryptionKey.keyPacket.algorithm; pkESKeyPacket.sessionKey = sessionKey; pkESKeyPacket.sessionKeyAlgorithm = algorithm; 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, algorithm, aeadAlgorithm, pwd))); packetlist.push(...results); }
return new Message(packetlist); }
async sign(signingKeys = [], signature = null, signingKeyIDs = [], date = new Date(), userIDs = [], 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.'); }
let i; let existingSigPacketlist; const signatureType = literalDataPacket.text === null ? enums.signature.binary : enums.signature.text;
if (signature) { existingSigPacketlist = signature.packets.filterByTag(enums.packet.signature); for (i = existingSigPacketlist.length - 1; i >= 0; i--) { const signaturePacket = existingSigPacketlist[i]; const onePassSig = new OnePassSignaturePacket(); onePassSig.signatureType = signaturePacket.signatureType; onePassSig.hashAlgorithm = signaturePacket.hashAlgorithm; onePassSig.publicKeyAlgorithm = signaturePacket.publicKeyAlgorithm; onePassSig.issuerKeyID = signaturePacket.issuerKeyID; if (!signingKeys.length && i === 0) { onePassSig.flags = 1; } packetlist.push(onePassSig); } }
await Promise.all(Array.from(signingKeys).reverse().map(async function (primaryKey, i) { if (!primaryKey.isPrivate()) { throw new Error('Need private key for signing'); } const signingKeyID = signingKeyIDs[signingKeys.length - 1 - i]; const signingKey = await primaryKey.getSigningKey(signingKeyID, date, userIDs, config); const onePassSig = new OnePassSignaturePacket(); onePassSig.signatureType = signatureType; onePassSig.hashAlgorithm = await getPreferredHashAlgo(primaryKey, signingKey.keyPacket, date, userIDs, config); onePassSig.publicKeyAlgorithm = signingKey.keyPacket.algorithm; onePassSig.issuerKeyID = signingKey.getKeyID(); if (i === signingKeys.length - 1) { onePassSig.flags = 1; } return onePassSig; })).then(onePassSignatureList => { onePassSignatureList.forEach(onePassSig => packetlist.push(onePassSig)); });
packetlist.push(literalDataPacket); packetlist.push(...(await createSignaturePackets(literalDataPacket, signingKeys, signature, signingKeyIDs, date, userIDs, false, config)));
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 = [], 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, 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; 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) { return armor(enums.armor.message, this.write(), null, null, null, config); }}
export async function createSignaturePackets(literalDataPacket, signingKeys, signature = null, signingKeyIDs = [], date = new Date(), userIDs = [], 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, 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 (streamType) { await stream.loadStreamsPonyfill(); input = stream.toStream(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 }) { let 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); if (streamType) { await stream.loadStreamsPonyfill(); input = stream.toStream(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;}