create login views

This commit is contained in:
Felix Hammerl 2013-10-21 13:10:42 +02:00
parent 4bcc1d6bdf
commit 488a377580
17 changed files with 407 additions and 134 deletions

View File

@ -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;

View File

@ -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('/write/:folder/:id', {
templateUrl: 'tpl/write.html',
controller: WriteCtrl

View File

@ -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;
});

View File

@ -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;
});

View File

@ -0,0 +1,13 @@
define(function() {
'use strict';
var LoginExistingCtrl = function($scope) {
$scope.confirmPassphrase = function() {
window.alert('Not implemented yet!');
};
};
return LoginExistingCtrl;
});

View File

@ -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();
}
};

View File

@ -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);
});
}
});
};
//

View File

@ -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);
});
};

View File

@ -23,3 +23,4 @@
@import "views/mail-list";
@import "views/read";
@import "views/write";
@import "views/login";

View File

@ -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;
}
}
}

1
src/tpl/loading.html Normal file
View File

@ -0,0 +1 @@
<p>loading...</p>

View File

@ -0,0 +1,15 @@
<div class="view-login">
<header>
<h1>WHITEOUT<span>.IO</span></h1>
</header>
<div class="content">
<div class="passphrase">
<p><span>Please enter your passphrase: </span><input type="password" ng-model="passphrase" class="passphrase" placeholder="Passphrase" tabindex="1"></p>
</div>
<div class="confirm-control">
<button ng-click="confirmPassphrase()" class="btn" ng-disabled="!passphrase" tabindex="2">Confirm passphrase</button>
</div>
</div>
</div>

View File

@ -0,0 +1,23 @@
<div class="view-login">
<header>
<h1>WHITEOUT<span>.IO</span></h1>
</header>
<div class="content">
<div class="passphrase">
<p><span>Please enter your passphrase: </span><input type="password" ng-model="passphrase" class="passphrase" placeholder="Passphrase" tabindex="1"></p>
</div>
<div class="info-text">
<p>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 <b>never</b> be stored by this application. So you might want to write it down and put it into a <b>safe</b> place. You will need this passphrase when you re-open this application. If you lose the passphrase, your communication is irretrievably lost and <b>cannot be restored</b>.</p>
</div>
<div class="passphrase">
<p><span>Please confirm your passphrase: </span><input type="password" ng-model="confirmation" class="confirmation" placeholder="Confirmation" tabindex="2"></p>
</div>
<div class="confirm-control">
<button ng-click="confirmPassphrase()" class="btn" ng-disabled="!passphrase || passphrase!==confirmation" tabindex="3">Confirm passphrase</button>
</div>
</div>
</div>

View File

@ -0,0 +1 @@
<p>not implemented yet...</p>

View File

@ -1 +0,0 @@
<div class="message">Logging in...</div>

View File

@ -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;

View File

@ -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();
});
});
});