1
0
mirror of https://github.com/moparisthebest/mail synced 2024-12-22 07:18:49 -05:00

Merge pull request #79 from whiteout-io/dev/WO-55

Dev/wo 55
This commit is contained in:
Felix Hammerl 2014-06-26 14:58:15 +02:00
commit 37b1862e9f
79 changed files with 5470 additions and 3809 deletions

4
.gitignore vendored
View File

@ -8,7 +8,5 @@ dist/
release/
test/integration/src/
src/lib/*.js
src/js/crypto/aes-cbc.js
src/js/crypto/crypto-batch.js
src/js/crypto/rsa.js
src/js/crypto/aes-gcm.js
src/js/crypto/util.js

View File

@ -44,29 +44,17 @@ module.exports = function(grunt) {
},
jshint: {
all: ['Gruntfile.js', 'src/*.js', 'src/js/**/*.js', 'test/new-unit/*.js', 'test/unit/*.js', 'test/integration/*.js'],
all: ['Gruntfile.js', 'src/*.js', 'src/js/**/*.js', 'test/unit/*.js', 'test/integration/*.js'],
options: {
jshintrc: '.jshintrc'
}
},
qunit: {
all: {
options: {
timeout: 20000,
urls: ['http://localhost:<%= connect.test.options.port %>/test/unit/index.html'
/*,
'http://localhost:<%= connect.test.options.port %>/test/integration/index.html'*/
]
}
}
},
mocha: {
all: {
options: {
urls: [
'http://localhost:<%= connect.test.options.port %>/test/new-unit/index.html',
'http://localhost:<%= connect.test.options.port %>/test/unit/index.html',
'http://localhost:<%= connect.test.options.port %>/test/integration/index.html'
],
run: false,
@ -255,7 +243,6 @@ module.exports = function(grunt) {
// Load the plugin(s)
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-qunit');
grunt.loadNpmTasks('grunt-mocha');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-csso');

View File

@ -32,7 +32,7 @@ We take the privacy of your data very seriously. Here are some of the technical
You can download a prebuilt bundle under [releases](https://github.com/whiteout-io/mail-html5/releases) or build your own from source (requires [node.js](http://nodejs.org/download/), [grunt](http://gruntjs.com/getting-started#installing-the-cli) and [sass](http://sass-lang.com/install)):
npm install && npm test
This will download all dependencies, run the tests and build the Chrome Packaged App bundle **DEV.zip** which can be installed under [chrome://extensions](chrome://extensions) in developer mode.
### Development
@ -40,7 +40,7 @@ For development you can start a connect dev server:
grunt dev
Then visit [http://localhost:8580/dist/chrome.html#/desktop](http://localhost:8580/dist/chrome.html#/desktop) for front-end code or [http://localhost:8580/test/new-unit/](http://localhost:8580/test/new-unit/) to test JavaScript changes. You can also start a watch task so you don't have rebuild everytime you make a change:
Then visit [http://localhost:8580/dist/chrome.html#/desktop](http://localhost:8580/dist/chrome.html#/desktop) for front-end code or [http://localhost:8580/test/unit/](http://localhost:8580/test/unit/) to test JavaScript changes. You can also start a watch task so you don't have rebuild everytime you make a change:
grunt watch

View File

@ -10,7 +10,7 @@
"start": "grunt && grunt dev"
},
"dependencies": {
"crypto-lib": "https://github.com/whiteout-io/crypto-lib/tarball/v0.1.1",
"crypto-lib": "https://github.com/whiteout-io/crypto-lib/tarball/v0.2.0",
"imap-client": "https://github.com/whiteout-io/imap-client/tarball/v0.3.3",
"mailreader": "https://github.com/whiteout-io/mailreader/tarball/v0.3.3",
"pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/v0.3.4",
@ -27,7 +27,6 @@
"sinon": "~1.7.3",
"grunt-contrib-connect": "~0.5.0",
"grunt-contrib-jshint": "~0.6.4",
"grunt-contrib-qunit": "~0.2.2",
"grunt-mocha": "~0.4.1",
"grunt-contrib-clean": "~0.5.0",
"grunt-csso": "~0.6.1",

View File

@ -25,8 +25,10 @@ define(function(require) {
*/
app.config = {
cloudUrl: cloudUrl || 'https://keys.whiteout.io',
symKeySize: 128,
symIvSize: 128,
privkeyServerUrl: 'https://keychain-test.whiteout.io',
serverPrivateKeyId: 'EE342F0DDBB0F3BE',
symKeySize: 256,
symIvSize: 96,
asymKeySize: 2048,
workerPath: 'js',
gmail: {

View File

@ -12,6 +12,7 @@ define(function(require) {
OutboxBO = require('js/bo/outbox'),
mailreader = require('mailreader'),
ImapClient = require('imap-client'),
Crypto = require('js/crypto/crypto'),
RestDAO = require('js/dao/rest-dao'),
EmailDAO = require('js/dao/email-dao'),
appConfig = require('js/app-config'),
@ -20,6 +21,7 @@ define(function(require) {
KeychainDAO = require('js/dao/keychain-dao'),
PublicKeyDAO = require('js/dao/publickey-dao'),
LawnchairDAO = require('js/dao/lawnchair-dao'),
PrivateKeyDAO = require('js/dao/privatekey-dao'),
InvitationDAO = require('js/dao/invitation-dao'),
DeviceStorageDAO = require('js/dao/devicestorage-dao'),
UpdateHandler = require('js/util/update/update-handler');
@ -55,7 +57,7 @@ define(function(require) {
};
self.buildModules = function(options) {
var lawnchairDao, restDao, pubkeyDao, emailDao, keychain, pgp, userStorage, pgpbuilder, oauth, appConfigStore;
var lawnchairDao, restDao, pubkeyDao, privkeyDao, crypto, emailDao, keychain, pgp, userStorage, pgpbuilder, oauth, appConfigStore;
// start the mailreader's worker thread
mailreader.startWorker(config.workerPath + '/../lib/mailreader-parser-worker.js');
@ -64,9 +66,12 @@ define(function(require) {
restDao = new RestDAO();
lawnchairDao = new LawnchairDAO();
pubkeyDao = new PublicKeyDAO(restDao);
privkeyDao = new PrivateKeyDAO(new RestDAO(config.privkeyServerUrl));
oauth = new OAuth(new RestDAO('https://www.googleapis.com'));
self._keychain = keychain = new KeychainDAO(lawnchairDao, pubkeyDao);
crypto = new Crypto();
self._pgp = pgp = new PGP();
self._keychain = keychain = new KeychainDAO(lawnchairDao, pubkeyDao, privkeyDao, crypto, pgp);
keychain.requestPermissionForKeyUpdate = function(params, callback) {
var message = params.newKey ? str.updatePublicKeyMsgNewKey : str.updatePublicKeyMsgRemovedKey;
message = message.replace('{0}', params.userId);
@ -85,7 +90,6 @@ define(function(require) {
self._auth = new Auth(appConfigStore, oauth, new RestDAO('/ca'));
self._userStorage = userStorage = new DeviceStorageDAO(lawnchairDao);
self._invitationDao = new InvitationDAO(restDao);
self._crypto = pgp = new PGP();
self._pgpbuilder = pgpbuilder = new PgpBuilder();
self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader);
self._outboxBo = new OutboxBO(emailDao, keychain, userStorage);

View File

@ -8,17 +8,19 @@ requirejs([
'js/controller/add-account',
'js/controller/account',
'js/controller/set-passphrase',
'js/controller/privatekey-upload',
'js/controller/contacts',
'js/controller/about',
'js/controller/login',
'js/controller/login-initial',
'js/controller/login-new-device',
'js/controller/login-existing',
'js/controller/login-privatekey-download',
'js/controller/mail-list',
'js/controller/read',
'js/controller/write',
'js/controller/navigation',
'cryptoLib/util',
'js/crypto/util',
'js/util/error',
'fastclick',
'angularSanitize',
@ -31,12 +33,14 @@ requirejs([
AddAccountCtrl,
AccountCtrl,
SetPassphraseCtrl,
PrivateKeyUploadCtrl,
ContactsCtrl,
AboutCtrl,
LoginCtrl,
LoginInitialCtrl,
LoginNewDeviceCtrl,
LoginExistingCtrl,
LoginPrivateKeyDownloadCtrl,
MailListCtrl,
ReadCtrl,
WriteCtrl,
@ -89,6 +93,10 @@ requirejs([
templateUrl: 'tpl/login-new-device.html',
controller: LoginNewDeviceCtrl
});
$routeProvider.when('/login-privatekey-download', {
templateUrl: 'tpl/login-privatekey-download.html',
controller: LoginPrivateKeyDownloadCtrl
});
$routeProvider.when('/desktop', {
templateUrl: 'tpl/desktop.html',
controller: NavigationCtrl
@ -113,6 +121,7 @@ requirejs([
app.controller('MailListCtrl', MailListCtrl);
app.controller('AccountCtrl', AccountCtrl);
app.controller('SetPassphraseCtrl', SetPassphraseCtrl);
app.controller('PrivateKeyUploadCtrl', PrivateKeyUploadCtrl);
app.controller('ContactsCtrl', ContactsCtrl);
app.controller('AboutCtrl', AboutCtrl);
app.controller('DialogCtrl', DialogCtrl);

View File

@ -2,7 +2,7 @@ define(function(require) {
'use strict';
var _ = require('underscore'),
util = require('cryptoLib/util'),
util = require('js/crypto/util'),
config = require('js/app-config').config,
outboxDb = 'email_OUTBOX';
@ -27,7 +27,7 @@ define(function(require) {
this._outboxBusy = false;
};
/**
/**
* This function activates the periodic checking of the local device storage for pending mails.
* @param {Function} callback(error, pendingMailsCount) Callback that informs you about the count of pending mails.
*/

View File

@ -12,7 +12,7 @@ define(function(require) {
var AccountCtrl = function($scope) {
userId = appController._emailDao._account.emailAddress;
keychain = appController._keychain;
pgp = appController._crypto;
pgp = appController._pgp;
$scope.state.account = {
toggle: function(to) {

View File

@ -12,7 +12,7 @@ define(function(require) {
var ContactsCtrl = function($scope) {
keychain = appController._keychain,
pgp = appController._crypto;
pgp = appController._pgp;
$scope.state.contacts = {
toggle: function(to) {

View File

@ -6,7 +6,7 @@ define(function(require) {
var LoginExistingCtrl = function($scope, $location) {
var emailDao = appController._emailDao,
pgp = appController._crypto;
pgp = appController._pgp;
$scope.incorrect = false;

View File

@ -0,0 +1,103 @@
define(function(require) {
'use strict';
var appController = require('js/app-controller');
var LoginPrivateKeyDownloadCtrl = function($scope, $location) {
var keychain = appController._keychain,
emailDao = appController._emailDao,
userId = emailDao._account.emailAddress;
$scope.step = 1;
$scope.verifyRecoveryToken = function(callback) {
if (!$scope.recoveryToken) {
$scope.onError(new Error('Please set the recovery token!'));
return;
}
keychain.getUserKeyPair(userId, function(err, keypair) {
if (err) {
$scope.onError(err);
return;
}
// remember for storage later
$scope.cachedKeypair = keypair;
keychain.downloadPrivateKey({
userId: userId,
keyId: keypair.publicKey._id,
recoveryToken: $scope.recoveryToken
}, function(err, encryptedPrivateKey) {
if (err) {
$scope.onError(err);
return;
}
$scope.encryptedPrivateKey = encryptedPrivateKey;
callback();
});
});
};
$scope.decryptAndStorePrivateKeyLocally = function() {
var inputCode = '' + $scope.code0 + $scope.code1 + $scope.code2 + $scope.code3 + $scope.code4 + $scope.code5;
if (!inputCode) {
$scope.onError(new Error('Please enter the keychain code!'));
return;
}
var options = $scope.encryptedPrivateKey;
options.code = inputCode.toUpperCase();
keychain.decryptAndStorePrivateKeyLocally(options, function(err, privateKey) {
if (err) {
$scope.onError(err);
return;
}
// add private key to cached keypair object
$scope.cachedKeypair.privateKey = privateKey;
// try empty passphrase
emailDao.unlock({
keypair: $scope.cachedKeypair,
passphrase: undefined
}, function(err) {
if (err) {
// go to passphrase login screen
$scope.goTo('/login-existing');
return;
}
// passphrase is corrent ... go to main app
$scope.goTo('/desktop');
});
});
};
$scope.goForward = function() {
if ($scope.step === 1) {
$scope.verifyRecoveryToken(function() {
$scope.step++;
$scope.$apply();
});
return;
}
if ($scope.step === 2) {
$scope.decryptAndStorePrivateKeyLocally();
return;
}
};
$scope.goTo = function(location) {
$location.path(location);
$scope.$apply();
};
};
return LoginPrivateKeyDownloadCtrl;
});

View File

@ -52,9 +52,28 @@ define(function(require) {
if (typeof availableKeys === 'undefined') {
// no public key available, start onboarding process
goTo('/login-initial');
} else if (!availableKeys.privateKey) {
// no private key, import key
goTo('/login-new-device');
} else if (availableKeys && !availableKeys.privateKey) {
// check if private key is synced
appController._keychain.requestPrivateKeyDownload({
userId: availableKeys.publicKey.userId,
keyId: availableKeys.publicKey._id,
}, function(err, privateKeySynced) {
if (err) {
$scope.onError(err);
return;
}
if (privateKeySynced) {
// private key is synced, proceed to download
goTo('/login-privatekey-download');
return;
}
// no private key, import key file
goTo('/login-new-device');
});
} else {
// public and private key available, try empty passphrase
appController._emailDao.unlock({

View File

@ -0,0 +1,170 @@
define(function(require) {
'use strict';
var appController = require('js/app-controller'),
keychain, pgp;
var PrivateKeyUploadCtrl = function($scope) {
keychain = appController._keychain;
pgp = keychain._pgp;
$scope.state.privateKeyUpload = {
toggle: function(to) {
// open lightbox
$scope.state.lightbox = (to) ? 'privatekey-upload' : undefined;
if (!to) {
return;
}
// show syncing status
$scope.step = 4;
// check if key is already synced
$scope.checkServerForKey(function(privateKeySynced) {
if (privateKeySynced) {
// close lightbox
$scope.state.lightbox = undefined;
// show message
$scope.onError({
title: 'Info',
message: 'Your PGP key has already been synced.'
});
return;
}
// show sync ui if key is not synced
$scope.displayUploadUi();
});
}
};
$scope.checkServerForKey = function(callback) {
var keyParams = pgp.getKeyParams();
keychain.requestPrivateKeyDownload({
userId: keyParams.userId,
keyId: keyParams._id,
}, function(err, privateKeySynced) {
if (err) {
$scope.onError(err);
return;
}
if (privateKeySynced) {
callback(privateKeySynced);
return;
}
callback();
});
};
$scope.displayUploadUi = function() {
// go to step 1
$scope.step = 1;
// generate new code for the user
$scope.code = $scope.generateCode();
$scope.displayedCode = $scope.code.slice(0, 4) + '-' + $scope.code.slice(4, 8) + '-' + $scope.code.slice(8, 12) + '-' + $scope.code.slice(12, 16) + '-' + $scope.code.slice(16, 20) + '-' + $scope.code.slice(20, 24);
};
$scope.generateCode = function() {
function randomString(length, chars) {
var result = '';
var randomValues = new Uint8Array(length); // get random length number of bytes
window.crypto.getRandomValues(randomValues);
for (var i = 0; i < length; i++) {
result += chars[Math.round(randomValues[i] / 255 * (chars.length - 1))];
}
return result;
}
return randomString(24, '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ');
};
$scope.verifyCode = function() {
var inputCode = '' + $scope.code0 + $scope.code1 + $scope.code2 + $scope.code3 + $scope.code4 + $scope.code5;
if (inputCode.toUpperCase() !== $scope.code) {
var err = new Error('The code does not match. Please go back and check the generated code.');
err.sync = true;
$scope.onError(err);
return false;
}
return true;
};
$scope.setDeviceName = function(callback) {
keychain.setDeviceName($scope.deviceName, callback);
};
$scope.encryptAndUploadKey = function(callback) {
var userId = appController._emailDao._account.emailAddress;
var code = $scope.code;
// register device to keychain service
keychain.registerDevice({
userId: userId
}, function(err) {
if (err) {
$scope.onError(err);
return;
}
// encrypt private PGP key using code and upload
keychain.uploadPrivateKey({
userId: userId,
code: code
}, callback);
});
};
$scope.goBack = function() {
if ($scope.step > 1) {
$scope.step--;
}
};
$scope.goForward = function() {
if ($scope.step < 2) {
$scope.step++;
return;
}
if ($scope.step === 2 && $scope.verifyCode()) {
$scope.step++;
return;
}
if ($scope.step === 3) {
// set device name to local storage
$scope.setDeviceName(function(err) {
if (err) {
$scope.onError(err);
return;
}
// show spinner
$scope.step++;
$scope.$apply();
// init key sync
$scope.encryptAndUploadKey(function(err) {
if (err) {
$scope.onError(err);
return;
}
// close sync dialog
$scope.state.privateKeyUpload.toggle(false);
// show success message
$scope.onError({
title: 'Success',
message: 'Whiteout Keychain setup successful!'
});
});
});
}
};
};
return PrivateKeyUploadCtrl;
});

View File

@ -5,7 +5,7 @@ define(function(require) {
download = require('js/util/download'),
angular = require('angular'),
str = require('js/app-config').string,
emailDao, invitationDao, outbox, crypto, keychain;
emailDao, invitationDao, outbox, pgp, keychain;
//
// Controller
@ -16,7 +16,7 @@ define(function(require) {
emailDao = appController._emailDao;
invitationDao = appController._invitationDao;
outbox = appController._outboxBo;
crypto = appController._crypto;
pgp = appController._pgp;
keychain = appController._keychain;
// set default value so that the popover height is correct on init
@ -47,7 +47,7 @@ define(function(require) {
return;
}
var fpr = crypto.getFingerprint(pubkey.publicKey);
var fpr = pgp.getFingerprint(pubkey.publicKey);
var formatted = fpr.slice(32);
$scope.keyId = 'PGP key: ' + formatted;

View File

@ -10,7 +10,7 @@ define(function(require) {
var SetPassphraseCtrl = function($scope) {
keychain = appController._keychain;
pgp = appController._crypto;
pgp = appController._pgp;
$scope.state.setPassphrase = {
toggle: function(to) {

View File

@ -4,17 +4,17 @@ define(function(require) {
var angular = require('angular'),
_ = require('underscore'),
appController = require('js/app-controller'),
aes = require('cryptoLib/aes-cbc'),
util = require('cryptoLib/util'),
aes = require('js/crypto/aes-gcm'),
util = require('js/crypto/util'),
str = require('js/app-config').string,
crypto, emailDao, outbox, keychainDao;
pgp, emailDao, outbox, keychainDao;
//
// Controller
//
var WriteCtrl = function($scope, $filter) {
crypto = appController._crypto;
pgp = appController._pgp;
emailDao = appController._emailDao,
outbox = appController._outboxBo;
keychainDao = appController._keychain;
@ -218,7 +218,7 @@ define(function(require) {
return;
}
var fpr = crypto.getFingerprint(recipient.key.publicKey);
var fpr = pgp.getFingerprint(recipient.key.publicKey);
var formatted = fpr.slice(32);
$scope.keyId = formatted;

View File

@ -1,93 +0,0 @@
(function() {
'use strict';
// import web worker dependencies
importScripts('../../lib/require.js');
/**
* In the web worker thread context, 'this' and 'self' can be used as a global
* variable namespace similar to the 'window' object in the main thread
*/
self.onmessage = function(e) {
// fetch dependencies via require.js
require(['../../require-config'], function() {
require.config({
baseUrl: '../../lib'
});
require(['cryptoLib/crypto-batch'], function(batch) {
var output;
try {
output = doOperation(batch, e.data);
} catch (e) {
output = {
err: {
errMsg: (e.message) ? e.message : e
}
};
}
// pass output back to main thread
self.postMessage(output);
});
});
};
function doOperation(batch, i) {
var output;
//
// Asymmetric encryption
//
if (i.type === 'asymEncrypt' && i.receiverPubkeys && i.senderPrivkey && i.list) {
// start encryption
output = batch.encryptListForUser(i.list, i.receiverPubkeys, i.senderPrivkey);
} else if (i.type === 'asymDecrypt' && i.senderPubkeys && i.receiverPrivkey && i.list) {
// start decryption
output = batch.decryptListForUser(i.list, i.senderPubkeys, i.receiverPrivkey);
}
//
// Symmetric encryption
//
else if (i.type === 'symEncrypt' && i.list) {
// start encryption
output = batch.authEncryptList(i.list);
} else if (i.type === 'symDecrypt' && i.list && i.keys) {
// start decryption
output = batch.authDecryptList(i.list, i.keys);
}
//
// Reencryption of asymmetric items to symmetric items
//
else if (i.type === 'reencrypt' && i.senderPubkeys && i.receiverPrivkey && i.list && i.symKey) {
// start validation and re-encryption
output = batch.reencryptListKeysForUser(i.list, i.senderPubkeys, i.receiverPrivkey, i.symKey);
} else if (i.type === 'decryptItems' && i.symKey && i.list) {
// start decryption
output = batch.decryptKeysAndList(i.list, i.symKey);
}
//
// Error
//
else {
output = {
err: {
errMsg: 'Not all arguments for web worker crypto are defined!'
}
};
}
return output;
}
}());

View File

@ -5,120 +5,54 @@
define(function(require) {
'use strict';
var util = require('cryptoLib/util'),
aes = require('cryptoLib/aes-cbc'),
rsa = require('cryptoLib/rsa'),
cryptoBatch = require('cryptoLib/crypto-batch'),
var aes = require('js/crypto/aes-gcm'),
pbkdf2 = require('js/crypto/pbkdf2'),
config = require('js/app-config').config;
var passBasedKey,
BATCH_WORKER = '/crypto/crypto-batch-worker.js',
PBKDF2_WORKER = '/crypto/pbkdf2-worker.js';
var PBKDF2_WORKER = '/crypto/pbkdf2-worker.js';
var Crypto = function() {
};
var Crypto = function() {};
/**
* Initializes the crypto modules by fetching the user's
* encrypted secret key from storage and storing it in memory.
* Encrypt plaintext using AES-GCM.
* @param {String} plaintext The input string in UTF-16
* @param {String} key The base64 encoded key
* @param {String} iv The base64 encoded IV
* @param {Function} callback(error, ciphertext)
* @return {String} The base64 encoded ciphertext
*/
Crypto.prototype.init = function(args, callback) {
var self = this;
Crypto.prototype.encrypt = function(plaintext, key, iv, callback) {
var ct;
// valdiate input
if (!args.emailAddress || !args.keySize || !args.rsaKeySize || typeof args.password !== 'string' || !args.salt) {
callback({
errMsg: 'Crypto init failed. Not all args set!'
});
try {
ct = aes.encrypt(plaintext, key, iv);
} catch (err) {
callback(err);
return;
}
self.emailAddress = args.emailAddress;
self.keySize = args.keySize;
self.ivSize = args.keySize;
self.rsaKeySize = args.rsaKeySize;
callback(null, ct);
};
// derive PBKDF2 from password in web worker thread
self.deriveKey(args.password, args.salt, self.keySize, function(err, derivedKey) {
if (err) {
callback(err);
return;
}
/**
* Decrypt ciphertext suing AES-GCM
* @param {String} ciphertext The base64 encoded ciphertext
* @param {String} key The base64 encoded key
* @param {String} iv The base64 encoded IV
* @param {Function} callback(error, plaintext)
* @return {String} The decrypted plaintext in UTF-16
*/
Crypto.prototype.decrypt = function(ciphertext, key, iv, callback) {
var pt;
// remember pbkdf2 for later use
passBasedKey = derivedKey;
// check if key exists
if (!args.storedKeypair) {
// generate keys, encrypt and persist if none exists
generateKeypair(derivedKey);
} else {
// decrypt key
decryptKeypair(args.storedKeypair, derivedKey);
}
});
function generateKeypair(derivedKey) {
// generate RSA keypair in web worker
rsa.generateKeypair(self.rsaKeySize, function(err, generatedKeypair) {
if (err) {
callback(err);
return;
}
// encrypt keypair
var iv = util.random(self.ivSize);
var encryptedPrivateKey = aes.encrypt(generatedKeypair.privkeyPem, derivedKey, iv);
// new encrypted keypair object
var newKeypair = {
publicKey: {
_id: generatedKeypair._id,
userId: self.emailAddress,
publicKey: generatedKeypair.pubkeyPem
},
privateKey: {
_id: generatedKeypair._id,
userId: self.emailAddress,
encryptedKey: encryptedPrivateKey,
iv: iv
}
};
// return generated keypair for storage in keychain dao
callback(null, newKeypair);
});
try {
pt = aes.decrypt(ciphertext, key, iv);
} catch (err) {
callback(err);
return;
}
function decryptKeypair(storedKeypair, derivedKey) {
var decryptedPrivateKey;
// validate input
if (!storedKeypair || !storedKeypair.privateKey || !storedKeypair.privateKey.encryptedKey || !storedKeypair.privateKey.iv) {
callback({
errMsg: 'Incomplete arguments for private key decryption!'
});
return;
}
// try to decrypt with derivedKey
try {
var prK = storedKeypair.privateKey;
decryptedPrivateKey = aes.decrypt(prK.encryptedKey, derivedKey, prK.iv);
} catch (ex) {
callback({
errMsg: 'Wrong password!'
});
return;
}
// set rsa keys
rsa.init(storedKeypair.publicKey.publicKey, decryptedPrivateKey, storedKeypair.publicKey._id);
callback();
}
callback(null, pt);
};
/**
@ -139,181 +73,6 @@ define(function(require) {
});
};
//
// En/Decrypt a list of items with AES in a WebWorker thread
//
Crypto.prototype.symEncryptList = function(list, callback) {
var self = this,
key, envelope, envelopes = [];
// generate single secret key shared for all list items
key = util.random(self.keySize);
// package objects into batchable envelope format
list.forEach(function(i) {
envelope = {
id: i.id,
plaintext: i,
key: key,
iv: util.random(self.ivSize)
};
envelopes.push(envelope);
});
startWorker({
script: BATCH_WORKER,
args: {
type: 'symEncrypt',
list: envelopes
},
callback: function(err, encryptedList) {
// return generated secret key
callback(err, {
key: key,
list: encryptedList
});
},
noWorker: function() {
return cryptoBatch.authEncryptList(envelopes);
}
});
};
Crypto.prototype.symDecryptList = function(list, keys, callback) {
startWorker({
script: BATCH_WORKER,
args: {
type: 'symDecrypt',
list: list,
keys: keys
},
callback: callback,
noWorker: function() {
return cryptoBatch.authDecryptList(list, keys);
}
});
};
//
// En/Decrypt something speficially using the user's secret key
//
Crypto.prototype.encryptListForUser = function(list, receiverPubkeys, callback) {
var self = this,
envelope, envelopes = [];
if (!receiverPubkeys || receiverPubkeys.length !== 1) {
callback({
errMsg: 'Encryption is currently implemented for only one receiver!'
});
return;
}
var keypair = rsa.exportKeys();
var senderPrivkey = {
_id: keypair._id,
privateKey: keypair.privkeyPem
};
// package objects into batchable envelope format
list.forEach(function(i) {
envelope = {
id: i.id,
plaintext: i,
key: util.random(self.keySize),
iv: util.random(self.ivSize),
receiverPk: receiverPubkeys[0]._id
};
envelopes.push(envelope);
});
startWorker({
script: BATCH_WORKER,
args: {
type: 'asymEncrypt',
list: envelopes,
senderPrivkey: senderPrivkey,
receiverPubkeys: receiverPubkeys
},
callback: callback,
noWorker: function() {
return cryptoBatch.encryptListForUser(envelopes, receiverPubkeys, senderPrivkey);
}
});
};
Crypto.prototype.decryptListForUser = function(list, senderPubkeys, callback) {
if (!senderPubkeys || senderPubkeys < 1) {
callback({
errMsg: 'Sender public keys must be set!'
});
return;
}
var keypair = rsa.exportKeys();
var receiverPrivkey = {
_id: keypair._id,
privateKey: keypair.privkeyPem
};
startWorker({
script: BATCH_WORKER,
args: {
type: 'asymDecrypt',
list: list,
receiverPrivkey: receiverPrivkey,
senderPubkeys: senderPubkeys
},
callback: callback,
noWorker: function() {
return cryptoBatch.decryptListForUser(list, senderPubkeys, receiverPrivkey);
}
});
};
//
// Re-encrypt keys item and items seperately
//
Crypto.prototype.reencryptListKeysForUser = function(list, senderPubkeys, callback) {
var keypair = rsa.exportKeys();
var receiverPrivkey = {
_id: keypair._id,
privateKey: keypair.privkeyPem
};
startWorker({
script: BATCH_WORKER,
args: {
type: 'reencrypt',
list: list,
receiverPrivkey: receiverPrivkey,
senderPubkeys: senderPubkeys,
symKey: passBasedKey
},
callback: callback,
noWorker: function() {
return cryptoBatch.reencryptListKeysForUser(list, senderPubkeys, receiverPrivkey, passBasedKey);
}
});
};
Crypto.prototype.decryptKeysAndList = function(list, callback) {
startWorker({
script: BATCH_WORKER,
args: {
type: 'decryptItems',
list: list,
symKey: passBasedKey
},
callback: callback,
noWorker: function() {
return cryptoBatch.decryptKeysAndList(list, passBasedKey);
}
});
};
//
// helper functions
//

View File

@ -1,24 +1,25 @@
/**
* A Wrapper for Forge's PBKDF2 function
*/
define(['node-forge'], function(forge) {
define(['forge'], function(forge) {
'use strict';
var self = {};
/**
* PBKDF2-HMAC-SHA1 key derivation with a random salt and 1000 iterations
* @param password [String] The password in UTF8
* @param salt [String] The base64 encoded salt
* @param keySize [Number] The key size in bits
* @return [String] The base64 encoded key
* PBKDF2-HMAC-SHA256 key derivation with a random salt and 10000 iterations
* @param {String} password The password in UTF8
* @param {String} salt The base64 encoded salt
* @param {String} keySize The key size in bits
* @return {String} The base64 encoded key
*/
self.getKey = function(password, salt, keySize) {
var key = forge.pkcs5.pbkdf2(password, forge.util.decode64(salt), 1000, keySize / 8);
var keyBase64 = forge.util.encode64(key);
var saltUtf8 = forge.util.decode64(salt);
var md = forge.md.sha256.create();
var key = forge.pkcs5.pbkdf2(password, saltUtf8, 10000, keySize / 8, md);
return keyBase64;
return forge.util.encode64(key);
};
return self;
});
});

View File

@ -20,23 +20,23 @@ define(function(require) {
var userId, passphrase;
if (!util.emailRegEx.test(options.emailAddress) || !options.keySize) {
callback({
errMsg: 'Crypto init failed. Not all options set!'
});
callback(new Error('Crypto init failed. Not all options set!'));
return;
}
// generate keypair (keytype 1=RSA)
// generate keypair
userId = 'Whiteout User <' + options.emailAddress + '>';
passphrase = (options.passphrase) ? options.passphrase : undefined;
openpgp.generateKeyPair(1, options.keySize, userId, passphrase, onGenerated);
openpgp.generateKeyPair({
keyType: 1, // (keytype 1=RSA)
numBits: options.keySize,
userId: userId,
passphrase: passphrase
}, onGenerated);
function onGenerated(err, keys) {
if (err) {
callback({
errMsg: 'Keygeneration failed!',
err: err
});
callback(new Error('Keygeneration failed!'));
return;
}
@ -141,9 +141,7 @@ define(function(require) {
// check options
if (!options.privateKeyArmored || !options.publicKeyArmored) {
callback({
errMsg: 'Importing keys failed. Not all options set!'
});
callback(new Error('Importing keys failed. Not all options set!'));
return;
}
@ -158,18 +156,14 @@ define(function(require) {
this._privateKey = openpgp.key.readArmored(options.privateKeyArmored).keys[0];
} catch (e) {
resetKeys();
callback({
errMsg: 'Importing keys failed. Parsing error!'
});
callback(new Error('Importing keys failed. Parsing error!'));
return;
}
// decrypt private key with passphrase
if (!this._privateKey.decrypt(options.passphrase)) {
resetKeys();
callback({
errMsg: 'Incorrect passphrase!'
});
callback(new Error('Incorrect passphrase!'));
return;
}
@ -178,9 +172,7 @@ define(function(require) {
privKeyId = this._privateKey.getKeyPacket().getKeyId().toHex();
if (!pubKeyId || !privKeyId || pubKeyId !== privKeyId) {
resetKeys();
callback({
errMsg: 'Key IDs dont match!'
});
callback(new Error('Key IDs dont match!'));
return;
}
@ -192,9 +184,7 @@ define(function(require) {
*/
PGP.prototype.exportKeys = function(callback) {
if (!this._publicKey || !this._privateKey) {
callback({
errMsg: 'Could not export keys!'
});
callback(new Error('Could not export keys!'));
return;
}
@ -215,9 +205,7 @@ define(function(require) {
newPassphrase = (options.newPassphrase) ? options.newPassphrase : undefined;
if (!options.privateKeyArmored) {
callback({
errMsg: 'Private key must be specified to change passphrase!'
});
callback(new Error('Private key must be specified to change passphrase!'));
return;
}
@ -231,17 +219,13 @@ define(function(require) {
try {
privKey = openpgp.key.readArmored(options.privateKeyArmored).keys[0];
} catch (e) {
callback({
errMsg: 'Importing key failed. Parsing error!'
});
callback(new Error('Importing key failed. Parsing error!'));
return;
}
// decrypt private key with passphrase
if (!privKey.decrypt(options.oldPassphrase)) {
callback({
errMsg: 'Old passphrase incorrect!'
});
callback(new Error('Old passphrase incorrect!'));
return;
}
@ -253,17 +237,13 @@ define(function(require) {
}
newKeyArmored = privKey.armor();
} catch (e) {
callback({
errMsg: 'Setting new passphrase failed!'
});
callback(new Error('Setting new passphrase failed!'));
return;
}
// check if new passphrase really works
if (!privKey.decrypt(newPassphrase)) {
callback({
errMsg: 'Decrypting key with new passphrase failed!'
});
callback(new Error('Decrypting key with new passphrase failed!'));
return;
}
@ -278,9 +258,7 @@ define(function(require) {
// check keys
if (!this._privateKey || publicKeysArmored.length < 1) {
callback({
errMsg: 'Error encrypting. Keys must be set!'
});
callback(new Error('Error encrypting. Keys must be set!'));
return;
}
@ -290,10 +268,7 @@ define(function(require) {
publicKeys = publicKeys.concat(openpgp.key.readArmored(pubkeyArmored).keys);
});
} catch (err) {
callback({
errMsg: 'Error encrypting plaintext!',
err: err
});
callback(new Error('Error encrypting plaintext!'));
return;
}
@ -309,9 +284,7 @@ define(function(require) {
// check keys
if (!this._privateKey || !publicKeyArmored) {
callback({
errMsg: 'Error decrypting. Keys must be set!'
});
callback(new Error('Error decrypting. Keys must be set!'));
return;
}
@ -320,10 +293,7 @@ define(function(require) {
publicKeys = openpgp.key.readArmored(publicKeyArmored).keys;
message = openpgp.message.readArmored(ciphertext);
} catch (err) {
callback({
errMsg: 'Error decrypting PGP message!',
err: err
});
callback(new Error('Error decrypting PGP message!'));
return;
}
@ -332,10 +302,7 @@ define(function(require) {
function onDecrypted(err, decrypted) {
if (err) {
callback({
errMsg: 'Error decrypting PGP message!',
err: err
});
callback(new Error('Error decrypting PGP message!'));
return;
}
@ -347,9 +314,7 @@ define(function(require) {
}
});
if (!signaturesValid) {
callback({
errMsg: 'Verifying PGP signature failed!'
});
callback(new Error('Verifying PGP signature failed!'));
return;
}

View File

@ -1,7 +1,7 @@
define(function(require) {
'use strict';
var util = require('cryptoLib/util'),
var util = require('js/crypto/util'),
_ = require('underscore'),
config = require('js/app-config').config,
str = require('js/app-config').string;
@ -11,14 +11,14 @@ define(function(require) {
* PGP de-/encryption, receiving via IMAP, sending via SMTP, MIME parsing, local db persistence
*
* @param {Object} keychain The keychain DAO handles keys transparently
* @param {Object} crypto Orchestrates decryption
* @param {Object} pgp Orchestrates decryption
* @param {Object} devicestorage Handles persistence to the local indexed db
* @param {Object} pgpbuilder Generates and encrypts MIME and SMTP messages
* @param {Object} mailreader Parses MIME messages received from IMAP
*/
var EmailDAO = function(keychain, crypto, devicestorage, pgpbuilder, mailreader) {
var EmailDAO = function(keychain, pgp, devicestorage, pgpbuilder, mailreader) {
this._keychain = keychain;
this._crypto = crypto;
this._pgp = pgp;
this._devicestorage = devicestorage;
this._pgpbuilder = pgpbuilder;
this._mailreader = mailreader;
@ -105,7 +105,7 @@ define(function(require) {
}
// no keypair for is stored for the user... generate a new one
self._crypto.generateKeys({
self._pgp.generateKeys({
emailAddress: self._account.emailAddress,
keySize: self._account.asymKeySize,
passphrase: options.passphrase
@ -121,8 +121,8 @@ define(function(require) {
function handleExistingKeypair(keypair) {
var privKeyParams, pubKeyParams;
try {
privKeyParams = self._crypto.getKeyParams(keypair.privateKey.encryptedKey);
pubKeyParams = self._crypto.getKeyParams(keypair.publicKey.publicKey);
privKeyParams = self._pgp.getKeyParams(keypair.privateKey.encryptedKey);
pubKeyParams = self._pgp.getKeyParams(keypair.publicKey.publicKey);
} catch (e) {
callback(new Error('Error reading key params!'));
return;
@ -148,7 +148,7 @@ define(function(require) {
}
// import existing key pair into crypto module
self._crypto.importKeys({
self._pgp.importKeys({
passphrase: options.passphrase,
privateKeyArmored: keypair.privateKey.encryptedKey,
publicKeyArmored: keypair.publicKey.publicKey
@ -159,14 +159,14 @@ define(function(require) {
}
// set decrypted privateKey to pgpMailer
self._pgpbuilder._privateKey = self._crypto._privateKey;
self._pgpbuilder._privateKey = self._pgp._privateKey;
callback();
});
}
function handleGenerated(generatedKeypair) {
// import the new key pair into crypto module
self._crypto.importKeys({
self._pgp.importKeys({
passphrase: options.passphrase,
privateKeyArmored: generatedKeypair.privateKeyArmored,
publicKeyArmored: generatedKeypair.publicKeyArmored
@ -196,7 +196,7 @@ define(function(require) {
}
// set decrypted privateKey to pgpMailer
self._pgpbuilder._privateKey = self._crypto._privateKey;
self._pgpbuilder._privateKey = self._pgp._privateKey;
callback();
});
});
@ -818,7 +818,7 @@ define(function(require) {
// get the receiver's public key to check the message signature
var encryptedNode = filterBodyParts(message.bodyParts, 'encrypted')[0];
self._crypto.decrypt(encryptedNode.content, senderPublicKey.publicKey, function(err, decrypted) {
self._pgp.decrypt(encryptedNode.content, senderPublicKey.publicKey, function(err, decrypted) {
if (err || !decrypted) {
showError(err.errMsg || err.message || 'An error occurred during the decryption.');
return;

View File

@ -5,13 +5,27 @@
define(function(require) {
'use strict';
var _ = require('underscore');
var _ = require('underscore'),
util = require('js/crypto/util'),
config = require('js/app-config').config;
var KeychainDAO = function(localDbDao, publicKeyDao) {
var DB_PUBLICKEY = 'publickey',
DB_PRIVATEKEY = 'privatekey',
DB_DEVICENAME = 'devicename',
DB_DEVICE_SECRET = 'devicesecret';
var KeychainDAO = function(localDbDao, publicKeyDao, privateKeyDao, crypto, pgp) {
this._localDbDao = localDbDao;
this._publicKeyDao = publicKeyDao;
this._privateKeyDao = privateKeyDao;
this._crypto = crypto;
this._pgp = pgp;
};
//
// Public key functions
//
/**
* Verifies the public key of a user o nthe public key store
* @param {String} uuid The uuid to verify the key
@ -170,7 +184,7 @@ define(function(require) {
var self = this;
// search local keyring for public key
self._localDbDao.list('publickey', 0, null, function(err, allPubkeys) {
self._localDbDao.list(DB_PUBLICKEY, 0, null, function(err, allPubkeys) {
if (err) {
callback(err);
return;
@ -234,6 +248,492 @@ define(function(require) {
}
};
//
// Device registration functions
//
/**
* Set the device's memorable name e.g 'iPhone Work'
* @param {String} deviceName The device name
* @param {Function} callback(error)
*/
KeychainDAO.prototype.setDeviceName = function(deviceName, callback) {
if (!deviceName) {
callback(new Error('Please set a device name!'));
return;
}
this._localDbDao.persist(DB_DEVICENAME, deviceName, callback);
};
/**
* Get the device' memorable name from local storage. Throws an error if not set
* @param {Function} callback(error, deviceName)
* @return {String} The device name
*/
KeychainDAO.prototype.getDeviceName = function(callback) {
// check if deviceName is already persisted in storage
this._localDbDao.read(DB_DEVICENAME, function(err, deviceName) {
if (err) {
callback(err);
return;
}
if (!deviceName) {
callback(new Error('Device name not set!'));
return;
}
callback(null, deviceName);
});
};
/**
* Geneate a device specific key and secret to authenticate to the private key service.
* @param {Function} callback(error, deviceSecret:[base64 encoded string])
*/
KeychainDAO.prototype.getDeviceSecret = function(callback) {
var self = this;
// generate random deviceSecret or get from storage
self._localDbDao.read(DB_DEVICE_SECRET, function(err, storedDevSecret) {
if (err) {
callback(err);
return;
}
if (storedDevSecret) {
// a device key is already available locally
callback(null, storedDevSecret);
return;
}
// generate random deviceSecret
var deviceSecret = util.random(config.symKeySize);
// persist deviceSecret to local storage (in plaintext)
self._localDbDao.persist(DB_DEVICE_SECRET, deviceSecret, function(err) {
if (err) {
callback(err);
return;
}
callback(null, deviceSecret);
});
});
};
/**
* Register the device on the private key server. This will give the device access to upload an encrypted private key.
* @param {String} options.userId The user's email address
* @param {Function} callback(error)
*/
KeychainDAO.prototype.registerDevice = function(options, callback) {
var self = this,
devName;
// check if deviceName is already persisted in storage
self.getDeviceName(function(err, deviceName) {
if (err) {
callback(err);
return;
}
requestDeviceRegistration(deviceName);
});
function requestDeviceRegistration(deviceName) {
devName = deviceName;
// request device registration session key
self._privateKeyDao.requestDeviceRegistration({
userId: options.userId,
deviceName: deviceName
}, function(err, regSessionKey) {
if (err) {
callback(err);
return;
}
if (!regSessionKey.encryptedRegSessionKey) {
callback(new Error('Invalid format for session key!'));
return;
}
decryptSessionKey(regSessionKey);
});
}
function decryptSessionKey(regSessionKey) {
self.lookupPublicKey(config.serverPrivateKeyId, function(err, serverPubkey) {
if (err) {
callback(err);
return;
}
if (!serverPubkey || !serverPubkey.publicKey) {
callback(new Error('Server public key for device registration not found!'));
return;
}
// decrypt the session key
var ct = regSessionKey.encryptedRegSessionKey;
self._pgp.decrypt(ct, serverPubkey.publicKey, function(err, decrypedSessionKey) {
if (err) {
callback(err);
return;
}
uploadDeviceSecret(decrypedSessionKey);
});
});
}
function uploadDeviceSecret(regSessionKey) {
// read device secret from local storage
self.getDeviceSecret(function(err, deviceSecret) {
if (err) {
callback(err);
return;
}
// generate iv
var iv = util.random(config.symIvSize);
// encrypt deviceSecret
self._crypto.encrypt(deviceSecret, regSessionKey, iv, function(err, encryptedDeviceSecret) {
if (err) {
callback(err);
return;
}
// upload encryptedDeviceSecret
self._privateKeyDao.uploadDeviceSecret({
userId: options.userId,
deviceName: devName,
encryptedDeviceSecret: encryptedDeviceSecret,
iv: iv
}, callback);
});
});
}
};
//
// Private key functions
//
/**
* Authenticate to the private key server (required before private PGP key upload).
* @param {String} userId The user's email address
* @param {Function} callback(error, authSessionKey)
* @return {Object} {sessionId:String, sessionKey:[base64 encoded]}
*/
KeychainDAO.prototype._authenticateToPrivateKeyServer = function(userId, callback) {
var self = this,
sessionId;
// request auth session key required for upload
self._privateKeyDao.requestAuthSessionKey({
userId: userId
}, function(err, authSessionKey) {
if (err) {
callback(err);
return;
}
if (!authSessionKey.encryptedAuthSessionKey || !authSessionKey.encryptedChallenge || !authSessionKey.sessionId) {
callback(new Error('Invalid format for session key!'));
return;
}
// remember session id for verification
sessionId = authSessionKey.sessionId;
decryptSessionKey(authSessionKey);
});
function decryptSessionKey(authSessionKey) {
self.lookupPublicKey(config.serverPrivateKeyId, function(err, serverPubkey) {
if (err) {
callback(err);
return;
}
if (!serverPubkey || !serverPubkey.publicKey) {
callback(new Error('Server public key for authentication not found!'));
return;
}
// decrypt the session key
var ct1 = authSessionKey.encryptedAuthSessionKey;
self._pgp.decrypt(ct1, serverPubkey.publicKey, function(err, decryptedSessionKey) {
if (err) {
callback(err);
return;
}
// decrypt the challenge
var ct2 = authSessionKey.encryptedChallenge;
self._pgp.decrypt(ct2, serverPubkey.publicKey, function(err, decryptedChallenge) {
if (err) {
callback(err);
return;
}
encryptChallenge(decryptedSessionKey, decryptedChallenge);
});
});
});
}
function encryptChallenge(sessionKey, challenge) {
// get device secret
self.getDeviceSecret(function(err, deviceSecret) {
if (err) {
callback(err);
return;
}
var iv = util.random(config.symIvSize);
// encrypt the challenge
self._crypto.encrypt(challenge, sessionKey, iv, function(err, encryptedChallenge) {
if (err) {
callback(err);
return;
}
// encrypt the device secret
self._crypto.encrypt(deviceSecret, sessionKey, iv, function(err, encryptedDeviceSecret) {
if (err) {
callback(err);
return;
}
replyChallenge({
encryptedChallenge: encryptedChallenge,
encryptedDeviceSecret: encryptedDeviceSecret,
iv: iv
}, sessionKey);
});
});
});
}
function replyChallenge(response, sessionKey) {
// respond to challenge by uploading the with the session key encrypted challenge
self._privateKeyDao.verifyAuthentication({
userId: userId,
sessionId: sessionId,
encryptedChallenge: response.encryptedChallenge,
encryptedDeviceSecret: response.encryptedDeviceSecret,
iv: response.iv
}, function(err) {
if (err) {
callback(err);
return;
}
callback(null, {
sessionId: sessionId,
sessionKey: sessionKey
});
});
}
};
/**
* Encrypt and upload the private PGP key to the server.
* @param {String} options.userId The user's email address
* @param {String} options.code The randomly generated or self selected code used to derive the key for the encryption of the private PGP key
* @param {Function} callback(error)
*/
KeychainDAO.prototype.uploadPrivateKey = function(options, callback) {
var self = this,
keySize = config.symKeySize,
salt;
if (!options.userId || !options.code) {
callback(new Error('Incomplete arguments!'));
return;
}
deriveKey(options.code);
function deriveKey(code) {
// generate random salt
salt = util.random(keySize);
// derive key from the code using PBKDF2
self._crypto.deriveKey(code, salt, keySize, function(err, key) {
if (err) {
callback(err);
return;
}
encryptPrivateKey(key);
});
}
function encryptPrivateKey(encryptionKey) {
// get private key from local storage
self.getUserKeyPair(options.userId, function(err, keypair) {
if (err) {
callback(err);
return;
}
var privkeyId = keypair.privateKey._id,
pgpBlock = keypair.privateKey.encryptedKey;
// encrypt the private key with the derived key
var iv = util.random(config.symIvSize);
self._crypto.encrypt(pgpBlock, encryptionKey, iv, function(err, ct) {
if (err) {
callback(err);
return;
}
var payload = {
_id: privkeyId,
userId: options.userId,
encryptedPrivateKey: ct,
salt: salt,
iv: iv
};
uploadPrivateKey(payload);
});
});
}
function uploadPrivateKey(payload) {
// authenticate to server for upload
self._authenticateToPrivateKeyServer(options.userId, function(err, authSessionKey) {
if (err) {
callback(err);
return;
}
// encrypt encryptedPrivateKey again using authSessionKey
var pt = payload.encryptedPrivateKey,
iv = payload.iv,
key = authSessionKey.sessionKey;
self._crypto.encrypt(pt, key, iv, function(err, ct) {
if (err) {
callback(err);
return;
}
// replace the encryptedPrivateKey with the double wrapped ciphertext
payload.encryptedPrivateKey = ct;
// set sessionId
payload.sessionId = authSessionKey.sessionId;
// upload the encrypted priavet key
self._privateKeyDao.upload(payload, callback);
});
});
}
};
/**
* Request downloading the user's encrypted private key. This will initiate the server to send the recovery token via email/sms to the user.
* @param {String} options.userId The user's email address
* @param {String} options.keyId The private PGP key id
* @param {Function} callback(error)
*/
KeychainDAO.prototype.requestPrivateKeyDownload = function(options, callback) {
this._privateKeyDao.requestDownload(options, callback);
};
/**
* Download the encrypted private PGP key from the server using the recovery token.
* @param {String} options.userId The user's email address
* @param {String} options.keyId The user's email address
* @param {String} options.recoveryToken The recovery token acquired via email/sms from the key server
* @param {Function} callback(error, encryptedPrivateKey)
*/
KeychainDAO.prototype.downloadPrivateKey = function(options, callback) {
this._privateKeyDao.download(options, callback);
};
/**
* This is called after the encrypted private key has successfully been downloaded and it's ready to be decrypted and stored in localstorage.
* @param {String} options._id The private PGP key id
* @param {String} options.userId The user's email address
* @param {String} options.code The randomly generated or self selected code used to derive the key for the decryption of the private PGP key
* @param {String} options.encryptedPrivateKey The encrypted private PGP key
* @param {String} options.salt The salt required to derive the code derived key
* @param {String} options.iv The iv used to encrypt the private PGP key
* @param {Function} callback(error, keyObject)
*/
KeychainDAO.prototype.decryptAndStorePrivateKeyLocally = function(options, callback) {
var self = this,
code = options.code,
salt = options.salt,
keySize = config.symKeySize;
if (!options._id || !options.userId || !options.code || !options.salt || !options.encryptedPrivateKey || !options.iv) {
callback(new Error('Incomplete arguments!'));
return;
}
// derive key from the code and the salt using PBKDF2
self._crypto.deriveKey(code, salt, keySize, function(err, key) {
if (err) {
callback(err);
return;
}
decryptAndStore(key);
});
function decryptAndStore(derivedKey) {
// decrypt the private key with the derived key
var ct = options.encryptedPrivateKey,
iv = options.iv;
self._crypto.decrypt(ct, derivedKey, iv, function(err, privateKeyArmored) {
if (err) {
callback(new Error('Invalid keychain code!'));
return;
}
// validate pgp key
var keyParams;
try {
keyParams = self._pgp.getKeyParams(privateKeyArmored);
} catch (e) {
callback(new Error('Error parsing private PGP key!'));
return;
}
if (keyParams._id !== options._id || keyParams.userId !== options.userId) {
callback(new Error('Private key parameters don\'t match with public key\'s!'));
return;
}
var keyObject = {
_id: options._id,
userId: options.userId,
encryptedKey: privateKeyArmored
};
// store private key locally
self.saveLocalPrivateKey(keyObject, function(err) {
if (err) {
callback(err);
return;
}
callback(null, keyObject);
});
});
}
};
//
// Keypair functions
//
/**
* Gets the local user's key either from local storage
* or fetches it from the cloud. The private key is encrypted.
@ -244,7 +744,7 @@ define(function(require) {
var self = this;
// search for user's public key locally
self._localDbDao.list('publickey', 0, null, function(err, allPubkeys) {
self._localDbDao.list(DB_PUBLICKEY, 0, null, function(err, allPubkeys) {
if (err) {
callback(err);
return;
@ -364,7 +864,7 @@ define(function(require) {
}
// lookup in local storage
self._localDbDao.read('publickey_' + id, function(err, pubkey) {
self._localDbDao.read(DB_PUBLICKEY + '_' + id, function(err, pubkey) {
if (err) {
callback(err);
return;
@ -400,29 +900,29 @@ define(function(require) {
*/
KeychainDAO.prototype.listLocalPublicKeys = function(callback) {
// search local keyring for public key
this._localDbDao.list('publickey', 0, null, callback);
this._localDbDao.list(DB_PUBLICKEY, 0, null, callback);
};
KeychainDAO.prototype.removeLocalPublicKey = function(id, callback) {
this._localDbDao.remove('publickey_' + id, callback);
this._localDbDao.remove(DB_PUBLICKEY + '_' + id, callback);
};
KeychainDAO.prototype.lookupPrivateKey = function(id, callback) {
// lookup in local storage
this._localDbDao.read('privatekey_' + id, callback);
this._localDbDao.read(DB_PRIVATEKEY + '_' + id, callback);
};
KeychainDAO.prototype.saveLocalPublicKey = function(pubkey, callback) {
// persist public key (email, _id)
var pkLookupKey = 'publickey_' + pubkey._id;
var pkLookupKey = DB_PUBLICKEY + '_' + pubkey._id;
this._localDbDao.persist(pkLookupKey, pubkey, callback);
};
KeychainDAO.prototype.saveLocalPrivateKey = function(privkey, callback) {
// persist private key (email, _id)
var prkLookupKey = 'privatekey_' + privkey._id;
var prkLookupKey = DB_PRIVATEKEY + '_' + privkey._id;
this._localDbDao.persist(prkLookupKey, privkey, callback);
};
return KeychainDAO;
});
});

View File

@ -0,0 +1,170 @@
define(function() {
'use strict';
var PrivateKeyDAO = function(restDao) {
this._restDao = restDao;
};
//
// Device registration functions
//
/**
* Request registration of a new device by fetching registration session key.
* @param {String} options.userId The user's email address
* @param {String} options.deviceName The device's memorable name
* @param {Function} callback(error, regSessionKey)
* @return {Object} {encryptedRegSessionKey:[base64]}
*/
PrivateKeyDAO.prototype.requestDeviceRegistration = function(options, callback) {
var uri;
if (!options.userId || !options.deviceName) {
callback(new Error('Incomplete arguments!'));
return;
}
uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName;
this._restDao.post(undefined, uri, callback);
};
/**
* Authenticate device registration by uploading the deviceSecret encrypted with the regSessionKeys.
* @param {String} options.userId The user's email address
* @param {String} options.deviceName The device's memorable name
* @param {String} options.encryptedDeviceSecret The base64 encoded encrypted device secret
* @param {String} options.iv The iv used for encryption
* @param {Function} callback(error)
*/
PrivateKeyDAO.prototype.uploadDeviceSecret = function(options, callback) {
var uri;
if (!options.userId || !options.deviceName || !options.encryptedDeviceSecret || !options.iv) {
callback(new Error('Incomplete arguments!'));
return;
}
uri = '/device/user/' + options.userId + '/devicename/' + options.deviceName;
this._restDao.put(options, uri, callback);
};
//
// Private key functions
//
/**
* Request authSessionKeys required for upload the encrypted private PGP key.
* @param {String} options.userId The user's email address
* @param {Function} callback(error, authSessionKey)
* @return {Object} {sessionId, encryptedAuthSessionKey:[base64 encoded], encryptedChallenge:[base64 encoded]}
*/
PrivateKeyDAO.prototype.requestAuthSessionKey = function(options, callback) {
var uri;
if (!options.userId) {
callback(new Error('Incomplete arguments!'));
return;
}
uri = '/auth/user/' + options.userId;
this._restDao.post(undefined, uri, callback);
};
/**
* Verifiy authentication by uploading the challenge and deviceSecret encrypted with the authSessionKeys as a response.
* @param {String} options.userId The user's email address
* @param {String} options.encryptedChallenge The server's base64 encoded challenge encrypted using the authSessionKey
* @param {String} options.encryptedDeviceSecret The server's base64 encoded deviceSecret encrypted using the authSessionKey
* @param {String} options.iv The iv used for encryption
* @param {Function} callback(error)
*/
PrivateKeyDAO.prototype.verifyAuthentication = function(options, callback) {
var uri;
if (!options.userId || !options.sessionId || !options.encryptedChallenge || !options.encryptedDeviceSecret || !options.iv) {
callback(new Error('Incomplete arguments!'));
return;
}
uri = '/auth/user/' + options.userId + '/session/' + options.sessionId;
this._restDao.put(options, uri, callback);
};
/**
* Upload the encrypted private PGP key.
* @param {String} options._id The hex encoded capital 16 char key id
* @param {String} options.userId The user's email address
* @param {String} options.encryptedPrivateKey The base64 encoded encrypted private PGP key
* @param {String} options.sessionId The session id
* @param {Function} callback(error)
*/
PrivateKeyDAO.prototype.upload = function(options, callback) {
var uri;
if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.sessionId || !options.salt || !options.iv) {
callback(new Error('Incomplete arguments!'));
return;
}
uri = '/privatekey/user/' + options.userId + '/session/' + options.sessionId;
this._restDao.post(options, uri, callback);
};
/**
* Request download for the encrypted private PGP key.
* @param {String} options.userId The user's email address
* @param {String} options.keyId The private PGP key id
* @param {Function} callback(error, found)
* @return {Boolean} weather the key was found on the server or not.
*/
PrivateKeyDAO.prototype.requestDownload = function(options, callback) {
var uri;
if (!options.userId || !options.keyId) {
callback(new Error('Incomplete arguments!'));
return;
}
uri = '/privatekey/user/' + options.userId + '/key/' + options.keyId;
this._restDao.get({
uri: uri
}, function(err) {
// 404: there is no encrypted private key on the server
if (err && err.code !== 200) {
callback(null, false);
return;
}
if (err) {
callback(err);
return;
}
callback(null, true);
});
};
/**
* Verify the download request for the private PGP key using the recovery token sent via email. This downloads the actual encrypted private key.
* @param {String} options.userId The user's email address
* @param {String} options.keyId The private key id
* @param {String} options.recoveryToken The token proving the user own the email account
* @param {Function} callback(error, encryptedPrivateKey)
* @return {Object} {_id:[hex encoded capital 16 char key id], encryptedPrivateKey:[base64 encoded], encryptedUserId: [base64 encoded]}
*/
PrivateKeyDAO.prototype.download = function(options, callback) {
var uri;
if (!options.userId || !options.keyId || !options.recoveryToken) {
callback(new Error('Incomplete arguments!'));
return;
}
uri = '/privatekey/user/' + options.userId + '/key/' + options.keyId + '/recovery/' + options.recoveryToken;
this._restDao.get({
uri: uri
}, callback);
};
return PrivateKeyDAO;
});

View File

@ -17,7 +17,48 @@ define(function(require) {
* @param {String} options.type (optional) The type of data that you're expecting back from the server: json, xml, text. Default: json.
*/
RestDAO.prototype.get = function(options, callback) {
var xhr, acceptHeader;
options.method = 'GET';
this._processRequest(options, callback);
};
/**
* POST (create) request
*/
RestDAO.prototype.post = function(item, uri, callback) {
this._processRequest({
method: 'POST',
payload: item,
uri: uri
}, callback);
};
/**
* PUT (update) request
*/
RestDAO.prototype.put = function(item, uri, callback) {
this._processRequest({
method: 'PUT',
payload: item,
uri: uri
}, callback);
};
/**
* DELETE (remove) request
*/
RestDAO.prototype.remove = function(uri, callback) {
this._processRequest({
method: 'DELETE',
uri: uri
}, callback);
};
//
// helper functions
//
RestDAO.prototype._processRequest = function(options, callback) {
var xhr, format;
if (typeof options.uri === 'undefined') {
callback({
@ -30,11 +71,11 @@ define(function(require) {
options.type = options.type || 'json';
if (options.type === 'json') {
acceptHeader = 'application/json';
format = 'application/json';
} else if (options.type === 'xml') {
acceptHeader = 'application/xml';
format = 'application/xml';
} else if (options.type === 'text') {
acceptHeader = 'text/plain';
format = 'text/plain';
} else {
callback({
code: 400,
@ -44,14 +85,16 @@ define(function(require) {
}
xhr = new XMLHttpRequest();
xhr.open('GET', this._baseUri + options.uri);
xhr.setRequestHeader('Accept', acceptHeader);
xhr.open(options.method, this._baseUri + options.uri);
xhr.setRequestHeader('Accept', format);
xhr.setRequestHeader('Content-Type', format);
xhr.onload = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
var res;
var res;
if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 201 || xhr.status === 304)) {
if (options.type === 'json') {
res = JSON.parse(xhr.responseText);
res = xhr.responseText ? JSON.parse(xhr.responseText) : xhr.responseText;
} else {
res = xhr.responseText;
}
@ -69,72 +112,11 @@ define(function(require) {
xhr.onerror = function() {
callback({
code: 42,
errMsg: 'Error calling GET on ' + options.uri
errMsg: 'Error calling ' + options.method + ' on ' + options.uri
});
};
xhr.send();
};
/**
* PUT (create/update) request
*/
RestDAO.prototype.put = function(item, uri, callback) {
var xhr;
xhr = new XMLHttpRequest();
xhr.open('PUT', this._baseUri + uri);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 201 || xhr.status === 304)) {
callback(null, xhr.responseText, xhr.status);
return;
}
callback({
code: xhr.status,
errMsg: xhr.statusText
});
};
xhr.onerror = function() {
callback({
errMsg: 'Error calling PUT on ' + uri
});
};
xhr.send(JSON.stringify(item));
};
/**
* DELETE (remove) request
*/
RestDAO.prototype.remove = function(uri, callback) {
var xhr;
xhr = new XMLHttpRequest();
xhr.open('DELETE', this._baseUri + uri);
xhr.onload = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
callback(null, xhr.responseText, xhr.status);
return;
}
callback({
code: xhr.status,
errMsg: xhr.statusText
});
};
xhr.onerror = function() {
callback({
errMsg: 'Error calling DELETE on ' + uri
});
};
xhr.send();
xhr.send(options.payload ? JSON.stringify(options.payload) : undefined);
};
return RestDAO;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
/*! OpenPGPjs.org this is LGPL licensed code, see LICENSE/our website for more information.- v0.5.1 - 2014-04-03 */!function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);throw new Error("Cannot find module '"+g+"'")}var j=c[g]={exports:{}};b[g][0].call(j.exports,function(a){var c=b[g][1][a];return e(c?c:a)},j,j.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g<d.length;g++)e(d[g]);return e}({1:[function(){function a(a){window.openpgp.crypto.random.randomBuffer.size<d&&postMessage({event:"request-seed"}),postMessage(a)}function b(a){var b=window.openpgp.packet.List.fromStructuredClone(a);return new window.openpgp.key.Key(b)}function c(a){var b=window.openpgp.packet.List.fromStructuredClone(a);return new window.openpgp.message.Message(b)}window={},importScripts("openpgp.min.js");var d=4e4,e=6e4;window.openpgp.crypto.random.randomBuffer.init(e),onmessage=function(d){var e=null,f=null,g=d.data,h=!1;switch(g.event){case"seed-random":g.buf instanceof Uint8Array||(g.buf=new Uint8Array(g.buf)),window.openpgp.crypto.random.randomBuffer.set(g.buf);break;case"encrypt-message":try{g.keys=g.keys.map(b),e=window.openpgp.encryptMessage(g.keys,g.text)}catch(i){f=i.message}a({event:"method-return",data:e,err:f});break;case"sign-and-encrypt-message":try{g.publicKeys=g.publicKeys.map(b),g.privateKey=b(g.privateKey),e=window.openpgp.signAndEncryptMessage(g.publicKeys,g.privateKey,g.text)}catch(i){f=i.message}a({event:"method-return",data:e,err:f});break;case"decrypt-message":try{g.privateKey=b(g.privateKey),g.message=c(g.message.packets),e=window.openpgp.decryptMessage(g.privateKey,g.message)}catch(i){f=i.message}a({event:"method-return",data:e,err:f});break;case"decrypt-and-verify-message":try{g.privateKey=b(g.privateKey),g.publicKeys=g.publicKeys.map(b),g.message=c(g.message.packets),e=window.openpgp.decryptAndVerifyMessage(g.privateKey,g.publicKeys,g.message)}catch(i){f=i.message}a({event:"method-return",data:e,err:f});break;case"sign-clear-message":try{g.privateKeys=g.privateKeys.map(b),e=window.openpgp.signClearMessage(g.privateKeys,g.text)}catch(i){f=i.message}a({event:"method-return",data:e,err:f});break;case"verify-clear-signed-message":try{g.publicKeys=g.publicKeys.map(b);var j=window.openpgp.packet.List.fromStructuredClone(g.message.packets);g.message=new window.openpgp.cleartext.CleartextMessage(g.message.text,j),e=window.openpgp.verifyClearSignedMessage(g.publicKeys,g.message)}catch(i){f=i.message}a({event:"method-return",data:e,err:f});break;case"generate-key-pair":try{e=window.openpgp.generateKeyPair(g.keyType,g.numBits,g.userId,g.passphrase),e.key=e.key.toPacketlist()}catch(i){f=i.message}a({event:"method-return",data:e,err:f});break;case"decrypt-key":try{g.privateKey=b(g.privateKey),h=g.privateKey.decrypt(g.password),h?e=g.privateKey.toPacketlist():f="Wrong password"}catch(i){f=i.message}a({event:"method-return",data:e,err:f});break;case"decrypt-key-packet":try{g.privateKey=b(g.privateKey),g.keyIds=g.keyIds.map(window.openpgp.Keyid.fromClone),h=g.privateKey.decryptKeyPacket(g.keyIds,g.password),h?e=g.privateKey.toPacketlist():f="Wrong password"}catch(i){f=i.message}a({event:"method-return",data:e,err:f});break;default:throw new Error("Unknown Worker Event.")}}},{}]},{},[1]);
/*! OpenPGPjs.org this is LGPL licensed code, see LICENSE/our website for more information.- v0.6.0 - 2014-05-09 */!function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);throw new Error("Cannot find module '"+g+"'")}var j=c[g]={exports:{}};b[g][0].call(j.exports,function(a){var c=b[g][1][a];return e(c?c:a)},j,j.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g<d.length;g++)e(d[g]);return e}({1:[function(){function a(a){window.openpgp.crypto.random.randomBuffer.size<d&&postMessage({event:"request-seed"}),postMessage(a)}function b(a){var b=window.openpgp.packet.List.fromStructuredClone(a);return new window.openpgp.key.Key(b)}function c(a){var b=window.openpgp.packet.List.fromStructuredClone(a);return new window.openpgp.message.Message(b)}window={},importScripts("openpgp.min.js");var d=4e4,e=6e4;window.openpgp.crypto.random.randomBuffer.init(e),onmessage=function(d){var e=null,f=null,g=d.data,h=!1;switch(g.event){case"seed-random":g.buf instanceof Uint8Array||(g.buf=new Uint8Array(g.buf)),window.openpgp.crypto.random.randomBuffer.set(g.buf);break;case"encrypt-message":try{g.keys=g.keys.map(b),e=window.openpgp.encryptMessage(g.keys,g.text)}catch(i){f=i.message}a({event:"method-return",data:e,err:f});break;case"sign-and-encrypt-message":try{g.publicKeys=g.publicKeys.map(b),g.privateKey=b(g.privateKey),e=window.openpgp.signAndEncryptMessage(g.publicKeys,g.privateKey,g.text)}catch(i){f=i.message}a({event:"method-return",data:e,err:f});break;case"decrypt-message":try{g.privateKey=b(g.privateKey),g.message=c(g.message.packets),e=window.openpgp.decryptMessage(g.privateKey,g.message)}catch(i){f=i.message}a({event:"method-return",data:e,err:f});break;case"decrypt-and-verify-message":try{g.privateKey=b(g.privateKey),g.publicKeys=g.publicKeys.map(b),g.message=c(g.message.packets),e=window.openpgp.decryptAndVerifyMessage(g.privateKey,g.publicKeys,g.message)}catch(i){f=i.message}a({event:"method-return",data:e,err:f});break;case"sign-clear-message":try{g.privateKeys=g.privateKeys.map(b),e=window.openpgp.signClearMessage(g.privateKeys,g.text)}catch(i){f=i.message}a({event:"method-return",data:e,err:f});break;case"verify-clear-signed-message":try{g.publicKeys=g.publicKeys.map(b);var j=window.openpgp.packet.List.fromStructuredClone(g.message.packets);g.message=new window.openpgp.cleartext.CleartextMessage(g.message.text,j),e=window.openpgp.verifyClearSignedMessage(g.publicKeys,g.message)}catch(i){f=i.message}a({event:"method-return",data:e,err:f});break;case"generate-key-pair":try{e=window.openpgp.generateKeyPair(g.options),e.key=e.key.toPacketlist()}catch(i){f=i.message}a({event:"method-return",data:e,err:f});break;case"decrypt-key":try{g.privateKey=b(g.privateKey),h=g.privateKey.decrypt(g.password),h?e=g.privateKey.toPacketlist():f="Wrong password"}catch(i){f=i.message}a({event:"method-return",data:e,err:f});break;case"decrypt-key-packet":try{g.privateKey=b(g.privateKey),g.keyIds=g.keyIds.map(window.openpgp.Keyid.fromClone),h=g.privateKey.decryptKeyPacket(g.keyIds,g.password),h?e=g.privateKey.toPacketlist():f="Wrong password"}catch(i){f=i.message}a({event:"method-return",data:e,err:f});break;default:throw new Error("Unknown Worker Event.")}}},{}]},{},[1]);

View File

@ -7,7 +7,6 @@
paths: {
js: '../js',
test: '../../test',
cryptoLib: '../js/crypto',
underscore: 'underscore/underscore-min',
lawnchair: 'lawnchair/lawnchair-git',
lawnchairSQL: 'lawnchair/lawnchair-adapter-webkit-sqlite-git',

View File

@ -32,6 +32,7 @@
@import "views/shared";
@import "views/add-account";
@import "views/account";
@import "views/privatekey-upload";
@import "views/set-passphrase";
@import "views/contacts";
@import "views/about";

View File

@ -1,8 +1,12 @@
.view-account {
a {
color: $color-blue;
}
table {
margin: 50px auto 60px auto;
td {
padding-top: 15px;

View File

@ -119,4 +119,15 @@
margin-right: 10px;
}
}
}
.view-login-privatekey-download {
.content {
max-width: 500px;
input.code {
margin-right: 0;
width: auto;
}
}
}

View File

@ -66,7 +66,6 @@
flex-shrink: 0;
position: relative;
height: 28px;
cursor: pointer;
padding: 0 $nav-padding;
background: $color-white;
width: 100%;

View File

@ -0,0 +1,17 @@
.view-privatekey-upload {
a {
color: $color-blue;
}
.step {
margin: 20px 0;
.working {
text-align: center;
font-size: 30px;
margin: 70px;
}
}
}

View File

@ -15,7 +15,7 @@
</tr>
<tr>
<td>PGP Key ID</td>
<td>{{keyId}}</td>
<td>{{keyId}} (<a href="https://whiteout.io/revocation.html" title="Click here to reset your account." target="_blank">Revoke key</a>)</td>
</tr>
<tr>
<td>PGP Fingerprint</td>

View File

@ -31,6 +31,10 @@
<div class="lightbox" ng-include="'tpl/set-passphrase.html'"></div>
</div>
<div class="lightbox-overlay" ng-class="{'show': state.lightbox === 'privatekey-upload'}">
<div class="lightbox" ng-include="'tpl/privatekey-upload.html'"></div>
</div>
<div class="lightbox-overlay" ng-class="{'show': state.lightbox === 'contacts'}">
<div class="lightbox" ng-include="'tpl/contacts.html'"></div>
</div>

View File

@ -0,0 +1,30 @@
<div class="view-login view-login-privatekey-download">
<div class="logo">
<img src="img/whiteout_logo.svg" alt="whiteout.io">
</div><!--/logo-->
<div class="content">
<div class="step" ng-show="step === 1">
<p><b>Key sync.</b> We have sent you an email containing a recovery token. Please copy and paste the identifier below.</p>
<input type="text" class="input-text" size="42" ng-model="recoveryToken" placeholder="Recovery token" focus-me="step === 1">
</div>
<div class="step" ng-show="step === 2">
<p><b>Key sync.</b> Please enter the keychain code you wrote down during sync setup.</p>
<input type="text" class="input-text code" size="4" maxlength="4" ng-model="code0" focus-me="step === 2"> -
<input type="text" class="input-text code" size="4" maxlength="4" ng-model="code1"> -
<input type="text" class="input-text code" size="4" maxlength="4" ng-model="code2"> -
<input type="text" class="input-text code" size="4" maxlength="4" ng-model="code3"> -
<input type="text" class="input-text code" size="4" maxlength="4" ng-model="code4"> -
<input type="text" class="input-text code" size="4" maxlength="4" ng-model="code5">
<!--<a href="https://whiteout.io/revocation.html" title="Click here to reset your account." target="_blank">Lost your keychain code?</a>-->
</div>
<div>
<button class="btn" ng-click="goForward()">Continue</button>
</div>
</div><!--/content-->
</div>

View File

@ -17,6 +17,7 @@
<ul class="nav-secondary">
<li><a href="#" ng-click="state.account.toggle(true); $event.preventDefault()">Account</a></li>
<li><a href="#" ng-click="state.contacts.toggle(true); $event.preventDefault()">Contacts</a></li>
<li><a href="#" ng-click="state.privateKeyUpload.toggle(true); $event.preventDefault()">Key sync (experimental)</a></li>
<li><a href="#" ng-click="state.about.toggle(true); $event.preventDefault()">About</a></li>
</ul>

View File

@ -0,0 +1,46 @@
<div class="lightbox-body" ng-controller="PrivateKeyUploadCtrl">
<header>
<h2>Setup Key Sync</h2>
<button class="close" ng-click="state.privateKeyUpload.toggle(false)" data-action="lightbox-close">&#xe007;</button>
</header>
<div class="content">
<div class="dialog view-privatekey-upload">
<div class="step" ng-show="step === 1">
<p>Your keychain code can be used to securely backup and sync your PGP key between devices. This feature is experimental and not recommended for production use. <a href="" target="_blank">Learn more</a>.</p>
<h2>{{displayedCode}}</h2>
<p>Please write down your keychain code and keep it in a safe place. Whiteout Networks cannot recover a lost code.</p>
</div>
<div class="step" ng-show="step === 2">
<p>Please confirm the keychain code you have written down.</p>
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code0" focus-me="step === 2"> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code1"> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code2"> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code3"> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code4"> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code5">
</div>
<div class="step" ng-show="step === 3">
<p>Please enter a memorable name for this device e.g. "MacBook Work".</p>
<input type="text" class="input-text" ng-model="deviceName" placeholder="Device name" focus-me="step === 3">
</div>
<div class="step" ng-show="step === 4">
<div class="working">
<span class="spinner"></span>
</div><!--/.working-->
</div>
<div class="control">
<button ng-show="step > 1 && step < 4" class="btn btn-alt" ng-click="goBack()">Go back</button>
<button ng-show="step < 4" class="btn" ng-click="goForward()">Continue</button>
</div>
</div><!-- /.view-privatekey-upload -->
</div><!-- /.content -->
</div><!-- /.lightbox-body -->

View File

@ -1,105 +0,0 @@
define(function(require) {
'use strict';
var LawnchairDAO = require('js/dao/lawnchair-dao'),
DeviceStorageDAO = require('js/dao/devicestorage-dao'),
expect = chai.expect;
var testUser = 'test@example.com';
describe('Device Storage DAO unit tests', function() {
var storageDao, lawnchairDaoStub;
beforeEach(function() {
lawnchairDaoStub = sinon.createStubInstance(LawnchairDAO);
storageDao = new DeviceStorageDAO(lawnchairDaoStub);
});
afterEach(function() {});
describe('init', function() {
it('should work', function(done) {
lawnchairDaoStub.init.yields();
storageDao.init(testUser, function(err) {
expect(err).to.not.exist;
expect(lawnchairDaoStub.init.calledOnce).to.be.true;
done();
});
});
});
describe('store list', function() {
it('should fail', function(done) {
var list = [{}];
storageDao.storeList(list, '', function(err) {
expect(err).to.exist;
done();
});
});
it('should work with empty list', function(done) {
var list = [];
storageDao.storeList(list, 'email', function(err) {
expect(err).to.not.exist;
done();
});
});
it('should work', function(done) {
lawnchairDaoStub.batch.yields();
var list = [{
foo: 'bar'
}];
storageDao.storeList(list, 'email', function(err) {
expect(err).to.not.exist;
expect(lawnchairDaoStub.batch.calledOnce).to.be.true;
done();
});
});
});
describe('remove list', function() {
it('should work', function(done) {
lawnchairDaoStub.removeList.yields();
storageDao.removeList('email', function(err) {
expect(err).to.not.exist;
expect(lawnchairDaoStub.removeList.calledOnce).to.be.true;
done();
});
});
});
describe('list items', function() {
it('should work', function(done) {
lawnchairDaoStub.list.yields();
storageDao.listItems('email', 0, null, function(err) {
expect(err).to.not.exist;
expect(lawnchairDaoStub.list.calledOnce).to.be.true;
done();
});
});
});
describe('clear', function() {
it('should work', function(done) {
lawnchairDaoStub.clear.yields();
storageDao.clear(function(err) {
expect(err).to.not.exist;
expect(lawnchairDaoStub.clear.calledOnce).to.be.true;
done();
});
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html style="overflow-y: auto">
<head>
<meta charset="utf-8">
<title>JavaScript Unit Tests</title>
<link rel="stylesheet" href="../lib/mocha.css" />
</head>
<body>
<div id="mocha"></div>
<script src="../lib/chai.js"></script>
<script src="../lib/sinon.js"></script>
<script src="../lib/mocha.js"></script>
<script data-main="main.js" src="../../src/lib/require.js"></script>
</body>
</html>

View File

@ -1,625 +0,0 @@
define(function(require) {
'use strict';
var LawnchairDAO = require('js/dao/lawnchair-dao'),
PublicKeyDAO = require('js/dao/publickey-dao'),
KeychainDAO = require('js/dao/keychain-dao'),
expect = chai.expect;
var testUser = 'test@example.com';
describe('Keychain DAO unit tests', function() {
var keychainDao, lawnchairDaoStub, pubkeyDaoStub;
beforeEach(function() {
lawnchairDaoStub = sinon.createStubInstance(LawnchairDAO);
pubkeyDaoStub = sinon.createStubInstance(PublicKeyDAO);
keychainDao = new KeychainDAO(lawnchairDaoStub, pubkeyDaoStub);
});
afterEach(function() {});
describe('verify public key', function() {
it('should verify public key', function(done) {
var uuid = 'asdfasdfasdfasdf';
pubkeyDaoStub.verify.yields();
keychainDao.verifyPublicKey(uuid, function() {
expect(pubkeyDaoStub.verify.calledWith(uuid)).to.be.true;
done();
});
});
});
describe('listLocalPublicKeys', function() {
it('should work', function(done) {
lawnchairDaoStub.list.withArgs('publickey', 0, null).yields();
keychainDao.listLocalPublicKeys(function() {
expect(lawnchairDaoStub.list.callCount).to.equal(1);
done();
});
});
});
describe('removeLocalPublicKey', function() {
it('should work', function(done) {
var id = 'asdf';
lawnchairDaoStub.remove.withArgs('publickey_' + id).yields();
keychainDao.removeLocalPublicKey(id, function() {
expect(lawnchairDaoStub.remove.callCount).to.equal(1);
done();
});
});
});
describe('refreshKeyForUserId', function() {
var getPubKeyStub,
oldKey = {
_id: 123
},
newKey = {
_id: 456
},
importedKey = {
_id: 789,
imported: true
};
beforeEach(function() {
getPubKeyStub = sinon.stub(keychainDao, 'getReceiverPublicKey');
});
afterEach(function() {
keychainDao.getReceiverPublicKey.restore();
delete keychainDao.requestPermissionForKeyUpdate;
});
it('should not find a key', function(done) {
getPubKeyStub.yields();
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.not.exist;
expect(key).to.not.exist;
done();
});
});
it('should not update the key when up to date', function(done) {
getPubKeyStub.yields(null, oldKey);
pubkeyDaoStub.get.withArgs(oldKey._id).yields(null, oldKey);
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.not.exist;
expect(key).to.to.equal(oldKey);
expect(getPubKeyStub.calledOnce).to.be.true;
expect(pubkeyDaoStub.get.calledOnce).to.be.true;
done();
});
});
it('should update key', function(done) {
getPubKeyStub.yields(null, oldKey);
pubkeyDaoStub.get.withArgs(oldKey._id).yields();
pubkeyDaoStub.getByUserId.withArgs(testUser).yields(null, newKey);
keychainDao.requestPermissionForKeyUpdate = function(opts, cb) {
expect(opts.userId).to.equal(testUser);
expect(opts.newKey).to.equal(newKey);
cb(true);
};
lawnchairDaoStub.remove.withArgs('publickey_' + oldKey._id).yields();
lawnchairDaoStub.persist.withArgs('publickey_' + newKey._id, newKey).yields();
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.not.exist;
expect(key).to.equal(newKey);
expect(getPubKeyStub.calledOnce).to.be.true;
expect(pubkeyDaoStub.get.calledOnce).to.be.true;
expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true;
expect(lawnchairDaoStub.remove.calledOnce).to.be.true;
expect(lawnchairDaoStub.persist.calledOnce).to.be.true;
done();
});
});
it('should remove key', function(done) {
getPubKeyStub.yields(null, oldKey);
pubkeyDaoStub.get.withArgs(oldKey._id).yields();
pubkeyDaoStub.getByUserId.withArgs(testUser).yields();
keychainDao.requestPermissionForKeyUpdate = function(opts, cb) {
expect(opts.userId).to.equal(testUser);
expect(opts.newKey).to.not.exist;
cb(true);
};
lawnchairDaoStub.remove.withArgs('publickey_' + oldKey._id).yields();
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.not.exist;
expect(key).to.not.exist;
expect(getPubKeyStub.calledOnce).to.be.true;
expect(pubkeyDaoStub.get.calledOnce).to.be.true;
expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true;
expect(lawnchairDaoStub.remove.calledOnce).to.be.true;
expect(lawnchairDaoStub.persist.called).to.be.false;
done();
});
});
it('should go offline while fetching new key', function(done) {
getPubKeyStub.yields(null, oldKey);
pubkeyDaoStub.get.withArgs(oldKey._id).yields();
pubkeyDaoStub.getByUserId.withArgs(testUser).yields({
code: 42
});
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.not.exist;
expect(key).to.to.equal(oldKey);
expect(getPubKeyStub.calledOnce).to.be.true;
expect(pubkeyDaoStub.get.calledOnce).to.be.true;
expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true;
expect(lawnchairDaoStub.remove.called).to.be.false;
expect(lawnchairDaoStub.persist.called).to.be.false;
done();
});
});
it('should not remove old key on user rejection', function(done) {
getPubKeyStub.yields(null, oldKey);
pubkeyDaoStub.get.withArgs(oldKey._id).yields();
pubkeyDaoStub.getByUserId.withArgs(testUser).yields(null, newKey);
keychainDao.requestPermissionForKeyUpdate = function(opts, cb) {
expect(opts.userId).to.equal(testUser);
expect(opts.newKey).to.exist;
cb(false);
};
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.not.exist;
expect(key).to.equal(oldKey);
expect(getPubKeyStub.calledOnce).to.be.true;
expect(pubkeyDaoStub.get.calledOnce).to.be.true;
expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true;
expect(lawnchairDaoStub.remove.called).to.be.false;
expect(lawnchairDaoStub.persist.called).to.be.false;
done();
});
});
it('should not remove manually imported key', function(done) {
getPubKeyStub.yields(null, importedKey);
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.not.exist;
expect(key).to.equal(importedKey);
expect(getPubKeyStub.calledOnce).to.be.true;
expect(pubkeyDaoStub.get.calledOnce).to.be.false;
done();
});
});
it('should update not the key when offline', function(done) {
getPubKeyStub.yields(null, oldKey);
pubkeyDaoStub.get.withArgs(oldKey._id).yields({
code: 42
});
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.not.exist;
expect(key).to.to.equal(oldKey);
expect(getPubKeyStub.calledOnce).to.be.true;
expect(pubkeyDaoStub.get.calledOnce).to.be.true;
expect(pubkeyDaoStub.getByUserId.called).to.be.false;
expect(lawnchairDaoStub.remove.called).to.be.false;
expect(lawnchairDaoStub.persist.called).to.be.false;
done();
});
});
it('should error while persisting new key', function(done) {
getPubKeyStub.yields(null, oldKey);
pubkeyDaoStub.get.withArgs(oldKey._id).yields();
pubkeyDaoStub.getByUserId.withArgs(testUser).yields(null, newKey);
keychainDao.requestPermissionForKeyUpdate = function(opts, cb) {
expect(opts.userId).to.equal(testUser);
expect(opts.newKey).to.equal(newKey);
cb(true);
};
lawnchairDaoStub.remove.withArgs('publickey_' + oldKey._id).yields();
lawnchairDaoStub.persist.yields({});
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.exist;
expect(key).to.not.exist;
expect(getPubKeyStub.calledOnce).to.be.true;
expect(pubkeyDaoStub.get.calledOnce).to.be.true;
expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true;
expect(lawnchairDaoStub.remove.calledOnce).to.be.true;
expect(lawnchairDaoStub.persist.calledOnce).to.be.true;
done();
});
});
it('should error while deleting old key', function(done) {
getPubKeyStub.yields(null, oldKey);
pubkeyDaoStub.get.withArgs(oldKey._id).yields();
pubkeyDaoStub.getByUserId.withArgs(testUser).yields();
keychainDao.requestPermissionForKeyUpdate = function(opts, cb) {
expect(opts.userId).to.equal(testUser);
cb(true);
};
lawnchairDaoStub.remove.yields({});
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.exist;
expect(key).to.not.exist;
expect(getPubKeyStub.calledOnce).to.be.true;
expect(pubkeyDaoStub.get.calledOnce).to.be.true;
expect(lawnchairDaoStub.remove.calledOnce).to.be.true;
expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true;
expect(lawnchairDaoStub.persist.called).to.be.false;
done();
});
});
it('should error while persisting new key', function(done) {
getPubKeyStub.yields(null, oldKey);
pubkeyDaoStub.get.withArgs(oldKey._id).yields();
pubkeyDaoStub.getByUserId.withArgs(testUser).yields(null, newKey);
keychainDao.requestPermissionForKeyUpdate = function(opts, cb) {
expect(opts.userId).to.equal(testUser);
expect(opts.newKey).to.equal(newKey);
cb(true);
};
lawnchairDaoStub.remove.withArgs('publickey_' + oldKey._id).yields();
lawnchairDaoStub.persist.yields({});
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.exist;
expect(key).to.not.exist;
expect(getPubKeyStub.calledOnce).to.be.true;
expect(pubkeyDaoStub.get.calledOnce).to.be.true;
expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true;
expect(lawnchairDaoStub.remove.calledOnce).to.be.true;
expect(lawnchairDaoStub.persist.calledOnce).to.be.true;
done();
});
});
it('should error when get failed', function(done) {
getPubKeyStub.yields(null, oldKey);
pubkeyDaoStub.get.withArgs(oldKey._id).yields({});
keychainDao.refreshKeyForUserId(testUser, function(err, key) {
expect(err).to.exist;
expect(key).to.not.exist;
done();
});
});
});
describe('lookup public key', function() {
it('should fail', function(done) {
keychainDao.lookupPublicKey(undefined, function(err, key) {
expect(err).to.exist;
expect(key).to.not.exist;
done();
});
});
it('should fail', function(done) {
lawnchairDaoStub.read.yields(42);
keychainDao.lookupPublicKey('12345', function(err, key) {
expect(err).to.exist;
expect(key).to.not.exist;
expect(lawnchairDaoStub.read.calledOnce).to.be.true;
done();
});
});
it('should work from local storage', function(done) {
lawnchairDaoStub.read.yields(null, {
_id: '12345',
publicKey: 'asdf'
});
keychainDao.lookupPublicKey('12345', function(err, key) {
expect(err).to.not.exist;
expect(key).to.exist;
expect(lawnchairDaoStub.read.calledOnce).to.be.true;
done();
});
});
it('should work from cloud', function(done) {
lawnchairDaoStub.read.yields();
pubkeyDaoStub.get.yields(null, {
_id: '12345',
publicKey: 'asdf'
});
lawnchairDaoStub.persist.yields();
keychainDao.lookupPublicKey('12345', function(err, key) {
expect(err).to.not.exist;
expect(key).to.exist;
expect(key._id).to.equal('12345');
expect(lawnchairDaoStub.read.calledOnce).to.be.true;
expect(pubkeyDaoStub.get.calledOnce).to.be.true;
expect(lawnchairDaoStub.persist.calledOnce).to.be.true;
done();
});
});
});
describe('get public keys by id', function() {
it('should fail', function(done) {
keychainDao.getPublicKeys([], function(err, keys) {
expect(err).to.not.exist;
expect(keys.length).to.equal(0);
done();
});
});
it('should fail', function(done) {
lawnchairDaoStub.read.yields(42);
var ids = [{
_id: '12345'
}];
keychainDao.getPublicKeys(ids, function(err, keys) {
expect(err).to.exist;
expect(keys).to.not.exist;
expect(lawnchairDaoStub.read.calledOnce).to.be.true;
done();
});
});
it('should work from local storage', function(done) {
lawnchairDaoStub.read.yields(null, {
_id: '12345',
publicKey: 'asdf'
});
var ids = [{
_id: '12345'
}];
keychainDao.getPublicKeys(ids, function(err, keys) {
expect(err).to.not.exist;
expect(keys.length).to.equal(1);
expect(keys[0]._id).to.equal('12345');
expect(lawnchairDaoStub.read.calledOnce).to.be.true;
done();
});
});
});
describe('get receiver public key', function() {
it('should fail due to error in lawnchair list', function(done) {
lawnchairDaoStub.list.yields(42);
keychainDao.getReceiverPublicKey(testUser, function(err, key) {
expect(err).to.exist;
expect(key).to.not.exist;
expect(lawnchairDaoStub.list.calledOnce).to.be.true;
done();
});
});
it('should work from lawnchair list', function(done) {
lawnchairDaoStub.list.yields(null, [{
_id: '12345',
userId: testUser,
publicKey: 'asdf'
}]);
keychainDao.getReceiverPublicKey(testUser, function(err, key) {
expect(err).to.not.exist;
expect(key).to.exist;
expect(key._id).to.equal('12345');
expect(lawnchairDaoStub.list.calledOnce).to.be.true;
done();
});
});
it('should work for keys with secondary userIds', function(done) {
lawnchairDaoStub.list.yields(null, [{
_id: '12345',
userId: 'not testUser',
userIds: [{
emailAddress: testUser
}],
publicKey: 'asdf'
}]);
keychainDao.getReceiverPublicKey(testUser, function(err, key) {
expect(err).to.not.exist;
expect(key).to.exist;
expect(key._id).to.equal('12345');
expect(lawnchairDaoStub.list.calledOnce).to.be.true;
done();
});
});
it('should fail due to error in pubkey dao', function(done) {
lawnchairDaoStub.list.yields(null, []);
pubkeyDaoStub.getByUserId.yields({});
keychainDao.getReceiverPublicKey(testUser, function(err, key) {
expect(err).to.exist;
expect(key).to.not.exist;
expect(lawnchairDaoStub.list.calledOnce).to.be.true;
expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true;
done();
});
});
it('should work from pubkey dao with empty result', function(done) {
lawnchairDaoStub.list.yields(null, []);
pubkeyDaoStub.getByUserId.yields();
keychainDao.getReceiverPublicKey(testUser, function(err, key) {
expect(err).to.not.exist;
expect(key).to.not.exist;
expect(lawnchairDaoStub.list.calledOnce).to.be.true;
expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true;
done();
});
});
it('should work from pubkey dao', function(done) {
lawnchairDaoStub.list.yields(null, []);
pubkeyDaoStub.getByUserId.yields(null, {
_id: '12345',
publicKey: 'asdf'
});
lawnchairDaoStub.persist.yields();
keychainDao.getReceiverPublicKey(testUser, function(err, key) {
expect(err).to.not.exist;
expect(key).to.exist;
expect(key._id).to.equal('12345');
expect(lawnchairDaoStub.list.calledOnce).to.be.true;
expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true;
expect(lawnchairDaoStub.persist.calledOnce).to.be.true;
done();
});
});
});
describe('get user key pair', function() {
it('should work if local keys are already present', function(done) {
lawnchairDaoStub.list.yields(null, [{
_id: '12345',
userId: testUser,
publicKey: 'asdf'
}]);
lawnchairDaoStub.read.yields(null, {
_id: '12345',
publicKey: 'asdf',
encryptedKey: 'qwer'
});
keychainDao.getUserKeyPair(testUser, function(err, keys) {
expect(err).to.not.exist;
expect(keys).to.exist;
expect(keys.publicKey).to.exist;
expect(keys.privateKey).to.exist;
expect(lawnchairDaoStub.list.calledOnce).to.be.true;
expect(lawnchairDaoStub.read.calledTwice).to.be.true;
done();
});
});
it('should work if local keys are not already present', function(done) {
lawnchairDaoStub.list.yields();
pubkeyDaoStub.getByUserId.yields();
keychainDao.getUserKeyPair(testUser, function(err, keys) {
expect(err).to.not.exist;
expect(keys).to.not.exist;
expect(lawnchairDaoStub.list.calledOnce).to.be.true;
expect(pubkeyDaoStub.getByUserId.calledOnce).to.be.true;
done();
});
});
it('should work if local keys are not already present', function(done) {
lawnchairDaoStub.list.yields();
pubkeyDaoStub.getByUserId.yields(null, {
_id: '12345',
publicKey: 'asdf'
});
lawnchairDaoStub.read.yields(null, {
_id: '12345',
publicKey: 'asdf',
encryptedKey: 'qwer'
});
keychainDao.getUserKeyPair(testUser, function(err, keys) {
expect(err).to.not.exist;
expect(keys).to.exist;
expect(keys.publicKey).to.exist;
expect(keys.privateKey).to.exist;
expect(lawnchairDaoStub.list.calledOnce).to.be.true;
expect(lawnchairDaoStub.read.calledTwice).to.be.true;
done();
});
});
});
describe('put user keypair', function() {
it('should fail', function(done) {
var keypair = {
publicKey: {
_id: '12345',
userId: testUser,
publicKey: 'asdf'
},
privateKey: {
_id: '12345',
encryptedKey: 'qwer'
}
};
keychainDao.putUserKeyPair(keypair, function(err) {
expect(err).to.exist;
done();
});
});
it('should work', function(done) {
var keypair = {
publicKey: {
_id: '12345',
userId: testUser,
publicKey: 'asdf'
},
privateKey: {
_id: '12345',
userId: testUser,
encryptedKey: 'qwer'
}
};
lawnchairDaoStub.persist.yields();
pubkeyDaoStub.put.yields();
keychainDao.putUserKeyPair(keypair, function(err) {
expect(err).to.not.exist;
expect(lawnchairDaoStub.persist.calledTwice).to.be.true;
expect(pubkeyDaoStub.put.calledOnce).to.be.true;
done();
});
});
});
});
});

View File

@ -1,62 +0,0 @@
'use strict';
require(['../../src/require-config'], function() {
require.config({
baseUrl: '../../src/lib',
paths: {
angularMocks: '../../test/lib/angular-mocks'
},
shim: {
angularMocks: {
exports: 'angular.mock',
deps: ['angular']
}
}
});
// Start the main app logic.
require(['js/app-config'], function(app) {
app.config.workerPath = '../../src/js';
startTests();
});
});
function startTests() {
mocha.setup('bdd');
require(
[
'test/new-unit/oauth-test',
'test/new-unit/auth-test',
'test/new-unit/email-dao-test',
'test/new-unit/app-controller-test',
'test/new-unit/pgp-test',
'test/new-unit/rest-dao-test',
'test/new-unit/publickey-dao-test',
'test/new-unit/lawnchair-dao-test',
'test/new-unit/keychain-dao-test',
'test/new-unit/devicestorage-dao-test',
'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',
'test/new-unit/login-new-device-ctrl-test',
'test/new-unit/login-ctrl-test',
'test/new-unit/read-ctrl-test',
'test/new-unit/navigation-ctrl-test',
'test/new-unit/mail-list-ctrl-test',
'test/new-unit/write-ctrl-test',
'test/new-unit/outbox-bo-test',
'test/new-unit/invitation-dao-test',
'test/new-unit/update-handler-test'
], function() {
//Tests loaded, run tests
mocha.run();
}
);
}

View File

@ -14,18 +14,18 @@ define(function(require) {
var scope, accountCtrl,
dummyFingerprint, expectedFingerprint,
dummyKeyId, expectedKeyId,
emailAddress, keySize, cryptoMock, keychainMock;
emailAddress, keySize, pgpMock, keychainMock;
beforeEach(function() {
appController._crypto = cryptoMock = sinon.createStubInstance(PGP);
appController._pgp = pgpMock = 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);
pgpMock.getFingerprint.returns(dummyFingerprint);
pgpMock.getKeyId.returns(dummyKeyId);
emailAddress = 'fred@foo.com';
keySize = 1234;
appController._emailDao = {
@ -34,7 +34,7 @@ define(function(require) {
asymKeySize: keySize
}
};
cryptoMock.getKeyParams.returns({
pgpMock.getKeyParams.returns({
_id: dummyKeyId,
fingerprint: dummyFingerprint,
userId: emailAddress,

View File

@ -1,24 +0,0 @@
define(['cryptoLib/aes-cbc', 'cryptoLib/util', 'test/test-data'], function(aes, util, testData) {
'use strict';
module("AES Crypto");
var aesTest = {
keySize: 128,
testMessage: testData.generateBigString(1000)
};
test("CBC mode", 4, function() {
var plaintext = aesTest.testMessage;
var key = util.random(aesTest.keySize);
var iv = util.random(aesTest.keySize);
ok(key, 'Key: ' + key);
equal(util.base642Str(key).length * 8, aesTest.keySize, 'Keysize ' + aesTest.keySize);
var ciphertext = aes.encrypt(plaintext, key, iv);
ok(ciphertext, 'Ciphertext lenght: ' + ciphertext.length);
var decrypted = aes.decrypt(ciphertext, key, iv);
equal(decrypted, plaintext, 'Decryption correct' + decrypted);
});
});

View File

@ -37,7 +37,7 @@ define(function(require) {
expect(controller._userStorage).to.exist;
expect(controller._invitationDao).to.exist;
expect(controller._keychain).to.exist;
expect(controller._crypto).to.exist;
expect(controller._pgp).to.exist;
expect(controller._pgpbuilder).to.exist;
expect(controller._emailDao).to.exist;
expect(controller._outboxBo).to.exist;

View File

@ -12,11 +12,11 @@ define(function(require) {
describe('Contacts Controller unit test', function() {
var scope, contactsCtrl,
origKeychain, keychainMock,
origCrypto, cryptoMock;
origPgp, pgpMock;
beforeEach(function() {
origCrypto = appController._crypto;
appController._crypto = cryptoMock = sinon.createStubInstance(PGP);
origPgp = appController._pgp;
appController._pgp = pgpMock = sinon.createStubInstance(PGP);
origKeychain = appController._keychain;
appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO);
@ -33,7 +33,7 @@ define(function(require) {
afterEach(function() {
// restore the module
appController._crypto = origCrypto;
appController._pgp = origPgp;
appController._keychain = origKeychain;
});
@ -60,7 +60,7 @@ define(function(require) {
keychainMock.listLocalPublicKeys.yields(null, [{
_id: '12345'
}]);
cryptoMock.getKeyParams.returns({
pgpMock.getKeyParams.returns({
fingerprint: 'asdf'
});
@ -92,7 +92,7 @@ define(function(require) {
it('should work', function(done) {
var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----';
cryptoMock.getKeyParams.returns({
pgpMock.getKeyParams.returns({
_id: '12345',
userId: 'max@example.com',
userIds: []
@ -127,7 +127,7 @@ define(function(require) {
it('should fail due to error in pgp.getKeyParams', function(done) {
var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----';
cryptoMock.getKeyParams.throws(new Error('WAT'));
pgpMock.getKeyParams.throws(new Error('WAT'));
scope.onError = function(err) {
expect(err).to.exist;
@ -140,7 +140,7 @@ define(function(require) {
it('should fail due to error in keychain.saveLocalPublicKey', function(done) {
var keyArmored = '-----BEGIN PGP PUBLIC KEY BLOCK-----';
cryptoMock.getKeyParams.returns({
pgpMock.getKeyParams.returns({
_id: '12345',
userId: 'max@example.com'
});

View File

@ -1,121 +1,58 @@
define(['js/crypto/crypto', 'cryptoLib/util', 'test/test-data'], function(Crypto, util, testData) {
'use strict';
define(function(require) {
'use strict';
module("Crypto Api");
var Crypto = require('js/crypto/crypto'),
util = require('js/crypto/util'),
config = require('js/app-config').config,
expect = chai.expect;
var cryptoTest = {
user: 'crypto_test@example.com',
password: 'Password',
keySize: 128,
ivSize: 128,
rsaKeySize: 1024,
salt: util.random(128)
};
describe('Crypto unit tests', function() {
this.timeout(20000);
var crypto;
var crypto,
password = 'password',
keySize = config.symKeySize,
ivSize = config.symIvSize;
asyncTest("Init without keypair", 4, function() {
crypto = new Crypto();
// init dependencies
ok(crypto, 'Crypto');
beforeEach(function() {
crypto = new Crypto();
});
// test without passing keys
crypto.init({
emailAddress: cryptoTest.user,
password: cryptoTest.password,
salt: cryptoTest.salt,
keySize: cryptoTest.keySize,
rsaKeySize: cryptoTest.rsaKeySize
}, function(err, generatedKeypair) {
ok(!err && generatedKeypair, 'Init crypto without keypair input');
var pk = generatedKeypair.publicKey;
ok(pk._id && pk.userId, 'Key ID: ' + pk._id);
ok(pk.publicKey.indexOf('-----BEGIN PUBLIC KEY-----') === 0, pk.publicKey);
cryptoTest.generatedKeypair = generatedKeypair;
afterEach(function() {});
start();
});
});
describe('AES encrypt/decrypt', function() {
it('should work', function(done) {
var plaintext = 'Hello, World!';
var key = util.random(keySize);
var iv = util.random(ivSize);
asyncTest("Init with keypair", 1, function() {
// test with passing keypair
crypto.init({
emailAddress: cryptoTest.user,
password: cryptoTest.password,
salt: cryptoTest.salt,
keySize: cryptoTest.keySize,
rsaKeySize: cryptoTest.rsaKeySize,
storedKeypair: cryptoTest.generatedKeypair
}, function(err, generatedKeypair) {
ok(!err && !generatedKeypair, 'Init crypto with keypair input');
crypto.encrypt(plaintext, key, iv, function(err, ciphertext) {
expect(err).to.not.exist;
expect(ciphertext).to.exist;
start();
});
});
crypto.decrypt(ciphertext, key, iv, function(err, decrypted) {
expect(err).to.not.exist;
expect(decrypted).to.equal(plaintext);
asyncTest("PBKDF2 (Async/Worker)", 2, function() {
crypto.deriveKey(cryptoTest.password, cryptoTest.salt, cryptoTest.keySize, function(err, key) {
ok(!err);
equal(util.base642Str(key).length * 8, cryptoTest.keySize, 'Keysize ' + cryptoTest.keySize);
done();
});
});
});
});
start();
});
});
describe("PBKDF2 (Async/Worker)", function() {
it('should work', function(done) {
var salt = util.random(keySize);
asyncTest("AES/HMAC encrypt batch (Async/Worker)", 2, function() {
// generate test data
cryptoTest.symlist = testData.getEmailCollection(10);
crypto.deriveKey(password, salt, keySize, function(err, key) {
expect(err).to.not.exist;
expect(util.base642Str(key).length * 8).to.equal(keySize);
crypto.symEncryptList(cryptoTest.symlist, function(err, result) {
ok(!err && result.key && result.list && result.list[0].hmac, 'Encrypt list for user');
equal(result.list.length, cryptoTest.symlist.length, 'Length of list');
cryptoTest.symEncryptedList = result.list;
cryptoTest.symKey = result.key;
done();
});
});
start();
});
});
asyncTest("AES/HMAC decrypt batch (Async/Worker)", 3, function() {
var keys = [];
for (var i = 0; i < cryptoTest.symEncryptedList.length; i++) {
keys.push(cryptoTest.symKey);
}
crypto.symDecryptList(cryptoTest.symEncryptedList, keys, function(err, decryptedList) {
ok(!err && decryptedList, 'Decrypt list');
equal(decryptedList.length, cryptoTest.symlist.length, 'Length of list');
deepEqual(decryptedList, cryptoTest.symlist, 'Decrypted list is correct');
start();
});
});
asyncTest("AES/RSA encrypt batch for User (Async/Worker)", 2, function() {
// generate test data
cryptoTest.list = testData.getEmailCollection(10);
var receiverPubkeys = [cryptoTest.generatedKeypair.publicKey];
crypto.encryptListForUser(cryptoTest.list, receiverPubkeys, function(err, encryptedList) {
ok(!err && encryptedList, 'Encrypt list for user');
equal(encryptedList.length, cryptoTest.list.length, 'Length of list');
cryptoTest.encryptedList = encryptedList;
start();
});
});
asyncTest("AES/RSA decrypt batch for User (Async/Worker)", 3, function() {
var senderPubkeys = [cryptoTest.generatedKeypair.publicKey];
crypto.decryptListForUser(cryptoTest.encryptedList, senderPubkeys, function(err, decryptedList) {
ok(!err && decryptedList, 'Decrypt list');
equal(decryptedList.length, cryptoTest.list.length, 'Length of list');
deepEqual(decryptedList, cryptoTest.list, 'Decrypted list is correct');
start();
});
});
});
});
});

View File

@ -1,106 +1,105 @@
define(['underscore', 'cryptoLib/util', 'js/crypto/crypto', 'js/dao/devicestorage-dao', 'test/test-data', 'js/dao/lawnchair-dao'], function(_, util, Crypto, DeviceStorageDAO, testData, LawnchairDAO) {
define(function(require) {
'use strict';
module("DeviceStorage");
var LawnchairDAO = require('js/dao/lawnchair-dao'),
DeviceStorageDAO = require('js/dao/devicestorage-dao'),
expect = chai.expect;
var devicestorageTest = {
user: 'devicestorage_test@example.com',
password: 'Password',
keySize: 128,
ivSize: 128,
rsaKeySize: 1024
};
var testUser = 'test@example.com';
var crypto, storage;
describe('Device Storage DAO unit tests', function() {
asyncTest("Init", 3, function() {
// init dependencies
storage = new DeviceStorageDAO(new LawnchairDAO());
storage.init(devicestorageTest.user, function() {
ok(storage, 'DeviceStorageDAO');
var storageDao, lawnchairDaoStub;
// generate test data
devicestorageTest.list = testData.getEmailCollection(100);
beforeEach(function() {
lawnchairDaoStub = sinon.createStubInstance(LawnchairDAO);
storageDao = new DeviceStorageDAO(lawnchairDaoStub);
});
// init crypto
crypto = new Crypto();
crypto.init({
emailAddress: devicestorageTest.user,
password: devicestorageTest.password,
salt: util.random(devicestorageTest.keySize),
keySize: devicestorageTest.keySize,
rsaKeySize: devicestorageTest.rsaKeySize
}, function(err, generatedKeypair) {
ok(!err && generatedKeypair, 'Init crypto');
devicestorageTest.generatedKeypair = generatedKeypair;
afterEach(function() {});
// clear db before tests
storage.clear(function(err) {
ok(!err, 'DB cleared. Error status: ' + err);
describe('init', function() {
it('should work', function(done) {
lawnchairDaoStub.init.yields();
start();
storageDao.init(testUser, function(err) {
expect(err).to.not.exist;
expect(lawnchairDaoStub.init.calledOnce).to.be.true;
done();
});
});
});
});
asyncTest("Encrypt list for user", 2, function() {
var receiverPubkeys = [devicestorageTest.generatedKeypair.publicKey];
describe('store list', function() {
it('should fail', function(done) {
var list = [{}];
crypto.encryptListForUser(devicestorageTest.list, receiverPubkeys, function(err, encryptedList) {
ok(!err);
equal(encryptedList.length, devicestorageTest.list.length, 'Encrypt list');
encryptedList.forEach(function(i) {
i.sentDate = _.findWhere(devicestorageTest.list, {
id: i.id
}).sentDate;
storageDao.storeList(list, '', function(err) {
expect(err).to.exist;
done();
});
});
devicestorageTest.encryptedList = encryptedList;
start();
});
});
it('should work with empty list', function(done) {
var list = [];
asyncTest("Store encrypted list", 1, function() {
storage.storeList(devicestorageTest.encryptedList, 'email_inbox', function() {
ok(true, 'Store encrypted list');
storageDao.storeList(list, 'email', function(err) {
expect(err).to.not.exist;
done();
});
});
start();
});
});
it('should work', function(done) {
lawnchairDaoStub.batch.yields();
asyncTest("List items", 4, function() {
var senderPubkeys = [devicestorageTest.generatedKeypair.publicKey];
var list = [{
foo: 'bar'
}];
var offset = 2,
num = 6;
// list encrypted items from storage
storage.listItems('email_inbox', offset, num, function(err, encryptedList) {
ok(!err);
// decrypt list
crypto.decryptListForUser(encryptedList, senderPubkeys, function(err, decryptedList) {
ok(!err);
equal(decryptedList.length, num, 'Found ' + decryptedList.length + ' items in store (and decrypted)');
var origSet = devicestorageTest.list.splice(92, num);
deepEqual(decryptedList, origSet, 'Messages decrypted correctly');
start();
storageDao.storeList(list, 'email', function(err) {
expect(err).to.not.exist;
expect(lawnchairDaoStub.batch.calledOnce).to.be.true;
done();
});
});
});
});
asyncTest("Delete List items", 1, function() {
// list encrypted items from storage
storage.removeList('email_inbox', function(err) {
ok(!err);
describe('remove list', function() {
it('should work', function(done) {
lawnchairDaoStub.removeList.yields();
start();
storageDao.removeList('email', function(err) {
expect(err).to.not.exist;
expect(lawnchairDaoStub.removeList.calledOnce).to.be.true;
done();
});
});
});
describe('list items', function() {
it('should work', function(done) {
lawnchairDaoStub.list.yields();
storageDao.listItems('email', 0, null, function(err) {
expect(err).to.not.exist;
expect(lawnchairDaoStub.list.calledOnce).to.be.true;
done();
});
});
});
describe('clear', function() {
it('should work', function(done) {
lawnchairDaoStub.clear.yields();
storageDao.clear(function(err) {
expect(err).to.not.exist;
expect(lawnchairDaoStub.clear.calledOnce).to.be.true;
done();
});
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +0,0 @@
define(['node-forge', 'cryptoLib/util', 'test/test-data'], function(forge, util, testData) {
'use strict';
module("Forge Crypto");
var forgeRsaTest = {
keySize: 1024,
testMessage: '06a9214036b8a15b512e03d534120006'
};
var forgeAesTest = {
keySize: 128,
testMessage: testData.generateBigString(1000)
};
test("SHA-1 Hash", 1, function() {
var sha1 = forge.md.sha1.create();
sha1.update(forgeAesTest.testMessage);
var digest = sha1.digest().toHex();
ok(digest, digest);
});
test("SHA-256 Hash", 1, function() {
forgeRsaTest.md = forge.md.sha256.create();
forgeRsaTest.md.update(forgeAesTest.testMessage);
var digest = forgeRsaTest.md.digest().toHex();
ok(digest, digest);
});
test("HMAC SHA-256", 1, function() {
var key = util.base642Str(util.random(forgeAesTest.keySize));
var iv = util.base642Str(util.random(forgeAesTest.keySize));
var hmac = forge.hmac.create();
hmac.start('sha256', key);
hmac.update(iv);
hmac.update(forgeAesTest.testMessage);
var digest = hmac.digest().toHex();
ok(digest, digest);
});
});

View File

@ -1,17 +1,19 @@
<!DOCTYPE html>
<html>
<html style="overflow-y: auto">
<head>
<meta charset="utf-8">
<title>JavaScript Unit Tests</title>
<link rel="stylesheet" href="../qunit-1.11.0.css">
<link rel="stylesheet" href="../lib/mocha.css" />
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<div id="mocha"></div>
<script src="../qunit-1.11.0.js"></script>
<script>QUnit.config.autostart = false;</script>
<script src="../lib/chai.js"></script>
<script src="../lib/sinon.js"></script>
<script src="../lib/mocha.js"></script>
<script data-main="main.js" src="../../src/lib/require.js"></script>
</body>
</html>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -7,10 +7,13 @@ define(function(require) {
LoginCtrl = require('js/controller/login'),
EmailDAO = require('js/dao/email-dao'),
Auth = require('js/bo/auth'),
appController = require('js/app-controller');
appController = require('js/app-controller'),
KeychainDAO = require('js/dao/keychain-dao');
describe('Login Controller unit test', function() {
var scope, location, ctrl, origEmailDao, emailDaoMock,
var scope, location, ctrl,
origEmailDao, emailDaoMock,
origKeychain, keychainMock,
emailAddress = 'fred@foo.com',
startAppStub,
checkForUpdateStub,
@ -21,14 +24,16 @@ define(function(require) {
var hasChrome, hasIdentity;
beforeEach(function() {
hasChrome = !! window.chrome;
hasIdentity = !! window.chrome.identity;
hasChrome = !!window.chrome;
hasIdentity = !!window.chrome.identity;
window.chrome = window.chrome || {};
window.chrome.identity = window.chrome.identity || {};
// remember original module to restore later, then replace it
origEmailDao = appController._emailDao;
origKeychain = appController._keychain;
appController._emailDao = emailDaoMock = sinon.createStubInstance(EmailDAO);
appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO);
appController._auth = authStub = sinon.createStubInstance(Auth);
startAppStub = sinon.stub(appController, 'start');
@ -48,6 +53,7 @@ define(function(require) {
// restore the app controller module
appController._emailDao = origEmailDao;
appController._keychain = origKeychain;
appController.start.restore && appController.start.restore();
appController.checkForUpdate.restore && appController.checkForUpdate.restore();
appController.init.restore && appController.init.restore();
@ -128,12 +134,42 @@ define(function(require) {
});
});
it('should forward to privatekey download login', function(done) {
startAppStub.yields();
authStub.getEmailAddress.yields(null, emailAddress);
initStub.yields(null, {
publicKey: 'b'
});
keychainMock.requestPrivateKeyDownload.yields(null, {});
angular.module('logintest', []);
mocks.module('logintest');
mocks.inject(function($controller, $rootScope, $location) {
location = $location;
sinon.stub(location, 'path', function(path) {
expect(path).to.equal('/login-privatekey-download');
expect(startAppStub.calledOnce).to.be.true;
expect(checkForUpdateStub.calledOnce).to.be.true;
expect(authStub.getEmailAddress.calledOnce).to.be.true;
expect(keychainMock.requestPrivateKeyDownload.calledOnce).to.be.true;
done();
});
scope = $rootScope.$new();
scope.state = {};
ctrl = $controller(LoginCtrl, {
$location: location,
$scope: scope
});
});
});
it('should forward to new device login', function(done) {
startAppStub.yields();
authStub.getEmailAddress.yields(null, emailAddress);
initStub.yields(null, {
publicKey: 'b'
});
keychainMock.requestPrivateKeyDownload.yields();
angular.module('logintest', []);
mocks.module('logintest');
@ -144,6 +180,7 @@ define(function(require) {
expect(startAppStub.calledOnce).to.be.true;
expect(checkForUpdateStub.calledOnce).to.be.true;
expect(authStub.getEmailAddress.calledOnce).to.be.true;
expect(keychainMock.requestPrivateKeyDownload.calledOnce).to.be.true;
done();
});
scope = $rootScope.$new();

View File

@ -0,0 +1,235 @@
define(function(require) {
'use strict';
var expect = chai.expect,
angular = require('angular'),
mocks = require('angularMocks'),
LoginPrivateKeyDownloadCtrl = require('js/controller/login-privatekey-download'),
EmailDAO = require('js/dao/email-dao'),
appController = require('js/app-controller'),
KeychainDAO = require('js/dao/keychain-dao');
describe('Login Private Key Download Controller unit test', function() {
var scope, location, ctrl,
origEmailDao, emailDaoMock,
origKeychain, keychainMock,
emailAddress = 'fred@foo.com';
beforeEach(function(done) {
// remember original module to restore later, then replace it
origEmailDao = appController._emailDao;
origKeychain = appController._keychain;
appController._emailDao = emailDaoMock = sinon.createStubInstance(EmailDAO);
appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO);
emailDaoMock._account = {
emailAddress: emailAddress
};
angular.module('login-privatekey-download-test', []);
mocks.module('login-privatekey-download-test');
mocks.inject(function($controller, $rootScope) {
scope = $rootScope.$new();
scope.state = {};
ctrl = $controller(LoginPrivateKeyDownloadCtrl, {
$location: location,
$scope: scope
});
done();
});
});
afterEach(function() {
// restore the app controller module
appController._emailDao = origEmailDao;
appController._keychain = origKeychain;
});
describe('initialization', function() {
it('should work', function() {
expect(scope.step).to.equal(1);
});
});
describe('verifyRecoveryToken', function() {
var testKeypair = {
publicKey: {
_id: 'id'
}
};
it('should fail for empty recovery token', function(done) {
scope.onError = function(err) {
expect(err).to.exist;
done();
};
scope.recoveryToken = undefined;
scope.verifyRecoveryToken();
});
it('should fail in keychain.getUserKeyPair', function(done) {
keychainMock.getUserKeyPair.yields(42);
scope.onError = function(err) {
expect(err).to.exist;
expect(keychainMock.getUserKeyPair.calledOnce).to.be.true;
done();
};
scope.recoveryToken = 'token';
scope.verifyRecoveryToken();
});
it('should fail in keychain.downloadPrivateKey', function(done) {
keychainMock.getUserKeyPair.yields(null, testKeypair);
keychainMock.downloadPrivateKey.yields(42);
scope.onError = function(err) {
expect(err).to.exist;
expect(keychainMock.getUserKeyPair.calledOnce).to.be.true;
expect(keychainMock.downloadPrivateKey.calledOnce).to.be.true;
done();
};
scope.recoveryToken = 'token';
scope.verifyRecoveryToken();
});
it('should work', function(done) {
keychainMock.getUserKeyPair.yields(null, testKeypair);
keychainMock.downloadPrivateKey.yields(null, 'encryptedPrivateKey');
scope.recoveryToken = 'token';
scope.verifyRecoveryToken(function() {
expect(scope.encryptedPrivateKey).to.equal('encryptedPrivateKey');
done();
});
});
});
describe('decryptAndStorePrivateKeyLocally', function() {
beforeEach(function() {
scope.code0 = '0';
scope.code1 = '1';
scope.code2 = '2';
scope.code3 = '3';
scope.code4 = '4';
scope.code5 = '5';
scope.encryptedPrivateKey = {
encryptedPrivateKey: 'encryptedPrivateKey'
};
scope.cachedKeypair = {
publicKey: {
_id: 'keyId'
}
};
});
it('should fail on empty code', function(done) {
scope.code0 = '';
scope.code1 = '';
scope.code2 = '';
scope.code3 = '';
scope.code4 = '';
scope.code5 = '';
scope.onError = function(err) {
expect(err).to.exist;
done();
};
scope.decryptAndStorePrivateKeyLocally();
});
it('should fail on decryptAndStorePrivateKeyLocally', function(done) {
keychainMock.decryptAndStorePrivateKeyLocally.yields(42);
scope.onError = function(err) {
expect(err).to.exist;
expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true;
done();
};
scope.decryptAndStorePrivateKeyLocally();
});
it('should goto /login-existing on emailDao.unlock fail', function(done) {
keychainMock.decryptAndStorePrivateKeyLocally.yields(null, {
encryptedKey: 'keyArmored'
});
emailDaoMock.unlock.yields(42);
scope.goTo = function(location) {
expect(location).to.equal('/login-existing');
expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true;
expect(emailDaoMock.unlock.calledOnce).to.be.true;
done();
};
scope.decryptAndStorePrivateKeyLocally();
});
it('should goto /desktop on emailDao.unlock success', function(done) {
keychainMock.decryptAndStorePrivateKeyLocally.yields(null, {
encryptedKey: 'keyArmored'
});
emailDaoMock.unlock.yields();
scope.goTo = function(location) {
expect(location).to.equal('/desktop');
expect(keychainMock.decryptAndStorePrivateKeyLocally.calledOnce).to.be.true;
expect(emailDaoMock.unlock.calledOnce).to.be.true;
done();
};
scope.decryptAndStorePrivateKeyLocally();
});
});
describe('goForward', function() {
it('should work in step 1', function() {
var verifyRecoveryTokenStub = sinon.stub(scope, 'verifyRecoveryToken');
verifyRecoveryTokenStub.yields();
scope.step = 1;
scope.goForward();
expect(verifyRecoveryTokenStub.calledOnce).to.be.true;
expect(scope.step).to.equal(2);
verifyRecoveryTokenStub.restore();
});
it('should work in step 2', function() {
var decryptAndStorePrivateKeyLocallyStub = sinon.stub(scope, 'decryptAndStorePrivateKeyLocally');
decryptAndStorePrivateKeyLocallyStub.returns();
scope.step = 2;
scope.goForward();
expect(decryptAndStorePrivateKeyLocallyStub.calledOnce).to.be.true;
decryptAndStorePrivateKeyLocallyStub.restore();
});
});
describe('goTo', function() {
it('should work', function(done) {
mocks.inject(function($controller, $rootScope, $location) {
location = $location;
sinon.stub(location, 'path', function(path) {
expect(path).to.equal('/desktop');
done();
});
scope = $rootScope.$new();
scope.state = {};
ctrl = $controller(LoginPrivateKeyDownloadCtrl, {
$location: location,
$scope: scope
});
});
scope.goTo('/desktop');
});
});
});
});

View File

@ -1,16 +1,22 @@
'use strict';
require(['../../src/require-config'], function() {
require.config({
baseUrl: '../../src/lib'
baseUrl: '../../src/lib',
paths: {
angularMocks: '../../test/lib/angular-mocks'
},
shim: {
angularMocks: {
exports: 'angular.mock',
deps: ['angular']
}
}
});
// Start the main app logic.
require(['js/app-config', 'cordova'], function(app) {
// clear session storage of failed tests, so async order is correct after fail & refresh
window.sessionStorage.clear();
// Start the main app logic.
require(['js/app-config'], function(app) {
app.config.workerPath = '../../src/js';
startTests();
@ -18,17 +24,43 @@ require(['../../src/require-config'], function() {
});
function startTests() {
mocha.setup('bdd');
require(
[
'test/unit/forge-test',
'test/unit/aes-test',
'test/unit/rsa-test',
'test/unit/keychain-dao-test',
'test/unit/oauth-test',
'test/unit/auth-test',
'test/unit/email-dao-test',
'test/unit/app-controller-test',
'test/unit/pgp-test',
'test/unit/crypto-test',
'test/unit/devicestorage-dao-test'
'test/unit/rest-dao-test',
'test/unit/publickey-dao-test',
'test/unit/privatekey-dao-test',
'test/unit/lawnchair-dao-test',
'test/unit/keychain-dao-test',
'test/unit/devicestorage-dao-test',
'test/unit/dialog-ctrl-test',
'test/unit/add-account-ctrl-test',
'test/unit/account-ctrl-test',
'test/unit/set-passphrase-ctrl-test',
'test/unit/contacts-ctrl-test',
'test/unit/login-existing-ctrl-test',
'test/unit/login-initial-ctrl-test',
'test/unit/login-new-device-ctrl-test',
'test/unit/login-privatekey-download-ctrl-test',
'test/unit/privatekey-upload-ctrl-test',
'test/unit/login-ctrl-test',
'test/unit/read-ctrl-test',
'test/unit/navigation-ctrl-test',
'test/unit/mail-list-ctrl-test',
'test/unit/write-ctrl-test',
'test/unit/outbox-bo-test',
'test/unit/invitation-dao-test',
'test/unit/update-handler-test'
], function() {
//Tests loaded, run tests
QUnit.start();
mocha.run();
}
);
}

View File

@ -13,7 +13,7 @@ define(function(require) {
keySize = 512,
keyId = 'F6F60E9B42CDFF4C',
pubkey = '-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n' +
'Version: OpenPGP.js v0.5.1\r\n' +
'Version: OpenPGP.js v0.6.0\r\n' +
'Comment: http://openpgpjs.org\r\n' +
'\r\n' +
'xk0EUlhMvAEB/2MZtCUOAYvyLFjDp3OBMGn3Ev8FwjzyPbIF0JUw+L7y2XR5\r\n' +
@ -24,7 +24,7 @@ define(function(require) {
'=6XMW\r\n' +
'-----END PGP PUBLIC KEY BLOCK-----\r\n\r\n',
privkey = '-----BEGIN PGP PRIVATE KEY BLOCK-----\r\n' +
'Version: OpenPGP.js v0.5.1\r\n' +
'Version: OpenPGP.js v0.6.0\r\n' +
'Comment: http://openpgpjs.org\r\n' +
'\r\n' +
'xcBeBFJYTLwBAf9jGbQlDgGL8ixYw6dzgTBp9xL/BcI88j2yBdCVMPi+8tl0\r\n' +
@ -92,7 +92,7 @@ define(function(require) {
publicKeyArmored: pubkey
}, function(err) {
expect(err).to.exist;
expect(err.errMsg).to.equal('Incorrect passphrase!');
expect(err.message).to.equal('Incorrect passphrase!');
pgp.exportKeys(function(err, keys) {
expect(err).to.exist;

View File

@ -0,0 +1,201 @@
define(function(require) {
'use strict';
var RestDAO = require('js/dao/rest-dao'),
PrivateKeyDAO = require('js/dao/privatekey-dao'),
expect = chai.expect;
describe('Private Key DAO unit tests', function() {
var privkeyDao, restDaoStub,
emailAddress = 'test@example.com',
deviceName = 'iPhone Work';
beforeEach(function() {
restDaoStub = sinon.createStubInstance(RestDAO);
privkeyDao = new PrivateKeyDAO(restDaoStub);
});
afterEach(function() {});
describe('requestDeviceRegistration', function() {
it('should fail due to invalid args', function(done) {
privkeyDao.requestDeviceRegistration({}, function(err, sessionKey) {
expect(err).to.exist;
expect(sessionKey).to.not.exist;
done();
});
});
it('should work', function(done) {
restDaoStub.post.yields(null, {
encryptedRegSessionKey: 'asdf'
});
privkeyDao.requestDeviceRegistration({
userId: emailAddress,
deviceName: deviceName
}, function(err, sessionKey) {
expect(err).to.not.exist;
expect(sessionKey).to.exist;
done();
});
});
});
describe('uploadDeviceSecret', function() {
it('should fail due to invalid args', function(done) {
privkeyDao.uploadDeviceSecret({}, function(err) {
expect(err).to.exist;
done();
});
});
it('should work', function(done) {
restDaoStub.put.yields();
privkeyDao.uploadDeviceSecret({
userId: emailAddress,
deviceName: deviceName,
encryptedDeviceSecret: 'asdf',
iv: 'iv'
}, function(err) {
expect(err).to.not.exist;
done();
});
});
});
describe('requestAuthSessionKey', function() {
it('should fail due to invalid args', function(done) {
privkeyDao.requestAuthSessionKey({}, function(err) {
expect(err).to.exist;
done();
});
});
it('should work', function(done) {
restDaoStub.post.withArgs(undefined, '/auth/user/' + emailAddress).yields();
privkeyDao.requestAuthSessionKey({
userId: emailAddress
}, function(err) {
expect(err).to.not.exist;
done();
});
});
});
describe('verifyAuthentication', function() {
it('should fail due to invalid args', function(done) {
privkeyDao.verifyAuthentication({}, function(err) {
expect(err).to.exist;
done();
});
});
it('should work', function(done) {
var sessionId = '1';
var options = {
userId: emailAddress,
sessionId: sessionId,
encryptedChallenge: 'asdf',
encryptedDeviceSecret: 'qwer',
iv: ' iv'
};
restDaoStub.put.withArgs(options, '/auth/user/' + emailAddress + '/session/' + sessionId).yields();
privkeyDao.verifyAuthentication(options, function(err) {
expect(err).to.not.exist;
done();
});
});
});
describe('upload', function() {
it('should fail due to invalid args', function(done) {
privkeyDao.upload({}, function(err) {
expect(err).to.exist;
done();
});
});
it('should work', function(done) {
var options = {
_id: '12345',
userId: emailAddress,
encryptedPrivateKey: 'asdf',
sessionId: '1',
salt: 'salt',
iv: 'iv'
};
restDaoStub.post.withArgs(options, '/privatekey/user/' + emailAddress + '/session/' + options.sessionId).yields();
privkeyDao.upload(options, function(err) {
expect(err).to.not.exist;
done();
});
});
});
describe('requestDownload', function() {
it('should fail due to invalid args', function(done) {
privkeyDao.requestDownload({}, function(err) {
expect(err).to.exist;
done();
});
});
it('should work', function(done) {
var key = {
_id: '12345'
};
restDaoStub.get.withArgs({
uri: '/privatekey/user/' + emailAddress + '/key/' + key._id
}).yields();
privkeyDao.requestDownload({
userId: emailAddress,
keyId: key._id
}, function(err) {
expect(err).to.not.exist;
done();
});
});
});
describe('download', function() {
it('should fail due to invalid args', function(done) {
privkeyDao.download({}, function(err) {
expect(err).to.exist;
done();
});
});
it('should work', function(done) {
var key = {
_id: '12345'
};
restDaoStub.get.withArgs({
uri: '/privatekey/user/' + emailAddress + '/key/' + key._id + '/recovery/token'
}).yields();
privkeyDao.download({
userId: emailAddress,
keyId: key._id,
recoveryToken: 'token'
}, function(err) {
expect(err).to.not.exist;
done();
});
});
});
});
});

View File

@ -0,0 +1,260 @@
define(function(require) {
'use strict';
var expect = chai.expect,
angular = require('angular'),
mocks = require('angularMocks'),
PrivateKeyUploadCtrl = require('js/controller/privatekey-upload'),
appController = require('js/app-controller'),
KeychainDAO = require('js/dao/keychain-dao'),
PGP = require('js/crypto/pgp');
describe('Private Key Upload Controller unit test', function() {
var scope, location, ctrl,
origEmailDao, emailDaoMock,
origKeychain, keychainMock,
pgpStub,
emailAddress = 'fred@foo.com';
beforeEach(function(done) {
// remember original module to restore later, then replace it
origEmailDao = appController._emailDao;
appController._emailDao = emailDaoMock = {
_account: {
emailAddress: emailAddress
}
};
origKeychain = appController._keychain;
appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO);
keychainMock._pgp = pgpStub = sinon.createStubInstance(PGP);
angular.module('login-privatekey-download-test', []);
mocks.module('login-privatekey-download-test');
mocks.inject(function($controller, $rootScope) {
scope = $rootScope.$new();
scope.state = {};
ctrl = $controller(PrivateKeyUploadCtrl, {
$location: location,
$scope: scope
});
done();
});
});
afterEach(function() {
// restore the app controller module
appController._keychain = origKeychain;
appController._emailDao = origEmailDao;
});
describe('checkServerForKey', function() {
var keyParams = {
userId: emailAddress,
_id: 'keyId'
};
it('should fail', function(done) {
pgpStub.getKeyParams.returns(keyParams);
keychainMock.requestPrivateKeyDownload.yields(42);
scope.onError = function(err) {
expect(err).to.exist;
expect(keychainMock.requestPrivateKeyDownload.calledOnce).to.be.true;
done();
};
scope.checkServerForKey();
});
it('should return true', function(done) {
pgpStub.getKeyParams.returns(keyParams);
keychainMock.requestPrivateKeyDownload.yields(null, true);
scope.checkServerForKey(function(privateKeySynced) {
expect(privateKeySynced).to.be.true;
done();
});
});
it('should return undefined', function(done) {
pgpStub.getKeyParams.returns(keyParams);
keychainMock.requestPrivateKeyDownload.yields(null, false);
scope.checkServerForKey(function(privateKeySynced) {
expect(privateKeySynced).to.be.undefined;
done();
});
});
});
describe('displayUploadUi', function() {
it('should work', function() {
var generateCodeStub = sinon.stub(scope, 'generateCode');
generateCodeStub.returns('asdf');
scope.displayUploadUi();
expect(scope.step).to.equal(1);
expect(scope.code).to.equal('asdf');
generateCodeStub.restore();
});
});
describe('generateCode', function() {
it('should work', function() {
expect(scope.generateCode().length).to.equal(24);
});
});
describe('verifyCode', function() {
it('should fail for wrong code', function() {
scope.code0 = 'b';
scope.code1 = 'b';
scope.code2 = 'b';
scope.code3 = 'b';
scope.code4 = 'b';
scope.code5 = 'b';
scope.code = 'aaaaaa';
scope.onError = function() {};
expect(scope.verifyCode()).to.be.false;
});
it('should work', function() {
scope.code0 = 'a';
scope.code1 = 'a';
scope.code2 = 'a';
scope.code3 = 'a';
scope.code4 = 'a';
scope.code5 = 'a';
scope.code = 'aaaaaa';
scope.onError = function() {};
expect(scope.verifyCode()).to.be.false;
});
});
describe('setDeviceName', function() {
it('should work', function(done) {
keychainMock.setDeviceName.yields();
scope.setDeviceName(done);
});
});
describe('encryptAndUploadKey', function() {
it('should fail due to keychain.registerDevice', function(done) {
keychainMock.registerDevice.yields(42);
scope.onError = function(err) {
expect(err).to.exist;
expect(keychainMock.registerDevice.calledOnce).to.be.true;
done();
};
scope.encryptAndUploadKey();
});
it('should work', function(done) {
keychainMock.registerDevice.yields();
keychainMock.uploadPrivateKey.yields();
scope.encryptAndUploadKey(function(err) {
expect(err).to.not.exist;
expect(keychainMock.registerDevice.calledOnce).to.be.true;
expect(keychainMock.uploadPrivateKey.calledOnce).to.be.true;
done();
});
});
});
describe('goBack', function() {
it('should work', function() {
scope.step = 2;
scope.goBack();
expect(scope.step).to.equal(1);
});
it('should not work for < 2', function() {
scope.step = 1;
scope.goBack();
expect(scope.step).to.equal(1);
});
});
describe('goForward', function() {
var verifyCodeStub, setDeviceNameStub, encryptAndUploadKeyStub;
beforeEach(function() {
verifyCodeStub = sinon.stub(scope, 'verifyCode');
setDeviceNameStub = sinon.stub(scope, 'setDeviceName');
encryptAndUploadKeyStub = sinon.stub(scope, 'encryptAndUploadKey');
});
afterEach(function() {
verifyCodeStub.restore();
setDeviceNameStub.restore();
encryptAndUploadKeyStub.restore();
});
it('should work for < 2', function() {
scope.step = 1;
scope.goForward();
expect(scope.step).to.equal(2);
});
it('should work for 2', function() {
verifyCodeStub.returns(true);
scope.step = 2;
scope.goForward();
expect(scope.step).to.equal(3);
});
it('should not work for 2 when code invalid', function() {
verifyCodeStub.returns(false);
scope.step = 2;
scope.goForward();
expect(scope.step).to.equal(2);
});
it('should fail for 3 due to error in setDeviceName', function(done) {
scope.step = 3;
setDeviceNameStub.yields(42);
scope.onError = function(err) {
expect(err).to.exist;
expect(scope.step).to.equal(3);
done();
};
scope.goForward();
});
it('should fail for 3 due to error in encryptAndUploadKey', function(done) {
scope.step = 3;
setDeviceNameStub.yields();
encryptAndUploadKeyStub.yields(42);
scope.onError = function(err) {
expect(err).to.exist;
expect(scope.step).to.equal(4);
done();
};
scope.goForward();
});
it('should work for 3', function(done) {
scope.step = 3;
setDeviceNameStub.yields();
encryptAndUploadKeyStub.yields();
scope.onError = function(err) {
expect(err.title).to.equal('Success');
expect(scope.step).to.equal(4);
done();
};
scope.goForward();
});
});
});
});

View File

@ -145,6 +145,31 @@ define(function(require) {
});
});
describe('post', function() {
it('should fail', function() {
restDao.post('/asdf', {}, function(err) {
expect(err).to.exist;
expect(err.code).to.equal(500);
});
expect(requests.length).to.equal(1);
requests[0].respond(500, {
"Content-Type": "text/plain"
}, 'Internal error');
});
it('should work', function() {
restDao.post('/asdf', {}, function(err, res, status) {
expect(err).to.not.exist;
expect(res).to.equal('');
expect(status).to.equal(201);
});
expect(requests.length).to.equal(1);
requests[0].respond(201);
});
});
describe('put', function() {
it('should fail', function() {
restDao.put('/asdf', {}, function(err) {

View File

@ -1,53 +0,0 @@
define(['cryptoLib/rsa'], function(rsa) {
'use strict';
module("RSA Crypto");
var rsaTest = {
keySize: 1024,
testMessage: '06a9214036b8a15b512e03d534120006'
};
asyncTest("Generate keypair", 1, function() {
rsa.generateKeypair(rsaTest.keySize, function(err) {
ok(!err);
start();
});
});
test("Export keys", 2, function() {
rsaTest.keypair = rsa.exportKeys();
ok(rsaTest.keypair.pubkeyPem.indexOf('-----BEGIN PUBLIC KEY-----') === 0, rsaTest.keypair.pubkeyPem);
ok(rsaTest.keypair.privkeyPem.indexOf('-----BEGIN RSA PRIVATE KEY-----') === 0, rsaTest.keypair.privkeyPem);
});
test("Init", 2, function() {
rsa.init(rsaTest.keypair.pubkeyPem, rsaTest.keypair.privkeyPem);
var exported = rsa.exportKeys();
ok(exported.pubkeyPem.indexOf('-----BEGIN PUBLIC KEY-----') === 0);
ok(exported.privkeyPem.indexOf('-----BEGIN RSA PRIVATE KEY-----') === 0);
});
test("Encrypt", 1, function() {
rsaTest.ct = rsa.encrypt(rsaTest.testMessage);
ok(rsaTest.ct);
});
test("Decrypt", 1, function() {
var pt = rsa.decrypt(rsaTest.ct);
equal(pt, rsaTest.testMessage);
});
test("Sign", 1, function() {
rsaTest.sig = rsa.sign([btoa('iv'), btoa(rsaTest.testMessage)]);
ok(rsaTest.sig);
});
test("Verify", 1, function() {
var res = rsa.verify([btoa('iv'), btoa(rsaTest.testMessage)], rsaTest.sig);
ok(res);
});
});

View File

@ -16,7 +16,7 @@ define(function(require) {
emailAddress, keySize, cryptoMock, keychainMock;
beforeEach(function() {
appController._crypto = cryptoMock = sinon.createStubInstance(PGP);
appController._pgp = cryptoMock = sinon.createStubInstance(PGP);
appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO);
dummyFingerprint = '3A2D39B4E1404190B8B949DE7D7E99036E712926';