Don't keep extra copies of streams in memory

This commit is contained in:
Daniel Huigens 2018-05-30 17:35:19 +02:00
parent 78a0ca937e
commit 56ec5b3a8d
9 changed files with 64 additions and 59 deletions

View File

@ -45,7 +45,7 @@ function KeyPair(curve, options) {
} }
KeyPair.prototype.sign = async function (message, hash_algo, hashed) { KeyPair.prototype.sign = async function (message, hash_algo, hashed) {
if (!message.locked) { if (message && !message.locked) {
message = await stream.readToEnd(message); message = await stream.readToEnd(message);
if (this.curve.web && util.getWebCrypto()) { if (this.curve.web && util.getWebCrypto()) {
// If browser doesn't support a curve, we'll catch it // If browser doesn't support a curve, we'll catch it
@ -65,7 +65,7 @@ KeyPair.prototype.sign = async function (message, hash_algo, hashed) {
}; };
KeyPair.prototype.verify = async function (message, signature, hash_algo, hashed) { KeyPair.prototype.verify = async function (message, signature, hash_algo, hashed) {
if (!message.locked) { if (message && !message.locked) {
message = await stream.readToEnd(message); message = await stream.readToEnd(message);
if (this.curve.web && util.getWebCrypto()) { if (this.curve.web && util.getWebCrypto()) {
// If browser doesn't support a curve, we'll catch it // If browser doesn't support a curve, we'll catch it

View File

@ -127,6 +127,9 @@ Message.prototype.decrypt = async function(privateKeys, passwords, sessionKeys)
exception = e; exception = e;
} }
} }
// We don't await stream.cancel here because... it sometimes hangs indefinitely. No clue why.
stream.cancel(symEncryptedPacket.encrypted); // Don't keep copy of encrypted data in memory.
symEncryptedPacket.encrypted = null;
if (!symEncryptedPacket.packets || !symEncryptedPacket.packets.length) { if (!symEncryptedPacket.packets || !symEncryptedPacket.packets.length) {
throw exception || new Error('Decryption failed.'); throw exception || new Error('Decryption failed.');
@ -163,6 +166,8 @@ Message.prototype.decryptSessionKeys = async function(privateKeys, passwords) {
util.print_debug_error(err); util.print_debug_error(err);
} }
})); }));
stream.cancel(keyPacket.encrypted); // Don't keep copy of encrypted data in memory.
keyPacket.encrypted = null;
})); }));
} else if (privateKeys) { } else if (privateKeys) {
const pkESKeyPacketlist = this.packets.filterByTag(enums.packet.publicKeyEncryptedSessionKey); const pkESKeyPacketlist = this.packets.filterByTag(enums.packet.publicKeyEncryptedSessionKey);
@ -188,6 +193,8 @@ Message.prototype.decryptSessionKeys = async function(privateKeys, passwords) {
util.print_debug_error(err); util.print_debug_error(err);
} }
})); }));
stream.cancel(keyPacket.encrypted); // Don't keep copy of encrypted data in memory.
keyPacket.encrypted = null;
})); }));
} else { } else {
throw new Error('No key or password specified.'); throw new Error('No key or password specified.');
@ -543,7 +550,7 @@ Message.prototype.verify = async function(keys, date=new Date()) {
onePassSig.signatureData = stream.fromAsync(() => new Promise(resolve => { onePassSig.signatureData = stream.fromAsync(() => new Promise(resolve => {
onePassSig.signatureDataResolve = resolve; onePassSig.signatureDataResolve = resolve;
})); }));
onePassSig.hash(literalDataList[0]); onePassSig.hashed = onePassSig.hash(literalDataList[0]);
}); });
const reader = stream.getReader(msg.packets.stream); const reader = stream.getReader(msg.packets.stream);
for (let i = 0; ; i++) { for (let i = 0; ; i++) {

View File

@ -365,14 +365,15 @@ export function decrypt({ message, privateKeys, passwords, sessionKeys, publicKe
} }
return message.decrypt(privateKeys, passwords, sessionKeys).then(async function(message) { return message.decrypt(privateKeys, passwords, sessionKeys).then(async function(message) {
const result = await parseMessage(message, format, asStream);
if (!publicKeys) { if (!publicKeys) {
publicKeys = []; publicKeys = [];
} }
const result = {};
result.signatures = signature ? await message.verifyDetached(signature, publicKeys, date) : await message.verify(publicKeys, date); result.signatures = signature ? await message.verifyDetached(signature, publicKeys, date) : await message.verify(publicKeys, date);
result.data = format === 'binary' ? message.getLiteralData() : message.getText();
result.data = await convertStream(result.data, asStream);
result.filename = message.getFilename();
return result; return result;
}).catch(onError.bind(null, 'Error decrypting message')); }).catch(onError.bind(null, 'Error decrypting message'));
} }
@ -596,27 +597,6 @@ function createMessage(data, filename, date=new Date(), type) {
return msg; return msg;
} }
/**
* Parse the message given a certain format.
* @param {Message} message the message object to be parse
* @param {String} format the output format e.g. 'utf8' or 'binary'
* @param {Boolean} asStream whether to return a ReadableStream
* @returns {Object} the parse data in the respective format
*/
async function parseMessage(message, format, asStream) {
let data;
if (format === 'binary') {
data = message.getLiteralData();
} else if (format === 'utf8') {
data = message.getText();
} else {
throw new Error('Invalid format');
}
data = await convertStream(data, asStream);
const filename = message.getFilename();
return { data, filename };
}
/** /**
* Convert data to or from Stream * Convert data to or from Stream
* @param {Object} data the data to convert * @param {Object} data the data to convert

View File

@ -61,8 +61,8 @@ Literal.prototype.setText = function(text, format='utf8') {
* with normalized end of line to \n * with normalized end of line to \n
* @returns {String} literal data as text * @returns {String} literal data as text
*/ */
Literal.prototype.getText = function() { Literal.prototype.getText = function(clone=false) {
if (this.text === null) { if (this.text === null || this.text.locked) {
let lastChar = ''; let lastChar = '';
const decoder = new TextDecoder('utf8'); const decoder = new TextDecoder('utf8');
// eslint-disable-next-line no-inner-declarations // eslint-disable-next-line no-inner-declarations
@ -79,9 +79,9 @@ Literal.prototype.getText = function() {
lastChar = ''; lastChar = '';
return normalized; return normalized;
} }
this.text = stream.transform(stream.clone(this.data), process, () => process(new Uint8Array(), true)); this.text = stream.transform(this.getBytes(clone), process, () => process(new Uint8Array(), true));
} }
return stream.clone(this.text); return this.text;
}; };
/** /**
@ -100,14 +100,17 @@ Literal.prototype.setBytes = function(bytes, format) {
* Get the byte sequence representing the literal packet data * Get the byte sequence representing the literal packet data
* @returns {Uint8Array} A sequence of bytes * @returns {Uint8Array} A sequence of bytes
*/ */
Literal.prototype.getBytes = function() { Literal.prototype.getBytes = function(clone=false) {
if (this.data === null) { if (this.data === null) {
// normalize EOL to \r\n // normalize EOL to \r\n
const text = util.canonicalizeEOL(this.text); const text = util.canonicalizeEOL(this.text);
// encode UTF8 // encode UTF8
this.data = util.str_to_Uint8Array(util.encode_utf8(text)); this.data = util.str_to_Uint8Array(util.encode_utf8(text));
} }
return stream.clone(this.data); if (clone) {
return stream.clone(this.data);
}
return this.data;
}; };

