diff --git a/src/js/app.js b/src/js/app.js index 89f6246..656bfdd 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -7,6 +7,7 @@ requirejs([ 'js/controller/popover', 'js/controller/add-account', 'js/controller/account', + 'js/controller/set-passphrase', 'js/controller/contacts', 'js/controller/login', 'js/controller/login-initial', @@ -26,6 +27,7 @@ requirejs([ PopoverCtrl, AddAccountCtrl, AccountCtrl, + SetPassphraseCtrl, ContactsCtrl, LoginCtrl, LoginInitialCtrl, @@ -92,6 +94,7 @@ requirejs([ app.controller('WriteCtrl', WriteCtrl); app.controller('MailListCtrl', MailListCtrl); app.controller('AccountCtrl', AccountCtrl); + app.controller('SetPassphraseCtrl', SetPassphraseCtrl); app.controller('ContactsCtrl', ContactsCtrl); app.controller('DialogCtrl', DialogCtrl); app.controller('PopoverCtrl', PopoverCtrl); diff --git a/src/js/controller/account.js b/src/js/controller/account.js index 5a9d13c..5e0a5c5 100644 --- a/src/js/controller/account.js +++ b/src/js/controller/account.js @@ -3,14 +3,16 @@ define(function(require) { var appController = require('js/app-controller'), dl = require('js/util/download'), - emailDao; + pgp, keychain, userId; // // Controller // var AccountCtrl = function($scope) { - emailDao = appController._emailDao; + userId = appController._emailDao._account.emailAddress; + keychain = appController._keychain; + pgp = appController._crypto; $scope.state.account = { open: false, @@ -23,32 +25,49 @@ define(function(require) { // scope variables // - var fpr = emailDao._crypto.getFingerprint(), - keyId = emailDao._crypto.getKeyId(); - $scope.eMail = emailDao._account.emailAddress; - $scope.keyId = keyId.slice(8); + var keyParams = pgp.getKeyParams(); + + $scope.eMail = userId; + $scope.keyId = keyParams._id.slice(8); + var fpr = keyParams.fingerprint; $scope.fingerprint = fpr.slice(0, 4) + ' ' + fpr.slice(4, 8) + ' ' + fpr.slice(8, 12) + ' ' + fpr.slice(12, 16) + ' ' + fpr.slice(16, 20) + ' ' + fpr.slice(20, 24) + ' ' + fpr.slice(24, 28) + ' ' + fpr.slice(28, 32) + ' ' + fpr.slice(32, 36) + ' ' + fpr.slice(36); - $scope.keysize = emailDao._account.asymKeySize; + $scope.keysize = keyParams.bitSize; // // scope functions // $scope.exportKeyFile = function() { - emailDao._crypto.exportKeys(function(err, keys) { + keychain.getUserKeyPair(userId, function(err, keys) { if (err) { $scope.onError(err); return; } - var id = 'whiteout_mail_' + emailDao._account.emailAddress + '_' + keys.keyId.substring(8, keys.keyId.length); + var keyId = keys.publicKey._id; + var file = 'whiteout_mail_' + userId + '_' + keyId.substring(8, keyId.length); + dl.createDownload({ - content: keys.publicKeyArmored + keys.privateKeyArmored, - filename: id + '.asc', + content: keys.publicKey.publicKey + keys.privateKey.encryptedKey, + filename: file + '.asc', contentType: 'text/plain' - }, $scope.onError); + }, onExport); }); }; + + function onExport(err) { + if (err) { + $scope.onError(err); + return; + } + + $scope.state.account.toggle(false); + $scope.$apply(); + $scope.onError({ + title: 'Success', + message: 'Exported keypair to file.' + }); + } }; return AccountCtrl; diff --git a/src/js/controller/set-passphrase.js b/src/js/controller/set-passphrase.js new file mode 100644 index 0000000..a280135 --- /dev/null +++ b/src/js/controller/set-passphrase.js @@ -0,0 +1,83 @@ +define(function(require) { + 'use strict'; + + var appController = require('js/app-controller'), + pgp, keychain; + + // + // Controller + // + + var SetPassphraseCtrl = function($scope) { + keychain = appController._keychain; + pgp = appController._crypto; + + $scope.state.setPassphrase = { + open: false, + toggle: function(to) { + this.open = to; + + $scope.newPassphrase = undefined; + $scope.oldPassphrase = undefined; + $scope.confirmation = undefined; + } + }; + + // + // scope variables + // + + // + // scope functions + // + + $scope.setPassphrase = function() { + var keyId = pgp.getKeyParams()._id; + keychain.lookupPrivateKey(keyId, function(err, savedKey) { + if (err) { + $scope.onError(err); + return; + } + + pgp.changePassphrase({ + privateKeyArmored: savedKey.encryptedKey, + oldPassphrase: $scope.oldPassphrase, + newPassphrase: $scope.newPassphrase + }, onPassphraseChanged); + }); + }; + + function onPassphraseChanged(err, newPrivateKeyArmored) { + if (err) { + $scope.onError(err); + return; + } + + // persist new armored key + var keyParams = pgp.getKeyParams(newPrivateKeyArmored); + var privateKey = { + _id: keyParams._id, + userId: keyParams.userId, + encryptedKey: newPrivateKeyArmored + }; + + keychain.saveLocalPrivateKey(privateKey, onKeyPersisted); + } + + function onKeyPersisted(err) { + if (err) { + $scope.onError(err); + return; + } + + $scope.state.setPassphrase.toggle(false); + $scope.$apply(); + $scope.onError({ + title: 'Success', + message: 'Passphrase change complete.' + }); + } + }; + + return SetPassphraseCtrl; +}); \ No newline at end of file diff --git a/src/js/crypto/pgp.js b/src/js/crypto/pgp.js index ab53d6f..649e0b3 100644 --- a/src/js/crypto/pgp.js +++ b/src/js/crypto/pgp.js @@ -17,7 +17,7 @@ define(function(require) { * Generate a key pair for the user */ PGP.prototype.generateKeys = function(options, callback) { - var userId; + var userId, passphrase; if (!util.emailRegEx.test(options.emailAddress) || !options.keySize) { callback({ @@ -28,7 +28,8 @@ define(function(require) { // generate keypair (keytype 1=RSA) userId = 'Whiteout User <' + options.emailAddress + '>'; - openpgp.generateKeyPair(1, options.keySize, userId, options.passphrase, onGenerated); + passphrase = (options.passphrase) ? options.passphrase : undefined; + openpgp.generateKeyPair(1, options.keySize, userId, passphrase, onGenerated); function onGenerated(err, keys) { if (err) { @@ -99,8 +100,18 @@ define(function(require) { * Read all relevant params of an armored key. */ PGP.prototype.getKeyParams = function(keyArmored) { - var key = openpgp.key.readArmored(keyArmored).keys[0], - packet = key.getKeyPacket(); + var key, packet; + + // process armored key input + if (keyArmored) { + key = openpgp.key.readArmored(keyArmored).keys[0]; + } else if (this._publicKey) { + key = this._publicKey; + } else { + throw new Error('Cannot read key params... keys not set!'); + } + + packet = key.getKeyPacket(); return { _id: packet.getKeyId().toHex().toUpperCase(), @@ -188,17 +199,24 @@ define(function(require) { * Change the passphrase of an ascii armored private key. */ PGP.prototype.changePassphrase = function(options, callback) { - var privKey, packets; + var privKey, packets, newPassphrase, newKeyArmored; - if (!options.privateKeyArmored || - typeof options.oldPassphrase !== 'string' || - typeof options.newPassphrase !== 'string') { + // set undefined instead of empty string as passphrase + newPassphrase = (options.newPassphrase) ? options.newPassphrase : undefined; + + if (!options.privateKeyArmored) { callback({ - errMsg: 'Could not export keys!' + errMsg: 'Private key must be specified to change passphrase!' }); return; } + if (options.oldPassphrase === newPassphrase || + (!options.oldPassphrase && !newPassphrase)) { + callback(new Error('New and old passphrase are the same!')); + return; + } + // read armored key try { privKey = openpgp.key.readArmored(options.privateKeyArmored).keys[0]; @@ -221,8 +239,9 @@ define(function(require) { try { packets = privKey.getAllKeyPackets(); for (var i = 0; i < packets.length; i++) { - packets[i].encrypt(options.newPassphrase); + packets[i].encrypt(newPassphrase); } + newKeyArmored = privKey.armor(); } catch (e) { callback({ errMsg: 'Setting new passphrase failed!' @@ -230,7 +249,15 @@ define(function(require) { return; } - callback(null, privKey.armor()); + // check if new passphrase really works + if (!privKey.decrypt(newPassphrase)) { + callback({ + errMsg: 'Decrypting key with new passphrase failed!' + }); + return; + } + + callback(null, newKeyArmored); }; /** diff --git a/src/js/util/error.js b/src/js/util/error.js index 025c4e3..05749a6 100644 --- a/src/js/util/error.js +++ b/src/js/util/error.js @@ -10,7 +10,11 @@ define(function() { return; } - console.error(options); + if (options.stack) { + console.error(options.stack); + } else { + console.error(options); + } scope.state.dialog = { open: true, diff --git a/src/sass/all.scss b/src/sass/all.scss index cb33f9d..5f94d17 100755 --- a/src/sass/all.scss +++ b/src/sass/all.scss @@ -15,6 +15,7 @@ @import "components/icons"; @import "components/lightbox"; @import "components/nav"; +@import "components/dialog"; @import "components/mail-list"; @import "components/layout"; @import "components/popover"; @@ -24,6 +25,7 @@ @import "views/shared"; @import "views/add-account"; @import "views/account"; +@import "views/set-passphrase"; @import "views/contacts"; @import "views/dialog"; @import "views/navigation"; diff --git a/src/sass/components/_dialog.scss b/src/sass/components/_dialog.scss new file mode 100644 index 0000000..2794a6d --- /dev/null +++ b/src/sass/components/_dialog.scss @@ -0,0 +1,17 @@ +.dialog { + padding: 0px; + color: $color-grey-dark; + + @include respond-to(mobile) { + top: 0; + max-width: 100%; + } + + .control { + float: right; + + button { + border: 0!important; + } + } +} \ No newline at end of file diff --git a/src/sass/views/_account.scss b/src/sass/views/_account.scss index 17b115f..5466502 100644 --- a/src/sass/views/_account.scss +++ b/src/sass/views/_account.scss @@ -1,13 +1,7 @@ .view-account { - padding: 0px; - color: $color-grey-dark; - - @include respond-to(mobile) { - height: 100%; - } table { - margin: 50px auto 100px auto; + margin: 50px auto 60px auto; td { padding-top: 15px; @@ -20,13 +14,4 @@ } } - button { - border: 0!important; - } - - .export-control { - position: absolute; - bottom: 15px; - right: 15px; - } } \ No newline at end of file diff --git a/src/sass/views/_dialog.scss b/src/sass/views/_dialog.scss index 2ec0daf..49a66aa 100644 --- a/src/sass/views/_dialog.scss +++ b/src/sass/views/_dialog.scss @@ -1,29 +1,17 @@ .view-dialog { - padding: 0px; - color: $color-grey-dark; max-width: 350px; height: auto; top: 30%; - @include respond-to(mobile) { - top: 0; - max-width: 100%; - } - p { text-align: center; max-width: 80%; - margin: 30px auto 70px auto; + margin: 30px auto; } .control { - position: absolute; - bottom: $lightbox-padding; - right: $lightbox-padding; - button { width: 100px; - border: 0!important; } } } \ No newline at end of file diff --git a/src/sass/views/_set-passphrase.scss b/src/sass/views/_set-passphrase.scss new file mode 100644 index 0000000..33d19a0 --- /dev/null +++ b/src/sass/views/_set-passphrase.scss @@ -0,0 +1,25 @@ +.view-set-passphrase { + + .inputs { + margin: 40px 60px 30px; + + div { + margin: 5px 0; + } + } + + table { + margin: 50px auto 60px auto; + + td { + padding-top: 15px; + + &:first-child { + text-align: right; + padding-right: 15px; + font-weight: bold; + } + } + } + +} \ No newline at end of file diff --git a/src/tpl/account.html b/src/tpl/account.html index 3c805ba..90c5491 100644 --- a/src/tpl/account.html +++ b/src/tpl/account.html @@ -5,7 +5,8 @@
-
+
\ No newline at end of file diff --git a/src/tpl/desktop.html b/src/tpl/desktop.html index 1573e6b..5a6b0e4 100644 --- a/src/tpl/desktop.html +++ b/src/tpl/desktop.html @@ -25,9 +25,12 @@ + \ No newline at end of file diff --git a/src/tpl/login-initial.html b/src/tpl/login-initial.html index 2efa93a..23f73df 100644 --- a/src/tpl/login-initial.html +++ b/src/tpl/login-initial.html @@ -39,6 +39,5 @@

A passphrase is like a password that protects your PGP key.

If your device is lost or stolen the passphrase protects the contents of your mailbox.

-

You cannot change your passphrase at a later time.

\ No newline at end of file diff --git a/src/tpl/login-new-device.html b/src/tpl/login-new-device.html index 2f68c87..56834ca 100644 --- a/src/tpl/login-new-device.html +++ b/src/tpl/login-new-device.html @@ -40,7 +40,7 @@
What is this?
-

The passphrase protects your encrypted mailbox.

+

A passphrase is like a password that protects your PGP key.

There is no way to access your messages without your passphrase.

If you have forgotten your passphrase, please request an account reset by sending an email to support@whiteout.io. You will not be able to read previous messages after a reset.

diff --git a/src/tpl/set-passphrase.html b/src/tpl/set-passphrase.html new file mode 100644 index 0000000..e72d573 --- /dev/null +++ b/src/tpl/set-passphrase.html @@ -0,0 +1,33 @@ + \ No newline at end of file diff --git a/test/new-unit/account-ctrl-test.js b/test/new-unit/account-ctrl-test.js index b0e4d99..d31d021 100644 --- a/test/new-unit/account-ctrl-test.js +++ b/test/new-unit/account-ctrl-test.js @@ -5,23 +5,20 @@ define(function(require) { angular = require('angular'), mocks = require('angularMocks'), AccountCtrl = require('js/controller/account'), - EmailDAO = require('js/dao/email-dao'), PGP = require('js/crypto/pgp'), dl = require('js/util/download'), - appController = require('js/app-controller'); + appController = require('js/app-controller'), + KeychainDAO = require('js/dao/keychain-dao'); describe('Account Controller unit test', function() { - var scope, accountCtrl, origEmailDao, emailDaoMock, + var scope, accountCtrl, dummyFingerprint, expectedFingerprint, dummyKeyId, expectedKeyId, - emailAddress, - keySize, - cryptoMock; + emailAddress, keySize, cryptoMock, keychainMock; beforeEach(function() { - origEmailDao = appController._emailDao; - appController._emailDao = emailDaoMock = sinon.createStubInstance(EmailDAO); - emailDaoMock._crypto = cryptoMock = sinon.createStubInstance(PGP); + appController._crypto = cryptoMock = sinon.createStubInstance(PGP); + appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); dummyFingerprint = '3A2D39B4E1404190B8B949DE7D7E99036E712926'; expectedFingerprint = '3A2D 39B4 E140 4190 B8B9 49DE 7D7E 9903 6E71 2926'; @@ -31,10 +28,18 @@ define(function(require) { cryptoMock.getKeyId.returns(dummyKeyId); emailAddress = 'fred@foo.com'; keySize = 1234; - emailDaoMock._account = { - emailAddress: emailAddress, - asymKeySize: keySize + appController._emailDao = { + _account: { + emailAddress: emailAddress, + asymKeySize: keySize + } }; + cryptoMock.getKeyParams.returns({ + _id: dummyKeyId, + fingerprint: dummyFingerprint, + userId: emailAddress, + bitSize: keySize + }); angular.module('accounttest', []); mocks.module('accounttest'); @@ -47,10 +52,7 @@ define(function(require) { }); }); - afterEach(function() { - // restore the module - appController._emailDao = origEmailDao; - }); + afterEach(function() {}); describe('scope variables', function() { it('should be set correctly', function() { @@ -63,16 +65,22 @@ define(function(require) { describe('export to key file', function() { it('should work', function(done) { var createDownloadMock = sinon.stub(dl, 'createDownload'); - cryptoMock.exportKeys.yields(null, { - publicKeyArmored: 'a', - privateKeyArmored: 'b', - keyId: dummyKeyId + keychainMock.getUserKeyPair.withArgs(emailAddress).yields(null, { + publicKey: { + _id: dummyKeyId, + publicKey: 'a' + }, + privateKey: { + encryptedKey: 'b' + } }); createDownloadMock.withArgs(sinon.match(function(arg) { return arg.content === 'ab' && arg.filename === 'whiteout_mail_' + emailAddress + '_' + expectedKeyId + '.asc' && arg.contentType === 'text/plain'; })).yields(); - scope.onError = function() { - expect(cryptoMock.exportKeys.calledOnce).to.be.true; + scope.onError = function(err) { + expect(err.title).to.equal('Success'); + expect(scope.state.account.open).to.be.false; + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; expect(dl.createDownload.calledOnce).to.be.true; dl.createDownload.restore(); done(); @@ -82,9 +90,10 @@ define(function(require) { }); it('should not work when key export failed', function(done) { - cryptoMock.exportKeys.yields(new Error('asdasd')); - scope.onError = function() { - expect(cryptoMock.exportKeys.calledOnce).to.be.true; + keychainMock.getUserKeyPair.yields(new Error('Boom!')); + scope.onError = function(err) { + expect(err.message).to.equal('Boom!'); + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; done(); }; @@ -93,14 +102,19 @@ define(function(require) { it('should not work when create download failed', function(done) { var createDownloadMock = sinon.stub(dl, 'createDownload'); - cryptoMock.exportKeys.yields(null, { - publicKeyArmored: 'a', - privateKeyArmored: 'b', - keyId: dummyKeyId + keychainMock.getUserKeyPair.withArgs(emailAddress).yields(null, { + publicKey: { + _id: dummyKeyId, + publicKey: 'a' + }, + privateKey: { + encryptedKey: 'b' + } }); createDownloadMock.withArgs().yields(new Error('asdasd')); - scope.onError = function() { - expect(cryptoMock.exportKeys.calledOnce).to.be.true; + scope.onError = function(err) { + expect(err.message).to.equal('asdasd'); + expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; expect(dl.createDownload.calledOnce).to.be.true; dl.createDownload.restore(); done(); diff --git a/test/new-unit/main.js b/test/new-unit/main.js index c8d4cf9..de90449 100644 --- a/test/new-unit/main.js +++ b/test/new-unit/main.js @@ -42,6 +42,7 @@ function startTests() { 'test/new-unit/dialog-ctrl-test', 'test/new-unit/add-account-ctrl-test', 'test/new-unit/account-ctrl-test', + 'test/new-unit/set-passphrase-ctrl-test', 'test/new-unit/contacts-ctrl-test', 'test/new-unit/login-existing-ctrl-test', 'test/new-unit/login-initial-ctrl-test', diff --git a/test/new-unit/pgp-test.js b/test/new-unit/pgp-test.js index 279c935..3ef57d4 100644 --- a/test/new-unit/pgp-test.js +++ b/test/new-unit/pgp-test.js @@ -140,6 +140,30 @@ define(function(require) { }); }); }); + + it('should fail when passphrases are equal', function(done) { + pgp.changePassphrase({ + privateKeyArmored: privkey, + oldPassphrase: passphrase, + newPassphrase: passphrase + }, function(err, reEncryptedKey) { + expect(err).to.exist; + expect(reEncryptedKey).to.not.exist; + done(); + }); + }); + + it('should fail when old passphrase is incorrect', function(done) { + pgp.changePassphrase({ + privateKeyArmored: privkey, + oldPassphrase: 'asd', + newPassphrase: 'yxcv' + }, function(err, reEncryptedKey) { + expect(err).to.exist; + expect(reEncryptedKey).to.not.exist; + done(); + }); + }); }); describe('Encrypt/Sign/Decrypt/Verify', function() { @@ -193,6 +217,15 @@ define(function(require) { expect(params.userId).to.equal("whiteout.test@t-online.de"); expect(params.algorithm).to.equal("rsa_encrypt_sign"); }); + + it('should work without param', function() { + var params = pgp.getKeyParams(); + expect(params.fingerprint).to.equal('5856CEF789C3A307E8A1B976F6F60E9B42CDFF4C'); + expect(params._id).to.equal("F6F60E9B42CDFF4C"); + expect(params.bitSize).to.equal(keySize); + expect(params.userId).to.equal("whiteout.test@t-online.de"); + expect(params.algorithm).to.equal("rsa_encrypt_sign"); + }); }); describe('Encrypt and sign', function() { diff --git a/test/new-unit/set-passphrase-ctrl-test.js b/test/new-unit/set-passphrase-ctrl-test.js new file mode 100644 index 0000000..fdc1393 --- /dev/null +++ b/test/new-unit/set-passphrase-ctrl-test.js @@ -0,0 +1,81 @@ +define(function(require) { + 'use strict'; + + var expect = chai.expect, + angular = require('angular'), + mocks = require('angularMocks'), + SetPassphraseCtrl = require('js/controller/set-passphrase'), + PGP = require('js/crypto/pgp'), + appController = require('js/app-controller'), + KeychainDAO = require('js/dao/keychain-dao'); + + describe('Set Passphrase Controller unit test', function() { + var scope, setPassphraseCtrl, + dummyFingerprint, expectedFingerprint, + dummyKeyId, expectedKeyId, + emailAddress, keySize, cryptoMock, keychainMock; + + beforeEach(function() { + appController._crypto = cryptoMock = sinon.createStubInstance(PGP); + appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO); + + dummyFingerprint = '3A2D39B4E1404190B8B949DE7D7E99036E712926'; + expectedFingerprint = '3A2D 39B4 E140 4190 B8B9 49DE 7D7E 9903 6E71 2926'; + dummyKeyId = '9FEB47936E712926'; + expectedKeyId = '6E712926'; + cryptoMock.getFingerprint.returns(dummyFingerprint); + cryptoMock.getKeyId.returns(dummyKeyId); + emailAddress = 'fred@foo.com'; + keySize = 1234; + + cryptoMock.getKeyParams.returns({ + _id: dummyKeyId, + fingerprint: dummyFingerprint, + userId: emailAddress, + bitSize: keySize + }); + + angular.module('setpassphrasetest', []); + mocks.module('setpassphrasetest'); + mocks.inject(function($rootScope, $controller) { + scope = $rootScope.$new(); + scope.state = {}; + setPassphraseCtrl = $controller(SetPassphraseCtrl, { + $scope: scope + }); + }); + }); + + afterEach(function() {}); + + describe('setPassphrase', function() { + it('should work', function(done) { + scope.oldPassphrase = 'old'; + scope.newPassphrase = 'new'; + + keychainMock.lookupPrivateKey.withArgs(dummyKeyId).yields(null, { + encryptedKey: 'encrypted' + }); + + cryptoMock.changePassphrase.withArgs({ + privateKeyArmored: 'encrypted', + oldPassphrase: 'old', + newPassphrase: 'new' + }).yields(null, 'newArmoredKey'); + + keychainMock.saveLocalPrivateKey.withArgs({ + _id: dummyKeyId, + userId: emailAddress, + encryptedKey: 'newArmoredKey' + }).yields(); + + scope.onError = function(err) { + expect(err.title).to.equal('Success'); + done(); + }; + + scope.setPassphrase(); + }); + }); + }); +}); \ No newline at end of file