From 16098a174388f9de07cebf462b95b0c2fb5d7edc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 2 Jun 2017 00:10:29 -0400 Subject: [PATCH] support user encryption key --- src/background.js | 9 +- src/models/api/requestModels.js | 3 +- src/models/api/responseModels.js | 4 + src/models/domainModels.js | 9 +- .../accounts/accountsRegisterController.js | 24 ++-- src/popup/app/config.js | 2 +- src/popup/app/services/authService.js | 9 +- src/services/apiService.js | 8 +- src/services/cryptoService.js | 119 +++++++++++++++--- src/services/folderService.js | 2 +- src/services/loginService.js | 2 +- src/services/syncService.js | 85 ++++++------- src/services/userService.js | 46 ++++++- 13 files changed, 218 insertions(+), 104 deletions(-) diff --git a/src/background.js b/src/background.js index c9e3c8115e..1f689e76a1 100644 --- a/src/background.js +++ b/src/background.js @@ -11,7 +11,8 @@ var userService = new UserService(tokenService, apiService, cryptoService); var settingsService = new SettingsService(userService); var loginService = new LoginService(cryptoService, userService, apiService, settingsService); var folderService = new FolderService(cryptoService, userService, apiService); -var syncService = new SyncService(loginService, folderService, userService, apiService, settingsService, cryptoService); +var syncService = new SyncService(loginService, folderService, userService, apiService, settingsService, + cryptoService, logout); var autofillService = new AutofillService(); var passwordGenerationService = new PasswordGenerationService(); @@ -86,7 +87,7 @@ chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) { setIcon(); function setIcon() { userService.isAuthenticated(function (isAuthenticated) { - cryptoService.getKey(function (key) { + cryptoService.getKey().then(function (key) { var suffix = ''; if (!isAuthenticated) { suffix = '_gray'; @@ -668,7 +669,7 @@ function logout(expired, callback) { settingsService.clear(function () { tokenService.clearToken(function () { cryptoService.clearKeys(function () { - userService.clearUserIdAndEmail(function () { + userService.clear(function () { loginService.clear(userId, function () { folderService.clear(userId, function () { chrome.runtime.sendMessage({ @@ -757,7 +758,7 @@ function checkLock() { return; } - cryptoService.getKey(function (key) { + cryptoService.getKey().then(function (key) { if (!key) { // no key so no need to lock return; diff --git a/src/models/api/requestModels.js b/src/models/api/requestModels.js index 2d8efdae43..53257649c9 100644 --- a/src/models/api/requestModels.js +++ b/src/models/api/requestModels.js @@ -48,11 +48,12 @@ var TokenRequest = function (email, masterPasswordHash, token, device) { }; }; -var RegisterRequest = function (email, masterPasswordHash, masterPasswordHint) { +var RegisterRequest = function (email, masterPasswordHash, masterPasswordHint, key) { this.name = null; this.email = email; this.masterPasswordHash = masterPasswordHash; this.masterPasswordHint = masterPasswordHint ? masterPasswordHint : null; + this.key = key; }; var PasswordHintRequest = function (email) { diff --git a/src/models/api/responseModels.js b/src/models/api/responseModels.js index 695edd4e8f..76b8b9dca5 100644 --- a/src/models/api/responseModels.js +++ b/src/models/api/responseModels.js @@ -38,6 +38,9 @@ var ProfileResponse = function (response) { this.masterPasswordHint = response.MasterPasswordHint; this.culture = response.Culture; this.twoFactorEnabled = response.TwoFactorEnabled; + this.key = response.Key; + this.privateKey = response.PrivateKey; + this.securityStamp = response.SecurityStamp; this.organizations = []; if (response.Organizations) { @@ -68,6 +71,7 @@ var IdentityTokenResponse = function (response) { this.tokenType = response.token_type; this.privateKey = response.PrivateKey; + this.key = response.Key; }; var ListResponse = function (data) { diff --git a/src/models/domainModels.js b/src/models/domainModels.js index a2c3b30d1b..ea080a0ec9 100644 --- a/src/models/domainModels.js +++ b/src/models/domainModels.js @@ -9,13 +9,8 @@ var CipherString = function () { var constants = chrome.extension.getBackgroundPage().constantsService; if (arguments.length >= 2) { - // ct and optional header - if (arguments[0] === constants.encType.AesCbc256_B64) { - this.encryptedString = arguments[1]; - } - else { - this.encryptedString = arguments[0] + '.' + arguments[1]; - } + // ct and header + this.encryptedString = arguments[0] + '.' + arguments[1]; // iv if (arguments.length > 2 && arguments[2]) { diff --git a/src/popup/app/accounts/accountsRegisterController.js b/src/popup/app/accounts/accountsRegisterController.js index 4312f9e041..36de3fa2c6 100644 --- a/src/popup/app/accounts/accountsRegisterController.js +++ b/src/popup/app/accounts/accountsRegisterController.js @@ -2,8 +2,8 @@ .module('bit.accounts') .controller( - 'accountsRegisterController', - function ($scope, $state, cryptoService, toastr, $q, apiService, utilsService, $analytics, i18nService) { + 'accountsRegisterController', + function ($scope, $state, cryptoService, toastr, $q, apiService, utilsService, $analytics, i18nService) { $scope.i18n = i18nService; $scope.model = {}; @@ -45,15 +45,17 @@ function registerPromise(key, masterPassword, email, hint) { return $q(function (resolve, reject) { - cryptoService.hashPassword(masterPassword, key, function (hashedPassword) { - var request = new RegisterRequest(email, hashedPassword, hint); - apiService.postRegister(request, - function () { - resolve(); - }, - function (error) { - reject(error); - }); + cryptoService.makeEncKey(key).then(function (encKey) { + cryptoService.hashPassword(masterPassword, key, function (hashedPassword) { + var request = new RegisterRequest(email, hashedPassword, hint, encKey.encryptedString); + apiService.postRegister(request, + function () { + resolve(); + }, + function (error) { + reject(error); + }); + }); }); }); } diff --git a/src/popup/app/config.js b/src/popup/app/config.js index b6860ca5d2..bda3de7cde 100644 --- a/src/popup/app/config.js +++ b/src/popup/app/config.js @@ -14,7 +14,7 @@ var userService = $injector.get('userService'); var cryptoService = $injector.get('cryptoService'); - cryptoService.getKey(function (key) { + cryptoService.getKey().then(function (key) { userService.isAuthenticated(function (isAuthenticated) { if (isAuthenticated) { if (!key) { diff --git a/src/popup/app/services/authService.js b/src/popup/app/services/authService.js index de9282a331..4c9790db31 100644 --- a/src/popup/app/services/authService.js +++ b/src/popup/app/services/authService.js @@ -24,12 +24,9 @@ cryptoService.setKey(key, function () { cryptoService.setKeyHash(hashedPassword, function () { userService.setUserIdAndEmail(tokenService.getUserId(), tokenService.getEmail(), function () { - if (!response.privateKey) { - loggedIn(deferred); - return; - } - - cryptoService.setEncPrivateKey(response.privateKey).then(function () { + cryptoService.setEncKey(response.key).then(function () { + return cryptoService.setEncPrivateKey(response.privateKey); + }).then(function () { loggedIn(deferred); }); }); diff --git a/src/services/apiService.js b/src/services/apiService.js index 9fb8c39391..0e192c5ba0 100644 --- a/src/services/apiService.js +++ b/src/services/apiService.js @@ -4,16 +4,16 @@ function ApiService(tokenService, appIdService, utilsService, logoutCallback) { //this.identityBaseUrl = 'http://localhost:33656'; // Desktop external - //this.baseUrl = 'http://192.168.1.8:4000'; - //this.identityBaseUrl = 'http://192.168.1.8:33656'; + this.baseUrl = 'http://192.168.1.6:4000'; + this.identityBaseUrl = 'http://192.168.1.6:33656'; // Preview //this.baseUrl = 'https://preview-api.bitwarden.com'; //this.identityBaseUrl = 'https://preview-identity.bitwarden.com'; // Production - this.baseUrl = 'https://api.bitwarden.com'; - this.identityBaseUrl = 'https://identity.bitwarden.com'; + //this.baseUrl = 'https://api.bitwarden.com'; + //this.identityBaseUrl = 'https://identity.bitwarden.com'; this.tokenService = tokenService; this.logoutCallback = logoutCallback; diff --git a/src/services/cryptoService.js b/src/services/cryptoService.js index 4d6ed8dad6..ba25834e73 100644 --- a/src/services/cryptoService.js +++ b/src/services/cryptoService.js @@ -5,6 +5,7 @@ function CryptoService(constantsService) { function initCryptoService(constantsService) { var _key, + _encKey, _legacyEtmKey, _keyHash, _privateKey, @@ -47,12 +48,26 @@ function initCryptoService(constantsService) { }); } + CryptoService.prototype.setEncKey = function (encKey) { + var deferred = Q.defer(); + + chrome.storage.local.set({ + 'encKey': encKey + }, function () { + _encKey = null; + deferred.resolve(); + }); + + return deferred.promise; + } + CryptoService.prototype.setEncPrivateKey = function (encPrivateKey) { var deferred = Q.defer(); chrome.storage.local.set({ 'encPrivateKey': encPrivateKey }, function () { + _privateKey = null; deferred.resolve(); }); @@ -76,21 +91,19 @@ function initCryptoService(constantsService) { return deferred.promise; } - CryptoService.prototype.getKey = function (callback) { - if (!callback || typeof callback !== 'function') { - throw 'callback function required'; - } + CryptoService.prototype.getKey = function () { + var deferred = Q.defer(); if (_key) { - callback(_key); - return; + deferred.resolve(_key); + return deferred.promise; } var self = this; chrome.storage.local.get(self.constantsService.lockOptionKey, function (obj) { if (obj && (obj[self.constantsService.lockOptionKey] || obj[self.constantsService.lockOptionKey] === 0)) { // if we have a lock option set, we do not try to fetch the storage key since it should not even be there - callback(null); + deferred.resolve(null); return; } @@ -99,9 +112,11 @@ function initCryptoService(constantsService) { _key = new SymmetricCryptoKey(obj.key, true); } - callback(_key); + deferred.resolve(_key); }); }); + + return deferred.promise; }; CryptoService.prototype.getKeyHash = function (callback) { @@ -123,6 +138,33 @@ function initCryptoService(constantsService) { }); }; + CryptoService.prototype.getEncKey = function () { + var deferred = Q.defer(); + if (_encKey) { + deferred.resolve(_encKey); + return deferred.promise; + } + + var self = this; + chrome.storage.local.get('encKey', function (obj) { + if (!obj || !obj.encKey) { + deferred.resolve(null); + return; + } + + self.getKey().then(function (key) { + return self.decrypt(new CipherString(obj.encKey), key, 'raw'); + }).then(function (encKey) { + _encKey = new SymmetricCryptoKey(encKey); + deferred.resolve(_encKey); + }, function () { + deferred.reject('Cannot get enc key. Decryption failed.'); + }); + }); + + return deferred.promise; + }; + CryptoService.prototype.getPrivateKey = function () { var deferred = Q.defer(); if (_privateKey) { @@ -232,8 +274,26 @@ function initCryptoService(constantsService) { return deferred.promise; }; + CryptoService.prototype.clearEncKey = function () { + var deferred = Q.defer(); + + _encKey = null; + chrome.storage.local.remove('encKey', function () { + deferred.resolve(); + }); + + return deferred.promise; + }; + CryptoService.prototype.clearPrivateKey = function () { + var deferred = Q.defer(); + _privateKey = null; + chrome.storage.local.remove('encPrivateKey', function () { + deferred.resolve(); + }); + + return deferred.promise; }; CryptoService.prototype.clearOrgKeys = function (memoryOnly) { @@ -258,8 +318,13 @@ function initCryptoService(constantsService) { } var self = this; - Q.all([self.clearKey(), self.clearKeyHash(), self.clearOrgKeys()]).then(function () { - self.clearPrivateKey(); + Q.all([ + self.clearKey(), + self.clearKeyHash(), + self.clearOrgKeys(), + self.clearEncKey(), + self.clearPrivateKey() + ]).then(function () { callback(); }); }; @@ -270,7 +335,7 @@ function initCryptoService(constantsService) { } var self = this; - self.getKey(function (key) { + self.getKey().then(function (key) { chrome.storage.local.get(self.constantsService.lockOptionKey, function (obj) { if (obj && (obj[self.constantsService.lockOptionKey] || obj[self.constantsService.lockOptionKey] === 0)) { // if we have a lock option set, clear the key @@ -299,7 +364,7 @@ function initCryptoService(constantsService) { }; CryptoService.prototype.hashPassword = function (password, key, callback) { - this.getKey(function (storedKey) { + this.getKey().then(function (storedKey) { key = key || storedKey; if (!password || !key) { @@ -311,6 +376,11 @@ function initCryptoService(constantsService) { }); }; + CryptoService.prototype.makeEncKey = function (key) { + var bytes = forge.random.getBytesSync(512 / 8); + return this.encrypt(bytes, key, 'raw'); + }; + CryptoService.prototype.encrypt = function (plainValue, key, plainValueEncoding) { var self = this; var deferred = Q.defer(); @@ -319,8 +389,8 @@ function initCryptoService(constantsService) { deferred.resolve(null); } else { - self.getKey(function (localKey) { - key = key || localKey; + getKeyForEncryption(self, key).then(function (keyToUse) { + key = keyToUse; if (!key) { deferred.reject('Encryption key unavailable.'); return; @@ -356,8 +426,8 @@ function initCryptoService(constantsService) { return; } - self.getKey(function (localKey) { - key = key || localKey; + getKeyForEncryption(self, key).then(function (keyToUse) { + key = keyToUse; if (!key) { deferred.reject('Encryption key unavailable.'); return; @@ -479,6 +549,23 @@ function initCryptoService(constantsService) { return b64Output ? forge.util.encode64(mac.getBytes()) : mac.getBytes(); } + function getKeyForEncryption(self, key) { + var deferred = Q.defer(); + + if (key) { + deferred.resolve(key); + } + else { + self.getEncKey().then(function (encKey) { + return encKey || self.getKey(); + }).then(function (keyToUse) { + deferred.resolve(keyToUse); + }); + } + + return deferred.promise; + } + // Safely compare two MACs in a way that protects against timing attacks (Double HMAC Verification). // ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/ function macsEqual(macKey, mac1, mac2) { diff --git a/src/services/folderService.js b/src/services/folderService.js index b02b296ef9..5abe6c57b7 100644 --- a/src/services/folderService.js +++ b/src/services/folderService.js @@ -68,7 +68,7 @@ function initFolderService() { var deferred = Q.defer(); var self = this; - cryptoService.getKey(function (key) { + cryptoService.getKey().then(function (key) { if (!key) { deferred.reject(); return; diff --git a/src/services/loginService.js b/src/services/loginService.js index 5148f337ea..cddfc06977 100644 --- a/src/services/loginService.js +++ b/src/services/loginService.js @@ -91,7 +91,7 @@ function initLoginService() { var deferred = Q.defer(); var self = this; - cryptoService.getKey(function (key) { + cryptoService.getKey().then(function (key) { if (!key) { deferred.reject(); return; diff --git a/src/services/syncService.js b/src/services/syncService.js index d7fb5e369e..7b36b6f6da 100644 --- a/src/services/syncService.js +++ b/src/services/syncService.js @@ -1,11 +1,13 @@ -function SyncService(loginService, folderService, userService, apiService, settingsService, cryptoService) { +function SyncService(loginService, folderService, userService, apiService, settingsService, + cryptoService, logoutCallback) { this.loginService = loginService; this.folderService = folderService; this.userService = userService; this.apiService = apiService; this.settingsService = settingsService; - this.cryptoService = settingsService; + this.cryptoService = cryptoService; this.syncInProgress = false; + this.logoutCallback = logoutCallback; initSyncService(); }; @@ -37,12 +39,12 @@ function initSyncService() { return; } - syncProfile().then(function () { - return syncFolders(userId); + syncProfile(self).then(function () { + return syncFolders(self, userId); }).then(function () { - return syncCiphers(userId); + return syncCiphers(self, userId); }).then(function () { - return syncSettings(userId); + return syncSettings(self, userId); }).then(function () { self.setLastSync(now, function () { self.syncCompleted(true); @@ -80,45 +82,37 @@ function initSyncService() { }); } - function syncProfile() { + function syncProfile(self) { var deferred = Q.defer(); - var self = this; - - function setKeys(hasPrivateKey, response, d) { - if (response.organizations && response.organizations.length && !hasPrivateKey) { - self.apiService.getKeys(function (keysResponse) { - if (keysResponse.privateKey) { - self.cryptoService.setEncPrivateKey(keysResponse.privateKey).then(function () { - return self.cryptoService.setOrgKeys(response.organizations); - }, function () { - d.reject(); - }).then(function () { - d.resolve(); - }, function () { - d.reject(); - }); - } - else { - d.resolve(); - } - }, function () { - d.reject(); - }); - } - else { - self.cryptoService.setOrgKeys(response.organizations).then(function () { - d.resolve(); - }, function () { - d.reject(); - }); - } - } self.apiService.getProfile(function (response) { - self.cryptoService.getPrivateKey().then(function (privateKey) { - setKeys(!!privateKey, response, deferred); + self.userService.getSecurityStamp().then(function (stamp) { + if (stamp && stamp != response.securityStamp) { + if (self.logoutCallback) { + self.logoutCallback(true, function () { }); + } + + deferred.reject(); + return; + } + + return self.cryptoService.setEncKey(response.key); + }).then(function () { + return self.cryptoService.setEncPrivateKey(response.privateKey); }, function () { - setKeys(true, response, deferred); + deferred.reject(); + }).then(function () { + return self.cryptoService.setOrgKeys(response.organizations); + }, function () { + deferred.reject(); + }).then(function () { + return self.userService.setSecurityStamp(response.securityStamp); + }, function () { + deferred.reject(); + }).then(function () { + deferred.resolve(); + }, function () { + deferred.reject(); }); }, function () { deferred.reject(); @@ -127,9 +121,8 @@ function initSyncService() { return deferred.promise } - function syncFolders(userId) { + function syncFolders(self, userId) { var deferred = Q.defer(); - var self = this; self.apiService.getFolders(function (response) { var folders = {}; @@ -148,9 +141,8 @@ function initSyncService() { return deferred.promise } - function syncCiphers(userId) { + function syncCiphers(self, userId) { var deferred = Q.defer(); - var self = this; self.apiService.getCiphers(function (response) { var logins = {}; @@ -172,9 +164,8 @@ function initSyncService() { return deferred.promise } - function syncSettings(userId) { + function syncSettings(self, userId) { var deferred = Q.defer(); - var self = this; var ciphers = self.apiService.getIncludedDomains(function (response) { var eqDomains = []; diff --git a/src/services/userService.js b/src/services/userService.js index c818815eac..72834e6ec1 100644 --- a/src/services/userService.js +++ b/src/services/userService.js @@ -8,10 +8,12 @@ function initUserService() { var userIdKey = 'userId', - userEmailKey = 'userEmail'; + userEmailKey = 'userEmail', + stampKey = 'securityStamp'; var _userId = null, - _email = null; + _email = null, + _stamp = null; UserService.prototype.setUserIdAndEmail = function (userId, email, callback) { if (!callback || typeof callback !== 'function') { @@ -33,6 +35,20 @@ function initUserService() { }); }; + UserService.prototype.setSecurityStamp = function (stamp) { + var deferred = Q.defer(); + + _stamp = stamp; + var stampObj = {}; + stampObj[stampKey] = stamp; + + chrome.storage.local.set(stampObj, function () { + deferred.resolve(); + }); + + return deferred.promise + }; + UserService.prototype.getUserId = function (callback) { if (!callback || typeof callback !== 'function') { throw 'callback function required'; @@ -69,15 +85,35 @@ function initUserService() { }); }; - UserService.prototype.clearUserIdAndEmail = function (callback) { + UserService.prototype.getSecurityStamp = function () { + var deferred = Q.defer(); + + if (_stamp) { + deferred.resolve(_stamp); + } + + chrome.storage.local.get(stampKey, function (obj) { + if (obj && obj[stampKey]) { + _stamp = obj[stampKey]; + } + + deferred.resolve(_stamp); + }); + + return deferred.promise + }; + + UserService.prototype.clear = function (callback) { if (!callback || typeof callback !== 'function') { throw 'callback function required'; } - _userId = _email = null; + _userId = _email = _stamp = null; chrome.storage.local.remove(userIdKey, function () { chrome.storage.local.remove(userEmailKey, function () { - callback(); + chrome.storage.local.remove(stampKey, function () { + callback(); + }); }); }); };