View File

@ -561,12 +561,12 @@ Signature.prototype.toSign = function (type, data) {
switch (type) { switch (type) {
case t.binary: case t.binary:
if (data.text !== null) { if (data.text !== null) {
return util.str_to_Uint8Array(data.getText()); return util.str_to_Uint8Array(data.getText(true));
} }
return data.getBytes(); return data.getBytes(true);
case t.text: { case t.text: {
let text = data.getText(); let text = data.getText(true);
// normalize EOL to \r\n // normalize EOL to \r\n
text = util.canonicalizeEOL(text); text = util.canonicalizeEOL(text);
// encode UTF8 // encode UTF8
@ -659,11 +659,8 @@ Signature.prototype.toHash = function(data) {
}; };
Signature.prototype.hash = function(data, toHash) { Signature.prototype.hash = function(data, toHash) {
if (!this.hashed) { const hashAlgorithm = enums.write(enums.hash, this.hashAlgorithm);
const hashAlgorithm = enums.write(enums.hash, this.hashAlgorithm); return crypto.hash.digest(hashAlgorithm, toHash || this.toHash(data));
this.hashed = crypto.hash.digest(hashAlgorithm, toHash || this.toHash(data));
}
return this.hashed;
}; };
@ -679,8 +676,15 @@ Signature.prototype.verify = async function (key, data) {
const publicKeyAlgorithm = enums.write(enums.publicKey, this.publicKeyAlgorithm); const publicKeyAlgorithm = enums.write(enums.publicKey, this.publicKeyAlgorithm);
const hashAlgorithm = enums.write(enums.hash, this.hashAlgorithm); const hashAlgorithm = enums.write(enums.hash, this.hashAlgorithm);
const toHash = this.toHash(data); let toHash;
const hash = await stream.readToEnd(this.hash(data, toHash)); let hash;
if (this.hashed) {
hash = this.hashed;
} else {
toHash = this.toHash(data);
hash = this.hash(data, toHash);
}
hash = await stream.readToEnd(hash);
if (this.signedHashValue[0] !== hash[0] || if (this.signedHashValue[0] !== hash[0] ||
this.signedHashValue[1] !== hash[1]) { this.signedHashValue[1] !== hash[1]) {

View File

@ -96,7 +96,7 @@ SymEncryptedAEADProtected.prototype.decrypt = async function (sessionKeyAlgorith
if (config.aead_protect_version !== 4) { if (config.aead_protect_version !== 4) {
this.cipherAlgo = enums.write(enums.symmetric, sessionKeyAlgorithm); this.cipherAlgo = enums.write(enums.symmetric, sessionKeyAlgorithm);
} }
await this.packets.read(await this.crypt('decrypt', key, this.encrypted)); await this.packets.read(await this.crypt('decrypt', key, stream.clone(this.encrypted)));
return true; return true;
}; };

View File

@ -62,8 +62,16 @@ function tee(input) {
function clone(input) { function clone(input) {
if (util.isStream(input)) { if (util.isStream(input)) {
const teed = tee(input); const teed = tee(input);
input.getReader = teed[0].getReader.bind(teed[0]); // Overwrite input.getReader, input.locked, etc to point to teed[0]
input.tee = teed[0].tee.bind(teed[0]); Object.entries(Object.getOwnPropertyDescriptors(ReadableStream.prototype)).forEach(([name, descriptor]) => {
if (name === 'constructor') return;
if (descriptor.value) {
descriptor.value = descriptor.value.bind(teed[0]);
} else {
descriptor.get = descriptor.get.bind(teed[0]);
}
Object.defineProperty(input, name, descriptor);
});
return teed[1]; return teed[1];
} }
return subarray(input); return subarray(input);
@ -108,6 +116,12 @@ async function readToEnd(input, join) {
return input; return input;
} }
async function cancel(input) {
if (util.isStream(input)) {
return input.cancel();
}
}
function fromAsync(fn) { function fromAsync(fn) {
return new ReadableStream({ return new ReadableStream({
pull: async controller => { pull: async controller => {
@ -187,7 +201,7 @@ if (nodeStream) {
} }
export default { concat, getReader, transform, clone, subarray, readToEnd, nodeToWeb, webToNode, fromAsync }; export default { concat, getReader, transform, clone, subarray, readToEnd, cancel, nodeToWeb, webToNode, fromAsync };
/*const readerAcquiredMap = new Map(); /*const readerAcquiredMap = new Map();

View File

@ -1828,14 +1828,13 @@ describe('OpenPGP.js public api tests', function() {
const literals = packets.packets.filterByTag(openpgp.enums.packet.literal); const literals = packets.packets.filterByTag(openpgp.enums.packet.literal);
expect(literals.length).to.equal(1); expect(literals.length).to.equal(1);
expect(+literals[0].date).to.equal(+past); expect(+literals[0].date).to.equal(+past);
expect(await openpgp.stream.readToEnd(packets.getText())).to.equal(plaintext); const signatures = await packets.verify(encryptOpt.publicKeys, past);
return packets.verify(encryptOpt.publicKeys, past);
}).then(function (signatures) {
expect(+signatures[0].signature.packets[0].created).to.equal(+past); expect(+signatures[0].signature.packets[0].created).to.equal(+past);
expect(signatures[0].valid).to.be.true; expect(signatures[0].valid).to.be.true;
expect(encryptOpt.privateKeys[0].getSigningKey(signatures[0].keyid, past)) expect(encryptOpt.privateKeys[0].getSigningKey(signatures[0].keyid, past))
.to.be.not.null; .to.be.not.null;
expect(signatures[0].signature.packets.length).to.equal(1); expect(signatures[0].signature.packets.length).to.equal(1);
expect(await openpgp.stream.readToEnd(packets.getText())).to.equal(plaintext);
}); });
}); });
@ -1857,14 +1856,13 @@ describe('OpenPGP.js public api tests', function() {
expect(literals.length).to.equal(1); expect(literals.length).to.equal(1);
expect(literals[0].format).to.equal('binary'); expect(literals[0].format).to.equal('binary');
expect(+literals[0].date).to.equal(+future); expect(+literals[0].date).to.equal(+future);
expect(await openpgp.stream.readToEnd(packets.getLiteralData())).to.deep.equal(data); const signatures = await packets.verify(encryptOpt.publicKeys, future);
return packets.verify(encryptOpt.publicKeys, future);
}).then(function (signatures) {
expect(+signatures[0].signature.packets[0].created).to.equal(+future); expect(+signatures[0].signature.packets[0].created).to.equal(+future);
expect(signatures[0].valid).to.be.true; expect(signatures[0].valid).to.be.true;
expect(encryptOpt.privateKeys[0].getSigningKey(signatures[0].keyid, future)) expect(encryptOpt.privateKeys[0].getSigningKey(signatures[0].keyid, future))
.to.be.not.null; .to.be.not.null;
expect(signatures[0].signature.packets.length).to.equal(1); expect(signatures[0].signature.packets.length).to.equal(1);
expect(await openpgp.stream.readToEnd(packets.getLiteralData())).to.deep.equal(data);
}); });
}); });
@ -1887,14 +1885,13 @@ describe('OpenPGP.js public api tests', function() {
expect(literals.length).to.equal(1); expect(literals.length).to.equal(1);
expect(literals[0].format).to.equal('mime'); expect(literals[0].format).to.equal('mime');
expect(+literals[0].date).to.equal(+future); expect(+literals[0].date).to.equal(+future);
expect(await openpgp.stream.readToEnd(packets.getLiteralData())).to.deep.equal(data); const signatures = await packets.verify(encryptOpt.publicKeys, future);
return packets.verify(encryptOpt.publicKeys, future);
}).then(function (signatures) {
expect(+signatures[0].signature.packets[0].created).to.equal(+future); expect(+signatures[0].signature.packets[0].created).to.equal(+future);
expect(signatures[0].valid).to.be.true; expect(signatures[0].valid).to.be.true;
expect(encryptOpt.privateKeys[0].getSigningKey(signatures[0].keyid, future)) expect(encryptOpt.privateKeys[0].getSigningKey(signatures[0].keyid, future))
.to.be.not.null; .to.be.not.null;
expect(signatures[0].signature.packets.length).to.equal(1); expect(signatures[0].signature.packets.length).to.equal(1);
expect(await openpgp.stream.readToEnd(packets.getLiteralData())).to.deep.equal(data);
}); });
}); });

View File

@ -523,15 +523,15 @@ describe("Signature", function() {
expect(pubKey2.getKeys(keyids[1])).to.not.be.empty; expect(pubKey2.getKeys(keyids[1])).to.not.be.empty;
expect(pubKey3.getKeys(keyids[0])).to.not.be.empty; expect(pubKey3.getKeys(keyids[0])).to.not.be.empty;
expect(await openpgp.stream.readToEnd(sMsg.getText())).to.equal(plaintext); return sMsg.verify([pubKey2, pubKey3]).then(async verifiedSig => {
return sMsg.verify([pubKey2, pubKey3]).then(verifiedSig => {
expect(verifiedSig).to.exist; expect(verifiedSig).to.exist;
expect(verifiedSig).to.have.length(2); expect(verifiedSig).to.have.length(2);
expect(verifiedSig[0].valid).to.be.true; expect(verifiedSig[0].valid).to.be.true;
expect(verifiedSig[1].valid).to.be.true; expect(verifiedSig[1].valid).to.be.true;
expect(verifiedSig[0].signature.packets.length).to.equal(1); expect(verifiedSig[0].signature.packets.length).to.equal(1);
expect(verifiedSig[1].signature.packets.length).to.equal(1); expect(verifiedSig[1].signature.packets.length).to.equal(1);
expect(await openpgp.stream.readToEnd(sMsg.getText())).to.equal(plaintext);
}); });
}); });