diff --git a/src/js/app-controller.js b/src/js/app-controller.js index 5e8c212..ccc3007 100644 --- a/src/js/app-controller.js +++ b/src/js/app-controller.js @@ -41,7 +41,7 @@ define(function(require) { /** * Request an OAuth token from chrome for gmail users */ - self.fetchOAuthToken = function(passphrase, callback) { + self.fetchOAuthToken = function(callback) { // get OAuth Token from chrome chrome.identity.getAuthToken({ 'interactive': true @@ -65,7 +65,10 @@ define(function(require) { } // init the email dao - self.init(emailAddress, passphrase, token, callback); + callback(null, { + emailAddress: emailAddress, + token: token + }); }); } ); @@ -124,7 +127,7 @@ define(function(require) { /** * Instanciate the mail email data access object and its dependencies. Login to imap on init. */ - self.init = function(userId, passphrase, token, callback) { + self.init = function(userId, token, callback) { var auth, imapOptions, smtpOptions, keychain, imapClient, smtpClient, pgp, userStorage; @@ -162,7 +165,7 @@ define(function(require) { emailAddress: userId, asymKeySize: config.asymKeySize }; - self._emailDao.init(account, passphrase, callback); + self._emailDao.init(account, callback); }; return self; diff --git a/src/js/app.js b/src/js/app.js index dec4562..b76a10f 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -4,13 +4,16 @@ window.name = 'NG_DEFER_BOOTSTRAP!'; require([ 'angular', 'js/controller/login', + 'js/controller/login-initial', + 'js/controller/login-new-device', + 'js/controller/login-existing', 'js/controller/mail-list', 'js/controller/read', 'js/controller/write', 'js/controller/navigation', 'angularRoute', 'angularTouch' -], function(angular, LoginCtrl, MailListCtrl, ReadCtrl, WriteCtrl, NavigationCtrl) { +], function(angular, LoginCtrl, LoginInitialCtrl, LoginNewDeviceCtrl, LoginExistingCtrl, MailListCtrl, ReadCtrl, WriteCtrl, NavigationCtrl) { 'use strict'; var app = angular.module('mail', ['ngRoute', 'ngTouch', 'navigation', 'mail-list', 'write', 'read']); @@ -18,9 +21,21 @@ require([ // set router paths app.config(function($routeProvider) { $routeProvider.when('/login', { - templateUrl: 'tpl/login.html', + templateUrl: 'tpl/loading.html', controller: LoginCtrl }); + $routeProvider.when('/login-existing', { + templateUrl: 'tpl/login-existing.html', + controller: LoginExistingCtrl + }); + $routeProvider.when('/login-initial', { + templateUrl: 'tpl/login-initial.html', + controller: LoginInitialCtrl + }); + $routeProvider.when('/login-new-device', { + templateUrl: 'tpl/login-new-device.html', + controller: LoginNewDeviceCtrl + }); $routeProvider.when('/desktop', { templateUrl: 'tpl/desktop.html', controller: NavigationCtrl diff --git a/src/js/controller/login-existing.js b/src/js/controller/login-existing.js new file mode 100644 index 0000000..e206448 --- /dev/null +++ b/src/js/controller/login-existing.js @@ -0,0 +1,53 @@ +define(function(require) { + 'use strict'; + + var appController = require('js/app-controller'); + + var LoginExistingCtrl = function($scope, $location) { + + $scope.confirmPassphrase = function() { + var passphrase = $scope.passphrase, + emailDao = appController._emailDao; + + if (!passphrase) { + return; + } + + unlockCrypto(imapLogin); + + function unlockCrypto(callback) { + var userId = emailDao._account.emailAddress; + emailDao._keychain.getUserKeyPair(userId, function(err, keypair) { + if (err) { + callback(err); + return; + } + emailDao.unlock(keypair, passphrase, callback); + }); + } + + function imapLogin(err) { + if (err) { + console.error(err); + return; + } + + // login to imap backend + appController._emailDao.imapLogin(function(err) { + if (err) { + console.error(err); + return; + } + onLogin(); + }); + } + }; + + function onLogin() { + $location.path('/desktop'); + $scope.$apply(); + } + }; + + return LoginExistingCtrl; +}); diff --git a/src/js/controller/login-initial.js b/src/js/controller/login-initial.js new file mode 100644 index 0000000..df640cf --- /dev/null +++ b/src/js/controller/login-initial.js @@ -0,0 +1,47 @@ +define(function(require) { + 'use strict'; + + var appController = require('js/app-controller'); + + var LoginInitialCtrl = function($scope, $location) { + + $scope.confirmPassphrase = function() { + var passphrase = $scope.passphrase, + confirmation = $scope.confirmation, + emailDao = appController._emailDao; + + if (!passphrase || passphrase !== confirmation) { + return; + } + + unlockCrypto(imapLogin); + + function unlockCrypto(callback) { + emailDao.unlock({}, passphrase, callback); + } + + function imapLogin(err) { + if (err) { + console.error(err); + return; + } + + // login to imap backend + appController._emailDao.imapLogin(function(err) { + if (err) { + console.error(err); + return; + } + onLogin(); + }); + } + }; + + function onLogin() { + $location.path('/desktop'); + $scope.$apply(); + } + }; + + return LoginInitialCtrl; +}); diff --git a/src/js/controller/login-new-device.js b/src/js/controller/login-new-device.js new file mode 100644 index 0000000..783b06c --- /dev/null +++ b/src/js/controller/login-new-device.js @@ -0,0 +1,13 @@ +define(function() { + 'use strict'; + + var LoginExistingCtrl = function($scope) { + + $scope.confirmPassphrase = function() { + window.alert('Not implemented yet!'); + }; + + }; + + return LoginExistingCtrl; +}); \ No newline at end of file diff --git a/src/js/controller/login.js b/src/js/controller/login.js index 164e086..0e0efb0 100644 --- a/src/js/controller/login.js +++ b/src/js/controller/login.js @@ -4,44 +4,52 @@ define(function(require) { var appController = require('js/app-controller'); var LoginCtrl = function($scope, $location) { - - // start the main app controller appController.start(function(err) { if (err) { console.error(err); return; } - if (window.chrome && chrome.identity) { - login('passphrase', onLogin); + if (!window.chrome || !chrome.identity) { + $location.path('/desktop'); + $scope.$apply(); return; } - onLogin(); + initializeUser(); }); - function login(password, callback) { + function initializeUser() { // get OAuth token from chrome - appController.fetchOAuthToken(password, function(err) { + appController.fetchOAuthToken(function(err, auth) { if (err) { console.error(err); return; } - // login to imap backend - appController._emailDao.imapLogin(function(err) { + appController.init(auth.emailAddress, auth.token, function(err, availableKeys) { if (err) { console.error(err); return; } - callback(); + redirect(availableKeys); }); }); } - function onLogin() { - $location.path('/desktop'); + function redirect(availableKeys) { + // redirect if needed + if (!availableKeys.publicKey) { + // no public key available, start onboarding process + $location.path('/login-initial'); + } else if (!availableKeys.privateKey) { + // no private key, import key + $location.path('/login-new-device'); + } else { + // public and private key available, just login + $location.path('/login-existing'); + } $scope.$apply(); } }; diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index c67a703..e479210 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -22,7 +22,7 @@ define(function(require) { /** * Inits all dependencies */ - EmailDAO.prototype.init = function(account, passphrase, callback) { + EmailDAO.prototype.init = function(account, callback) { var self = this; self._account = account; @@ -48,62 +48,63 @@ define(function(require) { callback(err); return; } - // init crypto - initCrypto(storedKeypair); + callback(null, storedKeypair); }); }); } + }; - function initCrypto(storedKeypair) { - if (storedKeypair && storedKeypair.privateKey && storedKeypair.publicKey) { - // import existing key pair into crypto module - self._crypto.importKeys({ - passphrase: passphrase, - privateKeyArmored: storedKeypair.privateKey.encryptedKey, - publicKeyArmored: storedKeypair.publicKey.publicKey - }, callback); + EmailDAO.prototype.unlock = function(keypair, passphrase, callback) { + var self = this; + + if (keypair && keypair.privateKey && keypair.publicKey) { + // import existing key pair into crypto module + self._crypto.importKeys({ + passphrase: passphrase, + privateKeyArmored: keypair.privateKey.encryptedKey, + publicKeyArmored: keypair.publicKey.publicKey + }, callback); + return; + } + + // no keypair for is stored for the user... generate a new one + self._crypto.generateKeys({ + emailAddress: self._account.emailAddress, + keySize: self._account.asymKeySize, + passphrase: passphrase + }, function(err, generatedKeypair) { + if (err) { + callback(err); return; } - // no keypair for is stored for the user... generate a new one - self._crypto.generateKeys({ - emailAddress: self._account.emailAddress, - keySize: self._account.asymKeySize, - passphrase: passphrase - }, function(err, generatedKeypair) { + // import the new key pair into crypto module + self._crypto.importKeys({ + passphrase: passphrase, + privateKeyArmored: generatedKeypair.privateKeyArmored, + publicKeyArmored: generatedKeypair.publicKeyArmored + }, function(err) { if (err) { callback(err); return; } - // import the new key pair into crypto module - self._crypto.importKeys({ - passphrase: passphrase, - privateKeyArmored: generatedKeypair.privateKeyArmored, - publicKeyArmored: generatedKeypair.publicKeyArmored - }, function(err) { - if (err) { - callback(err); - return; + // persist newly generated keypair + var newKeypair = { + publicKey: { + _id: generatedKeypair.keyId, + userId: self._account.emailAddress, + publicKey: generatedKeypair.publicKeyArmored + }, + privateKey: { + _id: generatedKeypair.keyId, + userId: self._account.emailAddress, + encryptedKey: generatedKeypair.privateKeyArmored } - - // persist newly generated keypair - var newKeypair = { - publicKey: { - _id: generatedKeypair.keyId, - userId: self._account.emailAddress, - publicKey: generatedKeypair.publicKeyArmored - }, - privateKey: { - _id: generatedKeypair.keyId, - userId: self._account.emailAddress, - encryptedKey: generatedKeypair.privateKeyArmored - } - }; - self._keychain.putUserKeyPair(newKeypair, callback); - }); + }; + self._keychain.putUserKeyPair(newKeypair, callback); }); - } + }); }; // diff --git a/src/js/dao/keychain-dao.js b/src/js/dao/keychain-dao.js index a14f60d..50ad085 100644 --- a/src/js/dao/keychain-dao.js +++ b/src/js/dao/keychain-dao.js @@ -155,24 +155,22 @@ define(['underscore', 'js/dao/lawnchair-dao'], function(_, jsonDao) { // persist private key in local storage self.lookupPrivateKey(keypairId, function(err, savedPrivkey) { + var keys = {}; + if (err) { callback(err); return; } - // validate fetched key - if (savedPubkey && savedPubkey.publicKey && savedPrivkey && savedPrivkey.encryptedKey) { - callback(null, { - publicKey: savedPubkey, - privateKey: savedPrivkey - }); - return; - - } else { - // continue without keypair... generate in crypto.js - callback(); - return; + if (savedPubkey && savedPubkey.publicKey) { + keys.publicKey = savedPubkey; } + + if (savedPrivkey && savedPrivkey.encryptedKey) { + keys.privateKey = savedPrivkey; + } + + callback(null, keys); }); }); } @@ -250,33 +248,9 @@ define(['underscore', 'js/dao/lawnchair-dao'], function(_, jsonDao) { }; KeychainDAO.prototype.lookupPrivateKey = function(id, callback) { - var self = this; - // lookup in local storage jsonDao.read('privatekey_' + id, function(privkey) { - if (!privkey) { - // fetch from cloud storage - self._cloudstorage.getPrivateKey(id, function(err, cloudPrivkey) { - if (err) { - callback(err); - return; - } - - // cache private key in cache - self.saveLocalPrivateKey(cloudPrivkey, function(err) { - if (err) { - callback(err); - return; - } - - callback(null, cloudPrivkey); - }); - - }); - - } else { - callback(null, privkey); - } + callback(null, privkey); }); }; diff --git a/src/sass/all.scss b/src/sass/all.scss index f5edfc2..896f5e7 100755 --- a/src/sass/all.scss +++ b/src/sass/all.scss @@ -23,3 +23,4 @@ @import "views/mail-list"; @import "views/read"; @import "views/write"; +@import "views/login"; diff --git a/src/sass/views/_login.scss b/src/sass/views/_login.scss new file mode 100644 index 0000000..a735c53 --- /dev/null +++ b/src/sass/views/_login.scss @@ -0,0 +1,49 @@ +.view-login { + width: 100%; + height: 100%; + background-color: #F9F9F9; + + h1, a { + color: #fff; + text-decoration: none; + text-shadow: 0 -1px 1px $color-grey-medium; + } + + header { + padding: 0 $nav-padding; + height: 84px; + h1 { + font-family: 'Mensch'; + font-weight: normal; + font-size: 5em; + height: 100px; + margin: 0px; + padding: 0px; + padding-top: 10px; + color: #00C6FF; + + span { + font-family: 'Mensch Thin'; + } + } + } + + .content { + padding: 100px $nav-padding 0 $nav-padding; + + input { + border: 0!important; + outline: none; + padding: 0; + } + + .passphrase {} + + .confirm-control {} + + .info-text { + text-align:justify; + text-justify:inter-word; + } + } +} \ No newline at end of file diff --git a/src/tpl/loading.html b/src/tpl/loading.html new file mode 100644 index 0000000..821d04c --- /dev/null +++ b/src/tpl/loading.html @@ -0,0 +1 @@ +

loading...

\ No newline at end of file diff --git a/src/tpl/login-existing.html b/src/tpl/login-existing.html new file mode 100644 index 0000000..7d19487 --- /dev/null +++ b/src/tpl/login-existing.html @@ -0,0 +1,15 @@ +
+
+

WHITEOUT.IO

+
+ +
+
+

Please enter your passphrase:

+
+ +
+ +
+
+
\ No newline at end of file diff --git a/src/tpl/login-initial.html b/src/tpl/login-initial.html new file mode 100644 index 0000000..8195ae4 --- /dev/null +++ b/src/tpl/login-initial.html @@ -0,0 +1,23 @@ +
+
+

WHITEOUT.IO

+
+ +
+
+

Please enter your passphrase:

+
+ +
+

Your passphrase protects the secrets that protect your privacy. You are the only person in charge of your privacy, which is a good thing and the way it should be. The passphrase will never be stored by this application. So you might want to write it down and put it into a safe place. You will need this passphrase when you re-open this application. If you lose the passphrase, your communication is irretrievably lost and cannot be restored.

+
+ +
+

Please confirm your passphrase:

+
+ +
+ +
+
+
\ No newline at end of file diff --git a/src/tpl/login-new-device.html b/src/tpl/login-new-device.html new file mode 100644 index 0000000..9b5ebcc --- /dev/null +++ b/src/tpl/login-new-device.html @@ -0,0 +1 @@ +

not implemented yet...

diff --git a/src/tpl/login.html b/src/tpl/login.html deleted file mode 100644 index 67f8032..0000000 --- a/src/tpl/login.html +++ /dev/null @@ -1 +0,0 @@ -
Logging in...
\ 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 a21f9c1..7401c78 100644 --- a/test/new-unit/app-controller-test.js +++ b/test/new-unit/app-controller-test.js @@ -59,7 +59,7 @@ define(function(require) { controller._appConfigStore.listItems.yields(null, []); controller._appConfigStore.storeList.yields(); - controller.fetchOAuthToken(appControllerTest.passphrase, function(err) { + controller.fetchOAuthToken(function(err) { expect(err).to.not.exist; expect(controller._appConfigStore.listItems.calledOnce).to.be.true; expect(controller._appConfigStore.storeList.calledOnce).to.be.true; @@ -72,7 +72,7 @@ define(function(require) { it('should work when the email address is cached', function(done) { controller._appConfigStore.listItems.yields(null, ['asdf']); - controller.fetchOAuthToken(appControllerTest.passphrase, function(err) { + controller.fetchOAuthToken(function(err) { expect(err).to.not.exist; expect(controller._appConfigStore.listItems.calledOnce).to.be.true; expect(window.chrome.identity.getAuthToken.calledOnce).to.be.true; diff --git a/test/new-unit/email-dao-test.js b/test/new-unit/email-dao-test.js index d4b0292..54fb894 100644 --- a/test/new-unit/email-dao-test.js +++ b/test/new-unit/email-dao-test.js @@ -53,61 +53,129 @@ define(function(require) { devicestorageStub = sinon.createStubInstance(DeviceStorageDAO); emailDao = new EmailDAO(keychainStub, imapClientStub, smtpClientStub, pgpStub, devicestorageStub); + emailDao._account = account; }); afterEach(function() {}); describe('init', function() { + beforeEach(function() { + delete emailDao._account; + }); + it('should fail due to error in getUserKeyPair', function(done) { devicestorageStub.init.yields(); keychainStub.getUserKeyPair.yields(42); - emailDao.init(account, emaildaoTest.passphrase, function(err) { + emailDao.init(account, function(err) { expect(devicestorageStub.init.calledOnce).to.be.true; expect(err).to.equal(42); done(); }); }); - it('should init with new keygen', function(done) { + it('should init', function(done) { + var mockKeyPair = {}; + devicestorageStub.init.yields(); - keychainStub.getUserKeyPair.yields(); + keychainStub.getUserKeyPair.yields(null, mockKeyPair); + + emailDao.init(account, function(err, keyPair) { + expect(err).to.not.exist; + + expect(keyPair === mockKeyPair).to.be.true; + expect(devicestorageStub.init.calledOnce).to.be.true; + expect(keychainStub.getUserKeyPair.calledOnce).to.be.true; + + done(); + }); + }); + }); + + describe('unlock', function() { + it('should unlock with new key', function(done) { pgpStub.generateKeys.yields(null, {}); pgpStub.importKeys.yields(); keychainStub.putUserKeyPair.yields(); - emailDao.init(account, emaildaoTest.passphrase, function(err) { - expect(devicestorageStub.init.calledOnce).to.be.true; - expect(keychainStub.getUserKeyPair.calledOnce).to.be.true; + emailDao.unlock({}, emaildaoTest.passphrase, function(err) { + expect(err).to.not.exist; + expect(pgpStub.generateKeys.calledOnce).to.be.true; expect(pgpStub.importKeys.calledOnce).to.be.true; expect(keychainStub.putUserKeyPair.calledOnce).to.be.true; - expect(err).to.not.exist; + expect(pgpStub.generateKeys.calledWith({ + emailAddress: account.emailAddress, + keySize: account.asymKeySize, + passphrase: emaildaoTest.passphrase + })).to.be.true; + done(); }); }); - it('should init with stored keygen', function(done) { - devicestorageStub.init.yields(); - keychainStub.getUserKeyPair.yields(null, { - publicKey: { - _id: 'keyId', - userId: emaildaoTest.user, - publicKey: 'publicKeyArmored' - }, - privateKey: { - _id: 'keyId', - userId: emaildaoTest.user, - encryptedKey: 'privateKeyArmored' - } - }); + it('should unlock with existing key pair', function(done) { pgpStub.importKeys.yields(); - emailDao.init(account, emaildaoTest.passphrase, function(err) { - expect(devicestorageStub.init.calledOnce).to.be.true; - expect(keychainStub.getUserKeyPair.calledOnce).to.be.true; - expect(pgpStub.importKeys.calledOnce).to.be.true; + emailDao.unlock({ + privateKey: { + encryptedKey: 'cryptocrypto' + }, + publicKey: { + publicKey: 'omgsocrypto' + } + }, emaildaoTest.passphrase, function(err) { expect(err).to.not.exist; + + expect(pgpStub.importKeys.calledOnce).to.be.true; + expect(pgpStub.importKeys.calledWith({ + passphrase: emaildaoTest.passphrase, + privateKeyArmored: 'cryptocrypto', + publicKeyArmored: 'omgsocrypto' + })).to.be.true; + + done(); + }); + }); + + it('should not unlock with error during keygen', function(done) { + pgpStub.generateKeys.yields(new Error('fubar')); + + emailDao.unlock({}, emaildaoTest.passphrase, function(err) { + expect(err).to.exist; + + expect(pgpStub.generateKeys.calledOnce).to.be.true; + + done(); + }); + }); + + it('should not unloch with error during key import', function(done) { + pgpStub.generateKeys.yields(null, {}); + pgpStub.importKeys.yields(new Error('fubar')); + + emailDao.unlock({}, emaildaoTest.passphrase, function(err) { + expect(err).to.exist; + + expect(pgpStub.generateKeys.calledOnce).to.be.true; + expect(pgpStub.importKeys.calledOnce).to.be.true; + + done(); + }); + }); + + it('should not unlock with error during key store', function(done) { + pgpStub.generateKeys.yields(null, {}); + pgpStub.importKeys.yields(); + keychainStub.putUserKeyPair.yields(new Error('omgwtf')); + + emailDao.unlock({}, emaildaoTest.passphrase, function(err) { + expect(err).to.exist; + + expect(pgpStub.generateKeys.calledOnce).to.be.true; + expect(pgpStub.importKeys.calledOnce).to.be.true; + expect(keychainStub.putUserKeyPair.calledOnce).to.be.true; + done(); }); }); @@ -141,14 +209,16 @@ define(function(require) { pgpStub.importKeys.yields(); keychainStub.putUserKeyPair.yields(); - emailDao.init(account, emaildaoTest.passphrase, function(err) { - expect(devicestorageStub.init.calledOnce).to.be.true; - expect(keychainStub.getUserKeyPair.calledOnce).to.be.true; - expect(pgpStub.generateKeys.calledOnce).to.be.true; - expect(pgpStub.importKeys.calledOnce).to.be.true; - expect(keychainStub.putUserKeyPair.calledOnce).to.be.true; - expect(err).to.not.exist; - done(); + emailDao.init(account, function(err, keyPair) { + emailDao.unlock(keyPair, emaildaoTest.passphrase, function(err) { + expect(devicestorageStub.init.calledOnce).to.be.true; + expect(keychainStub.getUserKeyPair.calledOnce).to.be.true; + expect(pgpStub.generateKeys.calledOnce).to.be.true; + expect(pgpStub.importKeys.calledOnce).to.be.true; + expect(keychainStub.putUserKeyPair.calledOnce).to.be.true; + expect(err).to.not.exist; + done(); + }); }); });