From 37014ecf308e393df8f697becd39b94470f3bb7a Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Wed, 16 May 2018 21:33:22 +0200 Subject: [PATCH] Pass more tests - Allow leading spaces in headers (since we were already accepting leading spaces everywhere else in the armored text). - Read ReadableStreams before passing them to a Worker --- src/encoding/armor.js | 13 +++++------ src/openpgp.js | 9 +++++--- src/packet/packet.js | 7 +++--- src/packet/sym_encrypted_aead_protected.js | 4 ++-- src/util.js | 27 +++++++++++++--------- src/worker/async_proxy.js | 7 +++--- test/general/armor.js | 12 +++++----- test/general/key.js | 10 ++++---- test/general/packet.js | 8 +++---- test/general/signature.js | 4 ++-- test/general/util.js | 18 +++++++-------- 11 files changed, 64 insertions(+), 55 deletions(-) diff --git a/src/encoding/armor.js b/src/encoding/armor.js index a18dce6d..713ead63 100644 --- a/src/encoding/armor.js +++ b/src/encoding/armor.js @@ -207,7 +207,6 @@ function dearmor(input) { const reEmptyLine = /^[ \f\r\t\u00a0\u2000-\u200a\u202f\u205f\u3000]*$/; const reader = stream.getReader(input); - let lineIndex = 0; let type; const headers = []; let lastHeaders = headers; @@ -232,13 +231,13 @@ function dearmor(input) { }); while (true) { let line = await reader.readLine(); - if (!line) break; - if (lineIndex++ === 0) { - // trim string - line = line.trim(); + if (line === undefined) { + controller.error('Misformed armored text'); + break; } // remove trailing whitespace at end of lines - line = line.replace(/[\t\r\n ]+$/g, ''); + // remove leading whitespace for compat with older versions of OpenPGP.js + line = line.trim(); if (!type) { if (reSplit.test(line)) { type = getType(line); @@ -257,7 +256,7 @@ function dearmor(input) { } else if (!textDone && type === 2) { if (!reSplit.test(line)) { // Reverse dash-escaping for msg - text.push(line.replace(/^- /mg, '')); + text.push(line.replace(/^- /, '')); } else { text = text.join('\r\n'); textDone = true; diff --git a/src/openpgp.js b/src/openpgp.js index c0fde9f4..a330771e 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -127,15 +127,15 @@ export function generateKey({ userIds=[], passphrase="", numBits=2048, keyExpira return asyncProxy.delegate('generateKey', options); } - return generate(options).then(key => { + return generate(options).then(async key => { const revocationCertificate = key.getRevocationCertificate(); key.revocationSignatures = []; return { key: key, - privateKeyArmored: key.armor(), - publicKeyArmored: key.toPublic().armor(), + privateKeyArmored: await stream.readToEnd(key.armor()), + publicKeyArmored: await stream.readToEnd(key.toPublic().armor()) revocationCertificate: revocationCertificate }; @@ -453,6 +453,9 @@ export function verify({ message, publicKeys, signature=null, date=new Date() }) return Promise.resolve().then(async function() { const result = {}; result.data = message instanceof CleartextMessage ? message.getText() : message.getLiteralData(); + if (!message.fromStream) { + result.data = await stream.readToEnd(result.data); + } result.signatures = signature ? await message.verifyDetached(signature, publicKeys, date) : await message.verify(publicKeys, date); diff --git a/src/packet/packet.js b/src/packet/packet.js index d7fd4f4a..b6fbcbf7 100644 --- a/src/packet/packet.js +++ b/src/packet/packet.js @@ -235,14 +235,15 @@ export default { if (bodydata === null) { bodydata = await reader.readBytes(packet_length); + const peekedByte = await reader.peekBytes(1); resolve({ tag: tag, packet: bodydata, - done: !await reader.peekBytes(1) + done: !(peekedByte && peekedByte.length) }); } else { - const { done } = await reader.read(); - if (!done) { + const { done, value } = await reader.read(); + if (!done && value.length) { throw new Error('Packets after a packet with partial lengths are not supported'); } else { controller.close(); diff --git a/src/packet/sym_encrypted_aead_protected.js b/src/packet/sym_encrypted_aead_protected.js index 2775ae5c..33e51fa1 100644 --- a/src/packet/sym_encrypted_aead_protected.js +++ b/src/packet/sym_encrypted_aead_protected.js @@ -165,7 +165,7 @@ SymEncryptedAEADProtected.prototype.crypt = async function (fn, key, data) { cryptedBytes += chunk.length - tagLengthIfDecrypting; queuedBytes += chunk.length - tagLengthIfDecrypting; latestPromise = latestPromise.then(() => cryptedPromise).then(crypted => { - if (crypted.length) controller.enqueue(crypted); + controller.enqueue(crypted); queuedBytes -= chunk.length; }).catch(err => controller.error(err)); // console.log(fn, done, queuedBytes, controller.desiredSize); @@ -181,6 +181,6 @@ SymEncryptedAEADProtected.prototype.crypt = async function (fn, key, data) { } }); } else { - return modeInstance[fn](data, this.iv); + return modeInstance[fn](await stream.readToEnd(data), this.iv); } }; diff --git a/src/util.js b/src/util.js index ba0052ec..2c951914 100644 --- a/src/util.js +++ b/src/util.js @@ -53,33 +53,38 @@ export default { /** * Get transferable objects to pass buffers with zero copy (similar to "pass by reference" in C++) * See: https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage + * Also, convert ReadableStreams to Uint8Arrays * @param {Object} obj the options object to be passed to the web worker * @returns {Array} an array of binary data to be passed */ - getTransferables: function(obj) { + prepareBuffers: async function(obj) { // Internet Explorer does not support Transferable objects. if (isIE11) { return undefined; } - if (config.zero_copy && Object.prototype.isPrototypeOf(obj)) { - const transferables = []; - util.collectBuffers(obj, transferables); - return transferables.length ? transferables : undefined; - } + const transferables = []; + await util.collectBuffers(obj, transferables); + return transferables.length ? transferables : undefined; }, - collectBuffers: function(obj, collection) { + collectBuffers: async function(obj, collection) { if (!obj) { return; } + if (util.isUint8Array(obj) && collection.indexOf(obj.buffer) === -1) { - collection.push(obj.buffer); + if (config.zero_copy) { + collection.push(obj.buffer); + } return; } if (Object.prototype.isPrototypeOf(obj)) { - Object.values(obj).forEach(value => { // recursively search all children - util.collectBuffers(value, collection); - }); + await Promise.all(Object.entries(obj).map(async ([key, value]) => { // recursively search all children + if (util.isStream(value)) { + obj[key] = value = await stream.readToEnd(value); + } + await util.collectBuffers(value, collection); + })); } }, diff --git a/src/worker/async_proxy.js b/src/worker/async_proxy.js index f1c767d7..9b82d07b 100644 --- a/src/worker/async_proxy.js +++ b/src/worker/async_proxy.js @@ -112,7 +112,7 @@ AsyncProxy.prototype.getID = function() { */ AsyncProxy.prototype.seedRandom = async function(workerId, size) { const buf = await crypto.random.getRandomBytes(size); - this.workers[workerId].postMessage({ event:'seed-random', buf }, util.getTransferables(buf)); + this.workers[workerId].postMessage({ event:'seed-random', buf }, await util.prepareBuffers(buf)); }; /** @@ -143,9 +143,10 @@ AsyncProxy.prototype.delegate = function(method, options) { } } - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { // clone packets (for web worker structured cloning algorithm) - this.workers[workerId].postMessage({ id:id, event:method, options:packet.clone.clonePackets(options) }, util.getTransferables(options)); + const transferables = await util.prepareBuffers(options); + this.workers[workerId].postMessage({ id:id, event:method, options:packet.clone.clonePackets(options) }, transferables); this.workers[workerId].requests++; // remember to handle parsing cloned packets from worker diff --git a/test/general/armor.js b/test/general/armor.js index 518292ff..f2b6c5a6 100644 --- a/test/general/armor.js +++ b/test/general/armor.js @@ -112,18 +112,18 @@ describe("ASCII armor", function() { await expect(msg).to.be.rejectedWith(Error, /Improperly formatted armor header/); }); - it('Exception if improperly formatted armor header - signature section', function () { - [' Space: leading', 'Space : trailing', 'Space :switched', ': empty', 'none', 'Space:missing'].forEach(function (invalidHeader) { - expect(openpgp.cleartext.readArmored(getArmor(['Hash: SHA1'], [invalidHeader]))).to.be.rejectedWith(Error, /Improperly formatted armor header/); - }); + it('Exception if improperly formatted armor header - signature section', async function () { + await Promise.all(['Space : trailing', 'Space :switched', ': empty', 'none', 'Space:missing'].map(async function (invalidHeader) { + await expect(openpgp.cleartext.readArmored(getArmor(['Hash: SHA1'], [invalidHeader]))).to.be.rejectedWith(Error, /Improperly formatted armor header/); + })); }); it('Ignore unknown armor header - signature section', async function () { const validHeaders = ['Version: BCPG C# v1.7.4114.6375', 'Independent Reserve Pty. Ltd. 2017: 1.0.0.0']; expect(await openpgp.cleartext.readArmored(getArmor(['Hash: SHA1'], validHeaders))).to.be.an.instanceof(openpgp.cleartext.CleartextMessage); - ['A: Hello', 'Ab: 1.2.3', 'Abcd: #!/yah', 'Acd 123 5.6.$.8: Hello', '_: Hello', '*: Hello', '* & ## ?? ()(): Hello', '( ): Weird'].forEach(async function (validHeader) { + await Promise.all(['A: Hello', 'Ab: 1.2.3', 'Abcd: #!/yah', 'Acd 123 5.6.$.8: Hello', '_: Hello', '*: Hello', '* & ## ?? ()(): Hello', '( ): Weird'].map(async function (validHeader) { expect(await openpgp.cleartext.readArmored(getArmor(['Hash: SHA1'], [validHeader]))).to.be.an.instanceof(openpgp.cleartext.CleartextMessage); - }); + })); }); it('Exception if wrong armor header type', async function () { diff --git a/test/general/key.js b/test/general/key.js index 88b7e97f..7d2cbf05 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -346,7 +346,7 @@ zoGJ6s48HcP591pN93uAitCcYcinY2ZslmdiCXw+zbeoX4spNrV4T4CYxBjNQdIa 'BIDZSFjrJY/gm2kgQX2Pn9hGqDdGhxiALjxhA0+OJQNw4v11y0zVGdofh0IHjkcZ', 'onCOcv4DKguN2w==', '=OqO3', - '-----END PGP PUBLIC KEY BLOCK----'].join('\n'); + '-----END PGP PUBLIC KEY BLOCK-----'].join('\n'); const pub_v3 = ['-----BEGIN PGP PUBLIC KEY BLOCK-----', @@ -1731,7 +1731,7 @@ t/ia1kMpSEiOVLlX5dfHZzhR3WNtBqU= }; const opt = {numBits: 512, userIds: 'test ', passphrase: 'hello'}; if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys - return openpgp.generateKey(opt).then(function(key) { + return openpgp.generateKey(opt).then(async function(key) { testPref(key.key); testPref((await openpgp.key.readArmored(key.publicKeyArmored)).keys[0]); }); @@ -1877,8 +1877,8 @@ VYGdb3eNlV8CfoEC const opt = {numBits: 512, userIds: userId, passphrase: 'passphrase'}; if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys const key = (await openpgp.generateKey(opt)).key; - const armor1 = key.armor(); - const armor2 = key.armor(); + const armor1 = await openpgp.stream.readToEnd(key.armor()); + const armor2 = await openpgp.stream.readToEnd(key.armor()); expect(armor1).to.equal(armor2); expect(await key.decrypt('passphrase')).to.be.true; expect(key.isDecrypted()).to.be.true; @@ -1888,7 +1888,7 @@ VYGdb3eNlV8CfoEC expect(key.isDecrypted()).to.be.false; expect(await key.decrypt('new_passphrase')).to.be.true; expect(key.isDecrypted()).to.be.true; - const armor3 = key.armor(); + const armor3 = await openpgp.stream.readToEnd(key.armor()); expect(armor3).to.not.equal(armor1); }); diff --git a/test/general/packet.js b/test/general/packet.js index 3516af72..6459dd01 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -249,10 +249,10 @@ describe("Packet", function() { return parsed[0].decrypt('test').then(() => { const key = parsed[0].sessionKey; - return parsed[1].decrypt(parsed[0].sessionKeyAlgorithm, key).then(() => { + return parsed[1].decrypt(parsed[0].sessionKeyAlgorithm, key).then(async () => { const compressed = parsed[1].packets[0]; - const result = stringify(compressed.packets[0].data); + const result = await stringify(compressed.packets[0].data); expect(result).to.equal('Hello world!\n'); }); @@ -393,7 +393,7 @@ describe("Packet", function() { return msg[0].decrypt(key).then(async () => { await msg[1].decrypt(msg[0].sessionKeyAlgorithm, msg[0].sessionKey); - const text = stringify(msg[1].packets[0].packets[0].data); + const text = await stringify(msg[1].packets[0].packets[0].data); expect(text).to.equal('Hello world!'); }); @@ -654,7 +654,7 @@ describe("Packet", function() { return msg[0].decrypt(key).then(async () => { await msg[1].decrypt(msg[0].sessionKeyAlgorithm, msg[0].sessionKey); - const text = stringify(msg[1].packets[0].packets[0].data); + const text = await stringify(msg[1].packets[0].packets[0].data); expect(text).to.equal('Hello world!'); }); diff --git a/test/general/signature.js b/test/general/signature.js index d58c2d05..51b5c7fe 100644 --- a/test/general/signature.js +++ b/test/general/signature.js @@ -523,7 +523,7 @@ describe("Signature", function() { expect(pubKey2.getKeys(keyids[1])).to.not.be.empty; expect(pubKey3.getKeys(keyids[0])).to.not.be.empty; - expect(sMsg.getText()).to.equal(plaintext); + expect(await openpgp.stream.readToEnd(sMsg.getText())).to.equal(plaintext); return sMsg.verify([pubKey2, pubKey3]).then(verifiedSig => { expect(verifiedSig).to.exist; @@ -720,7 +720,7 @@ yYDnCgA= const csMsg = await openpgp.message.readArmored(signed.data); return openpgp.verify({ publicKeys:[pubKey], message:csMsg }); - }).then(function(cleartextSig) { + }).then(async function(cleartextSig) { expect(cleartextSig).to.exist; expect(cleartextSig.data).to.deep.equal(plaintext); expect(cleartextSig.signatures).to.have.length(1); diff --git a/test/general/util.js b/test/general/util.js index a611590a..67e36647 100644 --- a/test/general/util.js +++ b/test/general/util.js @@ -116,7 +116,7 @@ describe('Util unit tests', function() { }); }); - describe('getTransferables', function() { + describe('prepareBuffers', function() { let zero_copyVal; const buf1 = new Uint8Array(1); const buf2 = new Uint8Array(1); @@ -137,18 +137,18 @@ describe('Util unit tests', function() { openpgp.config.zero_copy = zero_copyVal; }); - it('should return undefined when zero_copy is false', function() { + it('should return undefined when zero_copy is false', async function() { openpgp.config.zero_copy = false; - expect(openpgp.util.getTransferables(obj)).to.be.undefined; + expect(await openpgp.util.prepareBuffers(obj)).to.be.undefined; }); - it('should return undefined for no input', function() { - expect(openpgp.util.getTransferables()).to.be.undefined; + it('should return undefined for no input', async function() { + expect(await openpgp.util.prepareBuffers()).to.be.undefined; }); - it('should return undefined for an empty oject', function() { - expect(openpgp.util.getTransferables({})).to.be.undefined; + it('should return undefined for an empty oject', async function() { + expect(await openpgp.util.prepareBuffers({})).to.be.undefined; }); - it('should return two buffers', function() { - expect(openpgp.util.getTransferables(obj)).to.deep.equal([buf1.buffer, buf2.buffer]); + it('should return two buffers', async function() { + expect(await openpgp.util.prepareBuffers(obj)).to.deep.equal([buf1.buffer, buf2.buffer]); }); });