From a8c99845247a3324b79b189e105c562ff2be976d Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 6 Mar 2014 18:19:51 +0100 Subject: [PATCH] [WO-185] implement contacts ui to import external public keys --- src/js/app.js | 18 ++- src/js/controller/contacts.js | 124 ++++++++++++++++++++ src/js/controller/navigation.js | 5 + src/js/crypto/pgp.js | 24 +++- src/js/dao/email-dao.js | 6 +- src/js/dao/keychain-dao.js | 44 +++++--- src/sass/all.scss | 1 + src/sass/components/_buttons.scss | 2 +- src/sass/views/_contacts.scss | 42 +++++++ src/sass/views/_navigation.scss | 2 + src/tpl/account.html | 2 +- src/tpl/contacts.html | 45 ++++++++ src/tpl/desktop.html | 3 + src/tpl/login-new-device.html | 2 +- src/tpl/navigation.html | 3 +- test/new-unit/account-ctrl-test.js | 6 +- test/new-unit/contacts-ctrl-test.js | 168 ++++++++++++++++++++++++++++ test/new-unit/email-dao-test.js | 4 +- test/new-unit/keychain-dao-test.js | 24 ++++ test/new-unit/main.js | 1 + test/new-unit/pgp-test.js | 18 +-- 21 files changed, 501 insertions(+), 43 deletions(-) create mode 100644 src/js/controller/contacts.js create mode 100644 src/sass/views/_contacts.scss create mode 100644 src/tpl/contacts.html create mode 100644 test/new-unit/contacts-ctrl-test.js diff --git a/src/js/app.js b/src/js/app.js index d0ec6ac..e37b86c 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -7,6 +7,7 @@ requirejs([ 'js/controller/popover', 'js/controller/add-account', 'js/controller/account', + 'js/controller/contacts', 'js/controller/login', 'js/controller/login-initial', 'js/controller/login-new-device', @@ -24,6 +25,7 @@ requirejs([ PopoverCtrl, AddAccountCtrl, AccountCtrl, + ContactsCtrl, LoginCtrl, LoginInitialCtrl, LoginNewDeviceCtrl, @@ -32,14 +34,25 @@ requirejs([ ReadCtrl, WriteCtrl, NavigationCtrl, - util) { + util +) { 'use strict'; // reset window.name window.name = util.UUID(); // init main angular module including dependencies - var app = angular.module('mail', ['ngRoute', 'ngTouch', 'navigation', 'mail-list', 'write', 'read', 'login-new-device', 'popover']); + var app = angular.module('mail', [ + 'ngRoute', + 'ngTouch', + 'navigation', + 'mail-list', + 'write', + 'read', + 'contacts', + 'login-new-device', + 'popover' + ]); // set router paths app.config(function($routeProvider) { @@ -77,6 +90,7 @@ requirejs([ app.controller('WriteCtrl', WriteCtrl); app.controller('MailListCtrl', MailListCtrl); app.controller('AccountCtrl', AccountCtrl); + app.controller('ContactsCtrl', ContactsCtrl); app.controller('DialogCtrl', DialogCtrl); app.controller('PopoverCtrl', PopoverCtrl); diff --git a/src/js/controller/contacts.js b/src/js/controller/contacts.js new file mode 100644 index 0000000..d1162f2 --- /dev/null +++ b/src/js/controller/contacts.js @@ -0,0 +1,124 @@ +define(function(require) { + 'use strict'; + + var angular = require('angular'), + _ = require('underscore'), + appController = require('js/app-controller'), + keychain, pgp; + + // + // Controller + // + + var ContactsCtrl = function($scope) { + keychain = appController._keychain, + pgp = appController._crypto; + + $scope.state.contacts = { + open: false, + toggle: function(to) { + this.open = to; + $scope.listKeys(); + } + }; + + // set default value so that the popover height is correct on init + $scope.fingerprint = 'XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX'; + + // + // scope functions + // + + $scope.listKeys = function() { + keychain.listLocalPublicKeys(function(err, keys) { + if (err) { + $scope.onError(err); + return; + } + + keys.forEach(addParams); + + $scope.keys = keys; + $scope.$apply(); + + function addParams(key) { + var params = pgp.getKeyParams(key.publicKey); + _.extend(key, params); + } + }); + }; + + $scope.getFingerprint = function(key) { + var fpr = key.fingerprint; + var formatted = fpr.slice(0, 4) + ' ' + fpr.slice(4, 8) + ' ' + fpr.slice(8, 12) + ' ' + fpr.slice(12, 16) + ' ' + fpr.slice(16, 20) + ' ... ' + fpr.slice(20, 24) + ' ' + fpr.slice(24, 28) + ' ' + fpr.slice(28, 32) + ' ' + fpr.slice(32, 36) + ' ' + fpr.slice(36); + + $scope.fingerprint = formatted; + }; + + $scope.importKey = function(publicKeyArmored) { + var keyParams = pgp.getKeyParams(publicKeyArmored); + var pubkey = { + _id: keyParams._id, + userId: keyParams.userId, + publicKey: publicKeyArmored + }; + + keychain.saveLocalPublicKey(pubkey, function(err) { + if (err) { + $scope.onError(err); + return; + } + + // update displayed keys + $scope.listKeys(); + }); + }; + + $scope.removeKey = function(key) { + keychain.removeLocalPublicKey(key._id, function(err) { + if (err) { + $scope.onError(err); + return; + } + + // update displayed keys + $scope.listKeys(); + }); + }; + }; + + // + // Directives + // + + var ngModule = angular.module('contacts', []); + + ngModule.directive('keyfileInput', function() { + return function(scope, elm) { + elm.on('change', function(e) { + for (var i = 0; i < e.target.files.length; i++) { + importKey(e.target.files.item(i)); + } + }); + + function importKey(file) { + var reader = new FileReader(); + reader.onload = function(e) { + scope.importKey(e.target.result); + }; + reader.readAsText(file); + } + }; + }); + + ngModule.directive('keyfileBtn', function() { + return function(scope, elm) { + elm.on('click touchstart', function(e) { + e.preventDefault(); + document.querySelector('#keyfile-input').click(); + }); + }; + }); + + return ContactsCtrl; +}); \ No newline at end of file diff --git a/src/js/controller/navigation.js b/src/js/controller/navigation.js index 5a3af95..73f3c5e 100644 --- a/src/js/controller/navigation.js +++ b/src/js/controller/navigation.js @@ -172,6 +172,11 @@ define(function(require) { e.preventDefault(); scope.state.account.toggle(false); + } else if (e.keyCode === 27 && scope.state.contacts.open) { + // escape -> close contacts view + e.preventDefault(); + scope.state.contacts.toggle(false); + } else if (e.keyCode === 27 && scope.state.nav.open) { // escape -> close nav view e.preventDefault(); diff --git a/src/js/crypto/pgp.js b/src/js/crypto/pgp.js index 023672c..a515047 100644 --- a/src/js/crypto/pgp.js +++ b/src/js/crypto/pgp.js @@ -68,13 +68,8 @@ define(function(require) { return fingerprint(this._publicKey); }; - PGP.prototype.getUserId = function(keyArmored) { - var key = openpgp.key.readArmored(keyArmored).keys[0]; - return key.getUserIds()[0]; - }; - /** - * Show a user's key id + * Show a user's key id. */ PGP.prototype.getKeyId = function(keyArmored) { var key, pubKeyId, privKeyId; @@ -100,6 +95,23 @@ define(function(require) { return pubKeyId; }; + /** + * Read all relevant params of an armored key. + */ + PGP.prototype.getKeyParams = function(keyArmored) { + var key = openpgp.key.readArmored(keyArmored).keys[0], + packet = key.getKeyPacket(); + + return { + _id: packet.getKeyId().toHex().toUpperCase(), + userId: key.getUserIds()[0].split('<')[1].split('>')[0], + fingerprint: util.hexstrdump(packet.getFingerprint()).toUpperCase(), + algorithm: packet.algorithm, + bitSize: packet.getBitSize(), + created: packet.created, + }; + }; + /** * Import the user's key pair */ diff --git a/src/js/dao/email-dao.js b/src/js/dao/email-dao.js index aeb0d9c..b6668a5 100644 --- a/src/js/dao/email-dao.js +++ b/src/js/dao/email-dao.js @@ -156,8 +156,8 @@ define(function(require) { } // check if the key's user ID matches the current account - pubUserID = self._crypto.getUserId(keypair.publicKey.publicKey); - privUserID = self._crypto.getUserId(keypair.privateKey.encryptedKey); + pubUserID = self._crypto.getKeyParams(keypair.publicKey.publicKey).userId; + privUserID = self._crypto.getKeyParams(keypair.privateKey.encryptedKey).userId; if (pubUserID.indexOf(self._account.emailAddress) === -1 || privUserID.indexOf(self._account.emailAddress) === -1) { callback({ errMsg: 'User IDs dont match!' @@ -1342,4 +1342,4 @@ define(function(require) { }; return EmailDAO; -}); +}); \ No newline at end of file diff --git a/src/js/dao/keychain-dao.js b/src/js/dao/keychain-dao.js index c98a760..7669460 100644 --- a/src/js/dao/keychain-dao.js +++ b/src/js/dao/keychain-dao.js @@ -256,31 +256,43 @@ define(function(require) { return; } - if (!pubkey) { - // fetch from cloud storage - self._publicKeyDao.get(id, function(err, cloudPubkey) { + if (pubkey) { + callback(null, pubkey); + return; + } + + // fetch from cloud storage + self._publicKeyDao.get(id, function(err, cloudPubkey) { + if (err) { + callback(err); + return; + } + + // cache public key in cache + self.saveLocalPublicKey(cloudPubkey, function(err) { if (err) { callback(err); return; } - // cache public key in cache - self.saveLocalPublicKey(cloudPubkey, function(err) { - if (err) { - callback(err); - return; - } - - callback(null, cloudPubkey); - }); + callback(null, cloudPubkey); }); - - } else { - callback(null, pubkey); - } + }); }); }; + /** + * List all the locally stored public keys + */ + KeychainDAO.prototype.listLocalPublicKeys = function(callback) { + // search local keyring for public key + this._localDbDao.list('publickey', 0, null, callback); + }; + + KeychainDAO.prototype.removeLocalPublicKey = function(id, callback) { + this._localDbDao.remove('publickey_' + id, callback); + }; + KeychainDAO.prototype.lookupPrivateKey = function(id, callback) { // lookup in local storage this._localDbDao.read('privatekey_' + id, callback); diff --git a/src/sass/all.scss b/src/sass/all.scss index 5c5f594..cb33f9d 100755 --- a/src/sass/all.scss +++ b/src/sass/all.scss @@ -24,6 +24,7 @@ @import "views/shared"; @import "views/add-account"; @import "views/account"; +@import "views/contacts"; @import "views/dialog"; @import "views/navigation"; @import "views/mail-list"; diff --git a/src/sass/components/_buttons.scss b/src/sass/components/_buttons.scss index 024ce38..13051dd 100755 --- a/src/sass/components/_buttons.scss +++ b/src/sass/components/_buttons.scss @@ -20,6 +20,7 @@ transition: background-color 0.3s; text-decoration: none; font-weight: normal; + outline: 0; &:hover, &:focus { @@ -28,7 +29,6 @@ &:active, &.active { - outline: 0; background-image: none; box-shadow: none; top: 1px; diff --git a/src/sass/views/_contacts.scss b/src/sass/views/_contacts.scss new file mode 100644 index 0000000..a4bd33b --- /dev/null +++ b/src/sass/views/_contacts.scss @@ -0,0 +1,42 @@ +.view-contacts { + + .key-controls { + margin: 30px; + + input[type=text] { + line-height: 23px; + } + + input[type=file] { + visibility: hidden; + width: 0; + height: 0; + } + } + + .key-list { + max-height: 400px; + margin: 20px; + overflow-y: scroll; + + table { + th, td { + padding: 5px 10px; + } + + .hover { + cursor: pointer; + } + + button.remove { + font-family: $font-family-icons; + font-size: 0.75em; + color: $color-grey-input; + border: none; + background: none; + outline: none; + } + } + } + +} \ No newline at end of file diff --git a/src/sass/views/_navigation.scss b/src/sass/views/_navigation.scss index ba9ce68..753df15 100755 --- a/src/sass/views/_navigation.scss +++ b/src/sass/views/_navigation.scss @@ -79,6 +79,8 @@ margin-top: em(50, 16); padding: 0 $nav-padding; li { + margin-bottom: 0.25em; + a { font-size: $font-size-big; } diff --git a/src/tpl/account.html b/src/tpl/account.html index 34cb5c4..3c805ba 100644 --- a/src/tpl/account.html +++ b/src/tpl/account.html @@ -6,7 +6,7 @@
+ +
+ + + +
+
Fingerprint
+
{{fingerprint}}
+
+ + \ No newline at end of file diff --git a/src/tpl/desktop.html b/src/tpl/desktop.html index 2ab6f22..1573e6b 100644 --- a/src/tpl/desktop.html +++ b/src/tpl/desktop.html @@ -25,6 +25,9 @@ + \ No newline at end of file diff --git a/src/tpl/login-new-device.html b/src/tpl/login-new-device.html index 9a2995f..2f68c87 100644 --- a/src/tpl/login-new-device.html +++ b/src/tpl/login-new-device.html @@ -8,7 +8,7 @@
- +
diff --git a/src/tpl/navigation.html b/src/tpl/navigation.html index a9ffa8a..64d3c6f 100644 --- a/src/tpl/navigation.html +++ b/src/tpl/navigation.html @@ -16,7 +16,8 @@