From 8c38480db8c391a0135b1766bc1e94dd5d63f1d1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 24 Apr 2017 10:39:05 -0400 Subject: [PATCH] private keys, rsa dec, org keys w/ login dec --- src/background.js | 27 ++- src/models/api/responseModels.js | 20 +- src/models/dataModels.js | 1 + src/models/domainModels.js | 43 ++-- src/popup/app/services/authService.js | 21 +- src/services/apiService.js | 2 +- src/services/cryptoService.js | 320 ++++++++++++++++++++++---- 7 files changed, 348 insertions(+), 86 deletions(-) diff --git a/src/background.js b/src/background.js index c354fe77..22abba7c 100644 --- a/src/background.js +++ b/src/background.js @@ -575,8 +575,8 @@ function autofillPage() { command: 'fillForm', fillScript: fillScript }, { - frameId: pageDetailsToAutoFill[i].frameId - }); + frameId: pageDetailsToAutoFill[i].frameId + }); } } } @@ -660,18 +660,16 @@ function logout(expired, callback) { syncService.setLastSync(new Date(0), function () { settingsService.clear(function () { tokenService.clearToken(function () { - cryptoService.clearKey(function () { - cryptoService.clearKeyHash(function () { - userService.clearUserIdAndEmail(function () { - loginService.clear(userId, function () { - folderService.clear(userId, function () { - chrome.runtime.sendMessage({ - command: 'doneLoggingOut', expired: expired - }); - setIcon(); - refreshBadgeAndMenu(); - callback(); + cryptoService.clearKeys(function () { + userService.clearUserIdAndEmail(function () { + loginService.clear(userId, function () { + folderService.clear(userId, function () { + chrome.runtime.sendMessage({ + command: 'doneLoggingOut', expired: expired }); + setIcon(); + refreshBadgeAndMenu(); + callback(); }); }); }); @@ -758,7 +756,8 @@ function checkLock() { if (diffSeconds >= lockOptionSeconds) { // need to lock now - cryptoService.clearKey(function () { + Q.all([cryptoService.clearKey(), cryptoService.clearOrgKeys()]).then(function () { + cryptoService.clearPrivateKey(); setIcon(); folderService.clearCache(); loginService.clearCache(); diff --git a/src/models/api/responseModels.js b/src/models/api/responseModels.js index 6ae66df7..e55a40b3 100644 --- a/src/models/api/responseModels.js +++ b/src/models/api/responseModels.js @@ -1,5 +1,6 @@ var CipherResponse = function (response) { this.id = response.Id; + this.organizationId = response.OrganizationId; this.folderId = response.FolderId; this.type = response.Type; this.favorite = response.Favorite; @@ -15,6 +16,7 @@ var FolderResponse = function (response) { var LoginResponse = function (response) { this.id = response.Id; + this.organizationId = response.OrganizationId; this.folderId = response.FolderId; this.name = response.Name; this.uri = response.Uri; @@ -36,6 +38,22 @@ var ProfileResponse = function (response) { this.masterPasswordHint = response.MasterPasswordHint; this.culture = response.Culture; this.twoFactorEnabled = response.TwoFactorEnabled; + + this.organizations = []; + if (response.Organizations) { + for (var i = 0; i < response.Organizations.length; i++) { + this.organizations.push(new ProfileOrganizationResponse(response.Organizations[i])); + } + } +}; + +var ProfileOrganizationResponse = function (response) { + this.id = response.Id; + this.name = response.Name; + this.key = response.Key; + this.status = response.Status; + this.type = response.Type; + this.enabled = response.Enabled; }; var IdentityTokenResponse = function (response) { @@ -44,7 +62,7 @@ var IdentityTokenResponse = function (response) { this.refreshToken = response.refresh_token; this.tokenType = response.token_type; - // TODO: extras + this.privateKey = response.PrivateKey; }; var ListResponse = function (data) { diff --git a/src/models/dataModels.js b/src/models/dataModels.js index d1713b80..f7d4be23 100644 --- a/src/models/dataModels.js +++ b/src/models/dataModels.js @@ -17,6 +17,7 @@ var FolderData = function (response, userId) { var LoginData = function (response, userId) { this.id = response.id; + this.organizationId = response.organizationId; this.folderId = response.folderId; this.userId = userId; diff --git a/src/models/domainModels.js b/src/models/domainModels.js index b8f339f0..864e63c8 100644 --- a/src/models/domainModels.js +++ b/src/models/domainModels.js @@ -87,6 +87,7 @@ var CipherString = function () { var Login = function (obj, alreadyEncrypted) { this.id = obj.id ? obj.id : null; + this.organizationId = obj.organizationId ? obj.organizationId : null; this.folderId = obj.folderId ? obj.folderId : null; this.favorite = obj.favorite ? true : false; @@ -118,41 +119,41 @@ var Folder = function (obj, alreadyEncrypted) { }; !function () { - CipherString.prototype.decrypt = function (callback) { - var deferred = Q.defer(); - - if (!this.decryptedValue) { - var cryptoService = chrome.extension.getBackgroundPage().cryptoService; - cryptoService.decrypt(this).then(function (decValue) { - this.decryptedValue = decValue; - deferred.resolve(this.decryptedValue); - }, function () { - this.decryptedValue = '[error: cannot decrypt]'; - deferred.resolve(this.decryptedValue); - }); - } - else { - callback(this.decryptedValue); + CipherString.prototype.decrypt = function (orgId) { + if (this.decryptedValue) { + var deferred = Q.defer(); deferred.resolve(this.decryptedValue); + return deferred.promise; } - return deferred.promise; + var self = this; + var cryptoService = chrome.extension.getBackgroundPage().cryptoService; + return cryptoService.getOrgKey(orgId).then(function (orgKey) { + return cryptoService.decrypt(self, orgKey); + }).then(function (decValue) { + self.decryptedValue = decValue; + return self.decryptedValue; + }).catch(function () { + self.decryptedValue = '[error: cannot decrypt]'; + return self.decryptedValue; + }); }; Login.prototype.decrypt = function () { var self = this; var model = { id: self.id, + organizationId: self.organizationId, folderId: self.folderId, favorite: self.favorite }; var deferred = Q.defer(); - self.name.decrypt().then(function (val) { + self.name.decrypt(self.organizationId).then(function (val) { model.name = val; if (self.uri) { - return self.uri.decrypt(); + return self.uri.decrypt(self.organizationId); } return null; }).then(function (val) { @@ -162,19 +163,19 @@ var Folder = function (obj, alreadyEncrypted) { model.domain = utilsService.getDomain(val); if (self.username) { - return self.username.decrypt(); + return self.username.decrypt(self.organizationId); } return null; }).then(function (val) { model.username = val; if (self.password) { - return self.password.decrypt(); + return self.password.decrypt(self.organizationId); } return null; }).then(function (val) { model.password = val; if (self.notes) { - return self.notes.decrypt(); + return self.notes.decrypt(self.organizationId); } return null; }).then(function (val) { diff --git a/src/popup/app/services/authService.js b/src/popup/app/services/authService.js index 29034311..169ad769 100644 --- a/src/popup/app/services/authService.js +++ b/src/popup/app/services/authService.js @@ -24,8 +24,20 @@ cryptoService.setKey(key, function () { cryptoService.setKeyHash(hashedPassword, function () { userService.setUserIdAndEmail(tokenService.getUserId(), tokenService.getEmail(), function () { - chrome.runtime.sendMessage({ command: 'loggedIn' }); - deferred.resolve(false); + if (!response.privateKey) { + loggedIn(deferred); + return; + } + + cryptoService.setEncPrivateKey(response.privateKey).then(function () { + apiService.getProfile(function (profile) { + cryptoService.setOrgKeys(profile.organizations).then(function () { + loggedIn(deferred); + }); + }, function () { + loggedIn(deferred); + }); + }); }); }); }); @@ -48,5 +60,10 @@ callback(); }; + function loggedIn(deferred) { + chrome.runtime.sendMessage({ command: 'loggedIn' }); + deferred.resolve(false); + } + return _service; }); diff --git a/src/services/apiService.js b/src/services/apiService.js index ffd87fe4..5f09b873 100644 --- a/src/services/apiService.js +++ b/src/services/apiService.js @@ -283,7 +283,7 @@ function initApiService() { handleTokenState(self).then(function (token) { $.ajax({ type: 'GET', - url: self.baseUrl + '/ciphers?access_token2=' + token, + url: self.baseUrl + '/ciphers?includeFolders=true&includeShared=true&access_token2=' + token, dataType: 'json', success: function (response) { var data = []; diff --git a/src/services/cryptoService.js b/src/services/cryptoService.js index 7eff8cd5..7e496c06 100644 --- a/src/services/cryptoService.js +++ b/src/services/cryptoService.js @@ -7,7 +7,8 @@ function initCryptoService(constantsService) { var _key, _legacyEtmKey, _keyHash, - _b64KeyHash; + _privateKey, + _orgKeys; CryptoService.prototype.setKey = function (key, callback) { if (!callback || typeof callback !== 'function') { @@ -46,6 +47,35 @@ function initCryptoService(constantsService) { }); } + CryptoService.prototype.setEncPrivateKey = function (encPrivateKey) { + var deferred = Q.defer(); + + chrome.storage.local.set({ + 'encPrivateKey': encPrivateKey + }, function () { + deferred.resolve(); + }); + + return deferred.promise; + } + + CryptoService.prototype.setOrgKeys = function (orgs) { + var deferred = Q.defer(); + + var orgKeys = {}; + for (var i = 0; i < orgs.length; i++) { + orgKeys[orgs[i].id] = orgs[i].key; + } + + chrome.storage.local.set({ + 'encOrgKeys': orgKeys + }, function () { + deferred.resolve(); + }); + + return deferred.promise; + } + CryptoService.prototype.getKey = function (callback) { if (!callback || typeof callback !== 'function') { throw 'callback function required'; @@ -93,24 +123,138 @@ function initCryptoService(constantsService) { }); }; - CryptoService.prototype.clearKey = function (callback) { - if (!callback || typeof callback !== 'function') { - throw 'callback function required'; + CryptoService.prototype.getPrivateKey = function () { + var deferred = Q.defer(); + if (_privateKey) { + deferred.resolve(_privateKey); + return deferred.promise; } - _key = _legacyEtmKey = null; - chrome.storage.local.remove('key', function () { - callback(); + var self = this; + chrome.storage.local.get('encPrivateKey', function (obj) { + if (!obj || !obj.encPrivateKey) { + deferred.reject('Cannot get enc private key.'); + return; + } + + self.decrypt(new CipherString(obj.encPrivateKey), null, 'raw').then(function (privateKey) { + var privateKeyB64 = forge.util.encode64(privateKey); + _privateKey = fromB64ToBuffer(privateKeyB64); + deferred.resolve(_privateKey); + }, function () { + deferred.reject('Cannot get private key. Decryption failed.'); + }); + }); + + return deferred.promise; + }; + + CryptoService.prototype.getOrgKeys = function () { + var deferred = Q.defer(); + + if (_orgKeys && _orgKeys.length) { + deferred.resolve(_orgKeys); + return deferred.promise; + } + + var self = this; + chrome.storage.local.get('encOrgKeys', function (obj) { + if (obj && obj.encOrgKeys) { + var orgKeys = {}, + setKey = false; + + var decPromises = []; + for (var orgId in obj.encOrgKeys) { + if (obj.encOrgKeys.hasOwnProperty(orgId)) { + (function (orgIdInstance) { + var promise = self.rsaDecrypt(obj.encOrgKeys[orgIdInstance]).then(function (decValueB64) { + orgKeys[orgIdInstance] = new SymmetricCryptoKey(decValueB64, true); + setKey = true; + }, function (err) { + console.log('getOrgKeys error: ' + err); + }); + decPromises.push(promise); + })(orgId); + } + } + + Q.all(decPromises).then(function () { + if (setKey) { + _orgKeys = orgKeys; + } + + deferred.resolve(_orgKeys); + }); + } + else { + deferred.resolve(null); + } + }); + + return deferred.promise; + }; + + CryptoService.prototype.getOrgKey = function (orgId) { + if (!orgId) { + var deferred = Q.defer(); + deferred.resolve(null); + return deferred.promise; + } + + return this.getOrgKeys().then(function (orgKeys) { + if (!orgKeys || !(orgId in orgKeys)) { + return null; + } + + return orgKeys[orgId]; }); }; + CryptoService.prototype.clearKey = function (callback) { + var deferred = Q.defer(); + + _key = _legacyEtmKey = null; + chrome.storage.local.remove('key', function () { + deferred.resolve(); + }); + + return deferred.promise; + }; + CryptoService.prototype.clearKeyHash = function (callback) { + var deferred = Q.defer(); + + _keyHash = null; + chrome.storage.local.remove('keyHash', function () { + deferred.resolve(); + }); + + return deferred.promise; + }; + + CryptoService.prototype.clearPrivateKey = function () { + _privateKey = null; + }; + + CryptoService.prototype.clearOrgKeys = function () { + var deferred = Q.defer(); + + _orgKeys = {}; + chrome.storage.local.remove('encOrgKeys', function () { + deferred.resolve(); + }); + + return deferred.promise; + }; + + CryptoService.prototype.clearKeys = function (callback) { if (!callback || typeof callback !== 'function') { throw 'callback function required'; } - _keyHash = null; - chrome.storage.local.remove('keyHash', function () { + var self = this; + Q.all([self.clearKey(), self.clearKeyHash(), self.clearOrgKeys()]).then(function () { + self.clearPrivateKey(); callback(); }); }; @@ -125,7 +269,7 @@ function initCryptoService(constantsService) { 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 - self.clearKey(function () { + self.clearKey().then(function () { _key = key; callback(); return; @@ -201,42 +345,42 @@ function initCryptoService(constantsService) { var deferred = Q.defer(); var self = this; - if (cipherString === null || cipherString === undefined || !cipherString.encryptedString) { - throw 'cannot decrypt nothing'; - } - - self.getKey(function (localKey) { - key = key || localKey; - if (!key) { - throw 'Encryption key unavailable.'; + try { + if (cipherString === null || cipherString === undefined || !cipherString.encryptedString) { + throw 'cannot decrypt nothing'; } - outputEncoding = outputEncoding || 'utf8'; - - if (cipherString.encryptionType === constantsService.encType.AesCbc128_HmacSha256_B64 && - key.encType === constantsService.encType.AesCbc256_B64) { - // Old encrypt-then-mac scheme, swap out the key - _legacyEtmKey = _legacyEtmKey || - new SymmetricCryptoKey(key.key, false, constantsService.encType.AesCbc128_HmacSha256_B64); - key = _legacyEtmKey; - } - - if (cipherString.encryptionType !== key.encType) { - throw 'encType unavailable.'; - } - - var ivBytes = forge.util.decode64(cipherString.initializationVector); - var ctBytes = forge.util.decode64(cipherString.cipherText); - - if (key.macKey && cipherString.mac) { - var computedMac = computeMac(ctBytes, ivBytes, key.macKey); - if (computedMac !== cipherString.mac) { - console.error('MAC failed.'); - deferred.reject('MAC failed.'); + self.getKey(function (localKey) { + key = key || localKey; + if (!key) { + throw 'Encryption key unavailable.'; + } + + outputEncoding = outputEncoding || 'utf8'; + + if (cipherString.encryptionType === constantsService.encType.AesCbc128_HmacSha256_B64 && + key.encType === constantsService.encType.AesCbc256_B64) { + // Old encrypt-then-mac scheme, swap out the key + _legacyEtmKey = _legacyEtmKey || + new SymmetricCryptoKey(key.key, false, constantsService.encType.AesCbc128_HmacSha256_B64); + key = _legacyEtmKey; + } + + if (cipherString.encryptionType !== key.encType) { + throw 'encType unavailable.'; + } + + var ivBytes = forge.util.decode64(cipherString.initializationVector); + var ctBytes = forge.util.decode64(cipherString.cipherText); + + if (key.macKey && cipherString.mac) { + var computedMac = computeMac(ctBytes, ivBytes, key.macKey); + if (computedMac !== cipherString.mac) { + console.error('MAC failed.'); + deferred.reject('MAC failed.'); + } } - } - try { var ctBuffer = forge.util.createBuffer(ctBytes); var decipher = forge.cipher.createDecipher('AES-CBC', key.encKey); decipher.start({ iv: ivBytes }); @@ -250,16 +394,78 @@ function initCryptoService(constantsService) { else { decValue = decipher.output.getBytes(); } + deferred.resolve(decValue); - } - catch (e) { - deferred.reject('Decryption failed.'); - } - }); + }); + } + catch (e) { + deferred.reject('Decryption failed.'); + } return deferred.promise; }; + CryptoService.prototype.rsaDecrypt = function (encValue) { + var headerPieces = encValue.split('.'), + encType, + encPiece; + + if (headerPieces.length === 1) { + encType = constantsService.encType.Rsa2048_OaepSha256_B64; + encPiece = headerPieces[0]; + } + else if (headerPieces.length === 2) { + try { + encType = parseInt(headerPieces[0]); + encPiece = headerPieces[1]; + } + catch (e) { } + } + + var padding = null; + if (encType === constantsService.encType.Rsa2048_OaepSha256_B64) { + padding = { + name: 'RSA-OAEP', + hash: { name: 'SHA-256' } + } + } + else if (encType === constantsService.encType.Rsa2048_OaepSha1_B64) { + padding = { + name: 'RSA-OAEP', + hash: { name: 'SHA-1' } + } + } + else { + throw 'encType unavailable.'; + } + + return this.getPrivateKey().then(function (privateKeyBytes) { + if (!privateKeyBytes) { + throw 'No private key.'; + } + + if (!padding) { + throw 'Cannot determine padding.'; + } + + return window.crypto.subtle.importKey('pkcs8', privateKeyBytes, padding, false, ['decrypt']); + }).then(function (privateKey) { + if (!encPiece) { + throw 'encPiece unavailable.'; + } + + var ctBytes = fromB64ToBuffer(encPiece); + return window.crypto.subtle.decrypt({ name: padding.name }, privateKey, ctBytes); + }, function () { + throw 'Cannot import privateKey.'; + }).then(function (decBytes) { + var b64DecValue = toB64FromBuffer(decBytes); + return b64DecValue; + }, function () { + throw 'Cannot rsa decrypt.'; + }); + }; + function computeMac(ct, iv, macKey) { var hmac = forge.hmac.create(); hmac.start('sha256', macKey); @@ -315,4 +521,24 @@ function initCryptoService(constantsService) { throw 'Unsupported encType/key length.'; } } + + function fromB64ToBuffer(str) { + var binary_string = window.atob(str); + var len = binary_string.length; + var bytes = new Uint8Array(len); + for (var i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + return bytes.buffer; + } + + function toB64FromBuffer(buffer) { + var binary = ''; + var bytes = new Uint8Array(buffer); + var len = bytes.byteLength; + for (var i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + return window.btoa(binary); + } };