From 8767ccda3bd48a3ca91426e8ef416b2676a160b3 Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Tue, 13 May 2014 13:13:36 +0200 Subject: [PATCH] [WO-383] decrypt pgp/inline --- src/js/dao/email-dao.js | 46 +++++++++- test/new-unit/email-dao-test.js | 145 +++++++++++++++++++++++++------- 2 files changed, 156 insertions(+), 35 deletions(-) diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index 2ee8152..4e6b116 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -371,8 +371,38 @@ define(function(require) { root = signedPart.content; } + // if the message is plain text and contains pgp/inline, we are only interested in the encrypted + // content, the rest (corporate mail footer, attachments, etc.) is discarded. + var body = _.pluck(self._emailSync.filterBodyParts(root, 'text'), 'content').join('\n'); + + /* + * here's how the regex works: + * - any content before the PGP block will be discarded + * - "-----BEGIN PGP MESSAGE-----" must be at the beginning (and end) of a line + * - "-----END PGP MESSAGE-----" must be at the beginning (and end) of a line + * - the regex must not match a pgp block in a plain text reply or forward of a pgp/inline message. + * (the encryption will break for replies/forward, because "> " corrupts the PGP block with non-radix-64 characters) + */ + var match = body.match(/^-{5}BEGIN PGP MESSAGE-{5}$[^>]*^-{5}END PGP MESSAGE-{5}$/im); + if (match) { + // show the plain text content + message.body = match[0]; + + // - replace the bodyParts info with an artificial bodyPart of type "encrypted" + // - _isPgpInline is only used internally to avoid trying to parse non-MIME text with the mailreader + // - set the encrypted flag so we can signal the ui that we're handling encrypted content + message.encrypted = true; + message.bodyParts = [{ + type: 'encrypted', + content: match[0], + _isPgpInline: true + }]; + done(); + return; + } + message.attachments = self._emailSync.filterBodyParts(root, 'attachment'); - message.body = _.pluck(self._emailSync.filterBodyParts(root, 'text'), 'content').join('\n'); + message.body = body; message.html = _.pluck(self._emailSync.filterBodyParts(root, 'html'), 'content').join('\n'); done(); @@ -412,14 +442,23 @@ define(function(require) { var encryptedNode = self._emailSync.filterBodyParts(message.bodyParts, 'encrypted')[0]; self._crypto.decrypt(encryptedNode.content, senderPublicKey.publicKey, function(err, decrypted) { if (err || !decrypted) { - showError(err.errMsg || err.message); + showError(err.errMsg || err.message || 'An error occurred during the decryption.'); + return; + } + + // if the encrypted node contains pgp/inline, we must not parse it + // with the mailreader as it is not well-formed MIME + if (encryptedNode._isPgpInline) { + message.body = decrypted; + message.decrypted = true; + done(); return; } // the mailparser works on the .raw property encryptedNode.raw = decrypted; - // parse the decrpyted raw content in the mailparser + // parse the decrypted raw content in the mailparser self._mailreader.parse({ bodyParts: [encryptedNode] }, function(err, parsedBodyParts) { @@ -440,7 +479,6 @@ define(function(require) { message.decrypted = true; - // we're done here! done(); }); diff --git a/test/new-unit/email-dao-test.js b/test/new-unit/email-dao-test.js index 6439815..39a1c30 100644 --- a/test/new-unit/email-dao-test.js +++ b/test/new-unit/email-dao-test.js @@ -21,6 +21,7 @@ define(function(require) { var emailAddress, passphrase, asymKeySize, mockkeyId, dummyEncryptedMail, dummyDecryptedMail, mockKeyPair, account, verificationMail, verificationUuid, corruptedVerificationMail, corruptedVerificationUuid, + localListStub, localStoreStub, imapGetStub, nonWhitelistedMail; beforeEach(function(done) { @@ -122,6 +123,10 @@ define(function(require) { dao = new EmailDAO(keychainStub, pgpStub, devicestorageStub, pgpBuilderStub, mailreader, emailSync); dao._account = account; + localListStub = sinon.stub(emailSync, '_localListMessages'); + localStoreStub = sinon.stub(emailSync, '_localStoreMessages'); + imapGetStub = sinon.stub(emailSync, '_getBodyParts'); + expect(dao._keychain).to.equal(keychainStub); expect(dao._crypto).to.equal(pgpStub); expect(dao._devicestorage).to.equal(devicestorageStub); @@ -606,8 +611,7 @@ define(function(require) { describe('getBody', function() { var folder = 'asdasdasdasdasd', - uid = 1234, - localListStub, localStoreStub, imapGetStub; + uid = 1234; it('should not do anything if the message already has content', function() { var message = { @@ -629,7 +633,7 @@ define(function(require) { uid: uid }; - localListStub = sinon.stub(emailSync, '_localListMessages').withArgs({ + localListStub.withArgs({ folder: folder, uid: uid }).yieldsAsync(null, [{ @@ -656,7 +660,7 @@ define(function(require) { expect(message.loadingBody).to.be.true; }); - it('should read an encrypted body from the device', function(done) { + it('should read a pgp/mime from the device', function(done) { var message, ct, pt; pt = 'bender is great!'; @@ -666,7 +670,7 @@ define(function(require) { encrypted: true }; - localListStub = sinon.stub(emailSync, '_localListMessages').withArgs({ + localListStub.withArgs({ folder: folder, uid: uid }).yieldsAsync(null, [{ @@ -697,6 +701,48 @@ define(function(require) { expect(message.loadingBody).to.be.true; }); + it('should read a pgp/inline from the device', function(done) { + var message, ct, pt; + + ct = '-----BEGIN PGP MESSAGE-----\nasdasdasd\n-----END PGP MESSAGE-----'; + pt = 'bla bla yadda yadda'; + message = { + uid: uid + }; + + localListStub.yieldsAsync(null, [{ + bodyParts: [{ + type: 'text', + content: pt + }, { + type: 'text', + content: ct + }, { + type: 'text', + content: pt + }] + }]); + + dao.getBody({ + message: message, + folder: folder + }, function(err, msg) { + expect(err).to.not.exist; + + expect(msg).to.equal(message); + expect(msg.body).to.equal(ct); + expect(msg.bodyParts[0].type).to.equal('encrypted'); + expect(msg.bodyParts[0].content).to.equal(ct); + expect(msg.encrypted).to.be.true; + expect(message.loadingBody).to.be.false; + + expect(localListStub.calledOnce).to.be.true; + + done(); + }); + expect(message.loadingBody).to.be.true; + }); + it('should stream from imap and set plain text body', function(done) { var message, body; @@ -710,17 +756,17 @@ define(function(require) { }] }; - localListStub = sinon.stub(emailSync, '_localListMessages').withArgs({ + localListStub.withArgs({ folder: folder, uid: uid }).yieldsAsync(null, [message]); - localStoreStub = sinon.stub(emailSync, '_localStoreMessages').withArgs({ + localStoreStub.withArgs({ folder: folder, emails: [message] }).yieldsAsync(); - imapGetStub = sinon.stub(emailSync, '_getBodyParts').withArgs({ + imapGetStub.withArgs({ folder: folder, uid: message.uid, bodyParts: message.bodyParts @@ -763,17 +809,17 @@ define(function(require) { }] }; - localListStub = sinon.stub(emailSync, '_localListMessages').withArgs({ + localListStub.withArgs({ folder: folder, uid: uid }).yieldsAsync(null, [message]); - localStoreStub = sinon.stub(emailSync, '_localStoreMessages').withArgs({ + localStoreStub.withArgs({ folder: folder, emails: [message] }).yieldsAsync(); - imapGetStub = sinon.stub(emailSync, '_getBodyParts').withArgs({ + imapGetStub.withArgs({ folder: folder, uid: message.uid, bodyParts: message.bodyParts @@ -814,9 +860,9 @@ define(function(require) { }] }; - localListStub = sinon.stub(emailSync, '_localListMessages').yieldsAsync(null, [message]); - localStoreStub = sinon.stub(emailSync, '_localStoreMessages').yieldsAsync({}); - imapGetStub = sinon.stub(emailSync, '_getBodyParts').yieldsAsync(null, [{ + localListStub.yieldsAsync(null, [message]); + localStoreStub.yieldsAsync({}); + imapGetStub.yieldsAsync(null, [{ type: 'text', content: 'bender is great! bender is great!' }]); @@ -845,9 +891,8 @@ define(function(require) { }] }; - localListStub = sinon.stub(emailSync, '_localListMessages').yields(null, [message]); - imapGetStub = sinon.stub(emailSync, '_getBodyParts').yieldsAsync({}); - localStoreStub = sinon.stub(emailSync, '_localStoreMessages'); + localListStub.yields(null, [message]); + imapGetStub.yieldsAsync({}); dao.getBody({ message: message, @@ -867,6 +912,16 @@ define(function(require) { }); describe('decryptBody', function() { + var parseStub; + + beforeEach(function() { + parseStub = sinon.stub(mailreader, 'parse'); + }); + + afterEach(function() { + mailreader.parse.restore(); + }); + it('should do nothing when the message is not encrypted', function() { var message = { encrypted: false, @@ -917,7 +972,7 @@ define(function(require) { }); it('decrypt a pgp/mime message', function(done) { - var message, ct, pt, parsed, parseStub; + var message, ct, pt, parsed; pt = 'bender is great'; ct = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'; @@ -934,10 +989,9 @@ define(function(require) { }] }; - keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); pgpStub.decrypt.withArgs(ct, mockKeyPair.publicKey.publicKey).yieldsAsync(null, pt); - parseStub = sinon.stub(mailreader, 'parse').withArgs({ + parseStub.withArgs({ bodyParts: [{ type: 'encrypted', content: ct, @@ -955,17 +1009,54 @@ define(function(require) { message: message }, function(error, msg) { expect(error).to.not.exist; - expect(msg).to.equal(message); expect(message.decrypted).to.be.true; expect(message.body).to.equal(parsed); expect(message.decryptingBody).to.be.false; - expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; expect(pgpStub.decrypt.calledOnce).to.be.true; expect(parseStub.calledOnce).to.be.true; - mailreader.parse.restore(); + done(); + }); + + expect(message.decryptingBody).to.be.true; + }); + + it('decrypt a pgp/inline message', function(done) { + var message, ct, pt; + + pt = 'bender is great'; + ct = '-----BEGIN PGP MESSAGE-----asdasdasd-----END PGP MESSAGE-----'; + message = { + from: [{ + address: 'asdasdasd' + }], + body: ct, + encrypted: true, + bodyParts: [{ + type: 'encrypted', + content: ct, + _isPgpInline: true + }] + }; + + keychainStub.getReceiverPublicKey.withArgs(message.from[0].address).yieldsAsync(null, mockKeyPair.publicKey); + pgpStub.decrypt.withArgs(ct, mockKeyPair.publicKey.publicKey).yieldsAsync(null, pt); + + dao.decryptBody({ + message: message + }, function(error, msg) { + expect(error).to.not.exist; + + expect(msg).to.equal(message); + expect(message.decrypted).to.be.true; + expect(message.body).to.equal(pt); + expect(message.decryptingBody).to.be.false; + expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; + expect(pgpStub.decrypt.calledOnce).to.be.true; + expect(parseStub.called).to.be.false; + done(); }); @@ -985,7 +1076,6 @@ define(function(require) { }] }; - var parseStub = sinon.spy(mailreader, 'parse'); keychainStub.getReceiverPublicKey.yields(null, mockKeyPair.publicKey); pgpStub.decrypt.yields({ errMsg: 'asd' @@ -998,12 +1088,10 @@ define(function(require) { expect(msg.body).to.equal('asd'); expect(msg).to.exist; expect(message.decryptingBody).to.be.false; - expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; expect(pgpStub.decrypt.calledOnce).to.be.true; expect(parseStub.called).to.be.false; - mailreader.parse.restore(); done(); }); }); @@ -1021,23 +1109,18 @@ define(function(require) { }] }; - var parseStub = sinon.spy(mailreader, 'parse'); keychainStub.getReceiverPublicKey.yields({}); dao.decryptBody({ message: message }, function(error, msg) { expect(error).to.exist; - expect(msg).to.not.exist; - expect(message.decryptingBody).to.be.false; - expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true; expect(pgpStub.decrypt.called).to.be.false; expect(parseStub.called).to.be.false; - mailreader.parse.restore(); done(); }); });