diff --git a/src/js/app-controller.js b/src/js/app-controller.js index 27cc70c..341888f 100644 --- a/src/js/app-controller.js +++ b/src/js/app-controller.js @@ -127,11 +127,6 @@ define(function(require) { console.log('IMAP reconnecting...'); // re-init client modules on error self.onConnect(function(err) { - if (!self._initialized) { - callback(err); - return; - } - if (err) { console.error('IMAP reconnect failed!', err); return; @@ -398,15 +393,7 @@ define(function(require) { return; } - // connect tcp clients on first startup - self.onConnect(function(err) { - if (err) { - callback(err); - return; - } - - callback(null, keypair); - }); + callback(null, keypair); }); } }; diff --git a/src/js/controller/login-initial.js b/src/js/controller/login-initial.js index 3c9c25c..72564dd 100644 --- a/src/js/controller/login-initial.js +++ b/src/js/controller/login-initial.js @@ -2,8 +2,7 @@ define(function(require) { 'use strict'; var appController = require('js/app-controller'), - errorUtil = require('js/util/error'), - dl = require('js/util/download'); + errorUtil = require('js/util/error'); var LoginInitialCtrl = function($scope, $location) { var emailDao = appController._emailDao, @@ -53,12 +52,13 @@ define(function(require) { return str.substring(0, 1).toLowerCase() + str.substring(1); } - if (!passphrase || passphrase.length < 10) { - $scope.passphraseMsg = 'Too short'; + if (!passphrase) { + // no rating for empty passphrase + $scope.passphraseMsg = ''; return; } - if (SAME.test(passphrase)) { + if (passphrase.length < 8 || SAME.test(passphrase)) { $scope.passphraseMsg = 'Very weak'; return; } @@ -85,14 +85,14 @@ define(function(require) { var passphrase = $scope.state.passphrase, confirmation = $scope.state.confirmation; - if (!passphrase || passphrase !== confirmation) { + if (passphrase !== confirmation) { return; } $scope.setState(states.PROCESSING); setTimeout(function() { emailDao.unlock({ - passphrase: passphrase + passphrase: (passphrase) ? passphrase : undefined }, function(err) { if (err) { $scope.setState(states.IDLE); @@ -100,42 +100,12 @@ define(function(require) { return; } - $scope.setState(states.DONE); + $location.path('/desktop'); $scope.$apply(); }); }, 500); }; - $scope.exportKeypair = function() { - // export keys from keychain - emailDao._crypto.exportKeys(function(err, keys) { - if (err) { - $scope.onError(err); - return; - } - - var id = keys.keyId.substring(8, keys.keyId.length); - dl.createDownload({ - content: keys.publicKeyArmored + keys.privateKeyArmored, - filename: 'whiteout_mail_' + emailDao._account.emailAddress + '_' + id + '.asc', - contentType: 'text/plain' - }, onSave); - }); - - function onSave(err) { - if (err) { - $scope.onError(err); - return; - } - $scope.proceed(); - $scope.$apply(); - } - }; - - $scope.proceed = function() { - $location.path('/desktop'); - }; - $scope.setState = function(state) { $scope.state.ui = state; }; diff --git a/src/js/controller/login-new-device.js b/src/js/controller/login-new-device.js index b1b59cc..9125dc5 100644 --- a/src/js/controller/login-new-device.js +++ b/src/js/controller/login-new-device.js @@ -17,11 +17,6 @@ define(function(require) { $scope.incorrect = false; $scope.confirmPassphrase = function() { - if (!$scope.passphrase) { - $scope.incorrect = true; - return; - } - $scope.incorrect = false; unlockCrypto(); }; diff --git a/src/js/controller/login.js b/src/js/controller/login.js index 8baeb04..ba22ba1 100644 --- a/src/js/controller/login.js +++ b/src/js/controller/login.js @@ -35,7 +35,7 @@ define(function(require) { // check if account needs to be selected if (!emailAddress) { - firstLogin(); + goTo('/add-account'); return; } @@ -53,23 +53,32 @@ define(function(require) { }); } - function firstLogin() { - $location.path('/add-account'); - $scope.$apply(); - } - function redirect(availableKeys) { // redirect if needed if (typeof availableKeys === 'undefined') { // no public key available, start onboarding process - $location.path('/login-initial'); + goTo('/login-initial'); } else if (!availableKeys.privateKey) { // no private key, import key - $location.path('/login-new-device'); + goTo('/login-new-device'); } else { - // public and private key available, just login - $location.path('/login-existing'); + // public and private key available, try empty passphrase + appController._emailDao.unlock({ + keypair: availableKeys, + passphrase: undefined + }, function(err) { + if (err) { + goTo('/login-existing'); + return; + } + + goTo('/desktop'); + }); } + } + + function goTo(location) { + $location.path(location); $scope.$apply(); } }; diff --git a/src/js/controller/navigation.js b/src/js/controller/navigation.js index 73f3c5e..1deb6fb 100644 --- a/src/js/controller/navigation.js +++ b/src/js/controller/navigation.js @@ -19,9 +19,6 @@ define(function(require) { // attach global error handler errorUtil.attachHandler($scope); - // app controller is initialized - appController._initialized = true; - emailDao = appController._emailDao; outboxBo = appController._outboxBo; @@ -69,7 +66,21 @@ define(function(require) { // init folders initFolders(); // select inbox as the current folder on init - $scope.openFolder($scope.account.folders[0]); + if ($scope.account.folders && $scope.account.folders.length > 0) { + $scope.openFolder($scope.account.folders[0]); + } + // connect imap/smtp clients on first startup + appController.onConnect(function(err) { + if (err) { + $scope.onError(err); + return; + } + + // select inbox if not yet selected + if (!$scope.state.nav.currentFolder) { + $scope.openFolder($scope.account.folders[0]); + } + }); // // helper functions diff --git a/src/js/crypto/pgp.js b/src/js/crypto/pgp.js index 5fea558..ab53d6f 100644 --- a/src/js/crypto/pgp.js +++ b/src/js/crypto/pgp.js @@ -19,7 +19,7 @@ define(function(require) { PGP.prototype.generateKeys = function(options, callback) { var userId; - if (!util.emailRegEx.test(options.emailAddress) || !options.keySize || typeof options.passphrase !== 'string') { + if (!util.emailRegEx.test(options.emailAddress) || !options.keySize) { callback({ errMsg: 'Crypto init failed. Not all options set!' }); @@ -119,7 +119,7 @@ define(function(require) { var pubKeyId, privKeyId, self = this; // check options - if (typeof options.passphrase !== 'string' || !options.privateKeyArmored || !options.publicKeyArmored) { + if (!options.privateKeyArmored || !options.publicKeyArmored) { callback({ errMsg: 'Importing keys failed. Not all options set!' }); @@ -184,6 +184,55 @@ define(function(require) { }); }; + /** + * Change the passphrase of an ascii armored private key. + */ + PGP.prototype.changePassphrase = function(options, callback) { + var privKey, packets; + + if (!options.privateKeyArmored || + typeof options.oldPassphrase !== 'string' || + typeof options.newPassphrase !== 'string') { + callback({ + errMsg: 'Could not export keys!' + }); + return; + } + + // read armored key + try { + privKey = openpgp.key.readArmored(options.privateKeyArmored).keys[0]; + } catch (e) { + callback({ + errMsg: 'Importing key failed. Parsing error!' + }); + return; + } + + // decrypt private key with passphrase + if (!privKey.decrypt(options.oldPassphrase)) { + callback({ + errMsg: 'Old passphrase incorrect!' + }); + return; + } + + // encrypt key with new passphrase + try { + packets = privKey.getAllKeyPackets(); + for (var i = 0; i < packets.length; i++) { + packets[i].encrypt(options.newPassphrase); + } + } catch (e) { + callback({ + errMsg: 'Setting new passphrase failed!' + }); + return; + } + + callback(null, privKey.armor()); + }; + /** * Encrypt and sign a pgp message for a list of receivers */ diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index ce706fa..6704f0b 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -74,6 +74,7 @@ define(function(require) { } self._account.folders = folders; + callback(null, keypair); }); } @@ -114,13 +115,6 @@ define(function(require) { // set status to online self._account.online = true; - // check memory - if (self._account.folders) { - // no need to init folder again on connect... already in memory - callback(); - return; - } - // init folders self._imapListFolders(function(err, folders) { if (err) { diff --git a/src/tpl/login-initial.html b/src/tpl/login-initial.html index bf60e8b..2efa93a 100644 --- a/src/tpl/login-initial.html +++ b/src/tpl/login-initial.html @@ -6,28 +6,22 @@
-

Generate PGP key. Choose a passphrase to protect your new key. If you forget it at a later time you will not be able to read past messages.

Alternatively you can also import an existing PGP key.

+

Generate PGP key. You can set a passphrase to protect your key on disk. This must be entered everytime you start the app. For no passphrase just press continue.

Alternatively you can also import an existing PGP key.


- - + +
- +
-

Generating keypair. Please stand by. This can take a while...

-
- -
-

Keypair generated. Your personal keypair has been generated. You can export it (e.g. to a USB flash drive) to setup Whiteout Mail on another computer or as a backup.

- - +

Generating key. Please stand by. This can take a while...

@@ -43,9 +37,8 @@
What is this?
-

A passphrase is like a password but longer.

+

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.

-

It must be at least 10 characters long and contain one special character or digit.

You cannot change your passphrase at a later time.

\ No newline at end of file diff --git a/test/new-unit/app-controller-test.js b/test/new-unit/app-controller-test.js index 5a4f05f..b3944cb 100644 --- a/test/new-unit/app-controller-test.js +++ b/test/new-unit/app-controller-test.js @@ -291,32 +291,11 @@ define(function(require) { }); }); - it('should fail due to error in onConnect', function(done) { - devicestorageStub.init.yields(); - updateHandlerStub.update.yields(); - emailDaoStub.init.yields(); - - onConnectStub.yields({}); - - controller.init({ - emailAddress: emailAddress - }, function(err) { - expect(err).to.exist; - expect(updateHandlerStub.update.calledOnce).to.be.true; - expect(emailDaoStub.init.calledOnce).to.be.true; - expect(devicestorageStub.init.calledOnce).to.be.true; - expect(onConnectStub.calledOnce).to.be.true; - done(); - }); - }); - it('should work and return a keypair', function(done) { devicestorageStub.init.withArgs(emailAddress).yields(); emailDaoStub.init.yields(null, {}); updateHandlerStub.update.yields(); - onConnectStub.yields(); - controller.init({ emailAddress: emailAddress }, function(err, keypair) { @@ -325,7 +304,6 @@ define(function(require) { expect(updateHandlerStub.update.calledOnce).to.be.true; expect(emailDaoStub.init.calledOnce).to.be.true; expect(devicestorageStub.init.calledOnce).to.be.true; - expect(onConnectStub.calledOnce).to.be.true; done(); }); }); diff --git a/test/new-unit/email-dao-test.js b/test/new-unit/email-dao-test.js index a426ba4..56eeca8 100644 --- a/test/new-unit/email-dao-test.js +++ b/test/new-unit/email-dao-test.js @@ -133,14 +133,18 @@ define(function(require) { dao._account.folders = []; imapClientStub.login.yields(); + var listFolderStub = sinon.stub(dao, '_imapListFolders').yields(null, []); + dao.onConnect({ imapClient: imapClientStub, pgpMailer: pgpMailerStub }, function(err) { expect(err).to.not.exist; expect(dao._account.online).to.be.true; + expect(dao._account.folders).to.deep.equal([]); expect(dao._imapClient).to.equal(dao._imapClient); expect(dao._smtpClient).to.equal(dao._smtpClient); + listFolderStub.restore(); done(); }); }); @@ -306,23 +310,7 @@ define(function(require) { }); }); - it('should work when folder already initiated', function(done) { - dao._account.folders = []; - imapLoginStub.yields(); - - dao.onConnect({ - imapClient: imapClientStub, - pgpMailer: pgpMailerStub - }, function(err) { - expect(err).to.not.exist; - expect(dao._account.online).to.be.true; - expect(dao._imapClient).to.equal(dao._imapClient); - expect(dao._smtpClient).to.equal(dao._smtpClient); - done(); - }); - }); - - it('should work when folder not yet initiated', function(done) { + it('should work', function(done) { var folders = []; imapLoginStub.yields(); imapListFoldersStub.yields(null, folders); diff --git a/test/new-unit/login-ctrl-test.js b/test/new-unit/login-ctrl-test.js index 357993e..7c858f7 100644 --- a/test/new-unit/login-ctrl-test.js +++ b/test/new-unit/login-ctrl-test.js @@ -60,13 +60,55 @@ define(function(require) { initStub.restore(); }); - it('should forward to existing user login', function(done) { - startAppStub.yields(); - getEmailAddressStub.yields(null, emailAddress); - initStub.yields(null, { + it('should forward directly to desktop for empty passphrase', function(done) { + var testKeys = { privateKey: 'a', publicKey: 'b' + }; + + startAppStub.yields(); + getEmailAddressStub.yields(null, emailAddress); + initStub.yields(null, testKeys); + + emailDaoMock.unlock.withArgs({ + keypair: testKeys, + passphrase: undefined + }).yields(); + + angular.module('logintest', []); + mocks.module('logintest'); + mocks.inject(function($controller, $rootScope, $location) { + location = $location; + sinon.stub(location, 'path', function(path) { + expect(path).to.equal('/desktop'); + expect(startAppStub.calledOnce).to.be.true; + expect(checkForUpdateStub.calledOnce).to.be.true; + expect(getEmailAddressStub.calledOnce).to.be.true; + done(); + }); + scope = $rootScope.$new(); + scope.state = {}; + ctrl = $controller(LoginCtrl, { + $location: location, + $scope: scope + }); }); + }); + + it('should forward to existing user login', function(done) { + var testKeys = { + privateKey: 'a', + publicKey: 'b' + }; + + startAppStub.yields(); + getEmailAddressStub.yields(null, emailAddress); + initStub.yields(null, testKeys); + + emailDaoMock.unlock.withArgs({ + keypair: testKeys, + passphrase: undefined + }).yields({}); angular.module('logintest', []); mocks.module('logintest'); diff --git a/test/new-unit/login-initial-ctrl-test.js b/test/new-unit/login-initial-ctrl-test.js index a424376..47b807b 100644 --- a/test/new-unit/login-initial-ctrl-test.js +++ b/test/new-unit/login-initial-ctrl-test.js @@ -5,7 +5,6 @@ define(function(require) { angular = require('angular'), mocks = require('angularMocks'), LoginInitialCtrl = require('js/controller/login-initial'), - dl = require('js/util/download'), PGP = require('js/crypto/pgp'), EmailDAO = require('js/dao/email-dao'), appController = require('js/app-controller'); @@ -54,8 +53,6 @@ define(function(require) { describe('initial state', function() { it('should be well defined', function() { - expect(scope.proceed).to.exist; - expect(scope.exportKeypair).to.exist; expect(scope.confirmPassphrase).to.exist; expect(scope.state.ui).to.equal(1); }); @@ -63,10 +60,10 @@ define(function(require) { describe('check passphrase quality', function() { it('should be too short', function() { - scope.state.passphrase = '&§DG36abc'; + scope.state.passphrase = '&§DG36'; scope.checkPassphraseQuality(); - expect(scope.passphraseMsg).to.equal('Too short'); + expect(scope.passphraseMsg).to.equal('Very weak'); expect(scope.passphraseRating).to.equal(0); }); @@ -112,16 +109,12 @@ define(function(require) { emailDaoMock.unlock.withArgs({ passphrase: passphrase }).yields(); - setStateStub = sinon.stub(scope, 'setState', function(state) { - if (setStateStub.calledOnce) { - expect(state).to.equal(2); - } else if (setStateStub.calledTwice) { - expect(state).to.equal(4); - expect(emailDaoMock.unlock.calledOnce).to.be.true; - scope.setState.restore(); - done(); - } - }); + + scope.$apply = function() { + expect(location.$$path).to.equal('/desktop'); + expect(emailDaoMock.unlock.calledOnce).to.be.true; + done(); + }; scope.confirmPassphrase(); }); @@ -139,6 +132,7 @@ define(function(require) { emailDaoMock.unlock.withArgs({ passphrase: passphrase }).yields(new Error('asd')); + setStateStub = sinon.stub(scope, 'setState', function(state) { if (setStateStub.calledOnce) { expect(state).to.equal(2); @@ -154,64 +148,5 @@ define(function(require) { }); }); - describe('proceed', function() { - it('should forward', function() { - var locationSpy = sinon.spy(location, 'path'); - - scope.proceed(); - - expect(locationSpy.calledWith('/desktop')).to.be.true; - }); - }); - - describe('export keypair', function() { - it('should work', function() { - var locationSpy, createDownloadMock; - - createDownloadMock = sinon.stub(dl, 'createDownload'); - cryptoMock.exportKeys.yields(null, { - publicKeyArmored: 'a', - privateKeyArmored: 'b', - keyId: keyId - }); - createDownloadMock.withArgs(sinon.match(function(arg) { - return arg.content === 'ab' && arg.filename === 'whiteout_mail_' + emailAddress + '_' + expectedKeyId + '.asc' && arg.contentType === 'text/plain'; - })).yields(); - - locationSpy = sinon.spy(location, 'path'); - - scope.exportKeypair(); - - expect(cryptoMock.exportKeys.calledOnce).to.be.true; - expect(createDownloadMock.calledOnce).to.be.true; - expect(locationSpy.calledWith('/desktop')).to.be.true; - dl.createDownload.restore(); - }); - - it('should not work when download fails', function() { - var createDownloadMock = sinon.stub(dl, 'createDownload'); - cryptoMock.exportKeys.yields(null, { - publicKeyArmored: 'a', - privateKeyArmored: 'b', - keyId: keyId - }); - createDownloadMock.yields({ - errMsg: 'snafu.' - }); - scope.exportKeypair(); - - expect(cryptoMock.exportKeys.calledOnce).to.be.true; - expect(createDownloadMock.calledOnce).to.be.true; - dl.createDownload.restore(); - }); - - it('should not work when export fails', function() { - cryptoMock.exportKeys.yields(new Error('snafu.')); - - scope.exportKeypair(); - - expect(cryptoMock.exportKeys.calledOnce).to.be.true; - }); - }); }); }); \ No newline at end of file diff --git a/test/new-unit/login-new-device-ctrl-test.js b/test/new-unit/login-new-device-ctrl-test.js index 52f2672..81985dd 100644 --- a/test/new-unit/login-new-device-ctrl-test.js +++ b/test/new-unit/login-new-device-ctrl-test.js @@ -98,14 +98,6 @@ define(function(require) { expect(keychainMock.getUserKeyPair.calledOnce).to.be.true; }); - it('should not do anything without passphrase', function() { - scope.state.passphrase = ''; - - scope.confirmPassphrase(); - - expect(scope.incorrect).to.be.true; - }); - it('should not work when keypair upload fails', function() { scope.passphrase = passphrase; scope.key = { diff --git a/test/new-unit/pgp-test.js b/test/new-unit/pgp-test.js index bcfecd8..ecb95e9 100644 --- a/test/new-unit/pgp-test.js +++ b/test/new-unit/pgp-test.js @@ -120,6 +120,28 @@ define(function(require) { }); }); + describe('Change passphrase of private key', function() { + it('should work', function(done) { + pgp.changePassphrase({ + privateKeyArmored: privkey, + oldPassphrase: passphrase, + newPassphrase: 'yxcv' + }, function(err, reEncryptedKey) { + expect(err).to.not.exist; + expect(reEncryptedKey).to.exist; + + pgp.importKeys({ + passphrase: 'yxcv', + privateKeyArmored: reEncryptedKey, + publicKeyArmored: pubkey + }, function(err) { + expect(err).to.not.exist; + done(); + }); + }); + }); + }); + describe('Encrypt/Sign/Decrypt/Verify', function() { var message = 'asdfs\n\nThursday, Nov 21, 2013 7:38 PM asdf@example.com wrote:\n' + '> asdf\n' +