diff --git a/src/models/api/requestModels.js b/src/models/api/requestModels.js index 8e5515d52a..5f4f6751ab 100644 --- a/src/models/api/requestModels.js +++ b/src/models/api/requestModels.js @@ -1,15 +1,15 @@ -var SiteRequest = function () { - this.folderId = null; - this.name = null; - this.uri = null; - this.username = null; - this.password = null; - this.notes = null; - this.favorite = false; +var SiteRequest = function (site) { + this.folderId = site.folderId; + this.name = site.name ? site.name.encryptedString : null; + this.uri = site.uri ? site.uri.encryptedString : null; + this.username = site.username ? site.username.encryptedString : null; + this.password = site.password ? site.password.encryptedString : null; + this.notes = site.notes ? site.notes.encryptedString : null; + this.favorite = site.favorite; }; -var FolderRequest = function () { - this.name = null; +var FolderRequest = function (folder) { + this.name = folder.name ? folder.name.encryptedString : null; }; var TokenRequest = function () { diff --git a/src/models/dataModels.js b/src/models/dataModels.js new file mode 100644 index 0000000000..d5fdb6efe9 --- /dev/null +++ b/src/models/dataModels.js @@ -0,0 +1,41 @@ +var FolderData = function (response, userId) { + var data = null; + if (response instanceof FolderResponse) { + data = response; + } + else if (response instanceof CipherResponse) { + data = response.Data; + } + else { + throw 'unsupported instance'; + } + + this.id = response.Id; + this.userId = userId; + this.name = data.Name; + this.revisionDate = response.RevisionDate; +}; + +var SiteData = function (response, userId) { + var data = null; + if (response instanceof SiteResponse) { + data = response; + } + else if (response instanceof CipherResponse) { + data = response.Data; + } + else { + throw 'unsupported instance'; + } + + this.id = response.Id; + this.folderId = response.FolderId; + this.userId = userId; + this.name = data.Name; + this.uri = data.Uri; + this.username = data.Username; + this.password = data.Password; + this.notes = data.Notes; + this.favorite = response.Favorite; + this.revisionDate = response.RevisionDate; +}; diff --git a/src/models/domainModels.js b/src/models/domainModels.js new file mode 100644 index 0000000000..3137c2be48 --- /dev/null +++ b/src/models/domainModels.js @@ -0,0 +1,37 @@ +var CipherString = function (encryptedString) { + this.encryptedString = encryptedString; + + if (encryptedString) { + this.initializationVector = this.encryptedString.split('|')[0]; + this.cipherText = this.encryptedString.split('|')[1]; + } +}; + +!function () { + var _decryptedValue = null; + + CipherString.prototype.decrypt = function (callback) { + if (!_decryptedValue) { + var cryptoService = chrome.extension.getBackgroundPage().cryptoService; + _decryptedValue = cryptoService.Decrypt(this); + } + + return _decryptedValue; + }; +}(); + +var Site = function (obj) { + this.id = obj.id; + this.folderId = obj.folderId; + this.name = new CipherString(obj.name); + this.uri = new CipherString(obj.uri); + this.username = new CipherString(obj.username); + this.password = new CipherString(obj.password); + this.notes = new CipherString(obj.notes); + this.favorite = new obj.favorite; +}; + +var Folder = function (obj) { + this.id = obj.id; + this.name = new CipherString(obj.name); +}; diff --git a/src/services/apiService.js b/src/services/apiService.js index 3cef04f9b3..6f67929017 100644 --- a/src/services/apiService.js +++ b/src/services/apiService.js @@ -5,13 +5,13 @@ !function () { // Account APIs + ApiService.prototype.getProfile = function (success, error) { var self = this; this.tokenService.getToken(function (token) { $.ajax({ type: 'GET', - url: self.baseUrl + '/accounts/profile', - data: 'access_token=' + token, + url: self.baseUrl + '/accounts/profile?access_token=' + token, dataType: 'json', success: function (response) { success(new ProfileResponse(response)) @@ -25,16 +25,122 @@ // Site APIs + ApiService.prototype.getSite = function (id, success, error) { + var self = this; + this.tokenService.getToken(function (token) { + $.ajax({ + type: 'GET', + url: self.baseUrl + '/sites/' + id + '?access_token=' + token, + dataType: 'json', + success: function (response) { + success(new SiteResponse(response)) + }, + error: function (jqXHR, textStatus, errorThrown) { + handleError(error, jqXHR, textStatus, errorThrown); + } + }); + }); + }; + + ApiService.prototype.postSite = function (siteRequest, success, error) { + var self = this; + this.tokenService.getToken(function (token) { + $.ajax({ + type: 'POST', + url: self.baseUrl + '/sites?access_token=' + token, + data: siteRequest, + dataType: 'json', + success: function (response) { + success(new SiteResponse(response)) + }, + error: function (jqXHR, textStatus, errorThrown) { + handleError(error, jqXHR, textStatus, errorThrown); + } + }); + }); + }; + + ApiService.prototype.putSite = function (id, siteRequest, success, error) { + var self = this; + this.tokenService.getToken(function (token) { + $.ajax({ + type: 'POST', + url: self.baseUrl + '/sites/' + id + '?access_token=' + token, + data: siteRequest, + dataType: 'json', + success: function (response) { + success(new SiteResponse(response)) + }, + error: function (jqXHR, textStatus, errorThrown) { + handleError(error, jqXHR, textStatus, errorThrown); + } + }); + }); + }; + // Folder APIs + ApiService.prototype.getFolder = function (id, success, error) { + var self = this; + this.tokenService.getToken(function (token) { + $.ajax({ + type: 'GET', + url: self.baseUrl + '/folders/' + id + '?access_token=' + token, + dataType: 'json', + success: function (response) { + success(new FolderResponse(response)) + }, + error: function (jqXHR, textStatus, errorThrown) { + handleError(error, jqXHR, textStatus, errorThrown); + } + }); + }); + }; + + ApiService.prototype.postFolder = function (folderRequest, success, error) { + var self = this; + this.tokenService.getToken(function (token) { + $.ajax({ + type: 'POST', + url: self.baseUrl + '/folders?access_token=' + token, + data: folderRequest, + dataType: 'json', + success: function (response) { + success(new FolderResponse(response)) + }, + error: function (jqXHR, textStatus, errorThrown) { + handleError(error, jqXHR, textStatus, errorThrown); + } + }); + }); + }; + + ApiService.prototype.putFolder = function (id, folderRequest, success, error) { + var self = this; + this.tokenService.getToken(function (token) { + $.ajax({ + type: 'POST', + url: self.baseUrl + '/folders/' + id + '?access_token=' + token, + data: folderRequest, + dataType: 'json', + success: function (response) { + success(new FolderResponse(response)) + }, + error: function (jqXHR, textStatus, errorThrown) { + handleError(error, jqXHR, textStatus, errorThrown); + } + }); + }); + }; + // Cipher APIs + ApiService.prototype.getCipher = function (id, success, error) { var self = this; this.tokenService.getToken(function (token) { $.ajax({ type: 'GET', - url: self.baseUrl + '/ciphers/' + id, - data: 'access_token=' + token, + url: self.baseUrl + '/ciphers/' + id + '?access_token=' + token, dataType: 'json', success: function (response) { success(new CipherResponse(response)) @@ -51,8 +157,7 @@ this.tokenService.getToken(function (token) { $.ajax({ type: 'GET', - url: self.baseUrl + '/ciphers', - data: 'access_token=' + token, + url: self.baseUrl + '/ciphers?access_token=' + token, dataType: 'json', success: function (response) { var data = []; @@ -69,6 +174,23 @@ }); }; + ApiService.prototype.deleteCipher = function (id, success, error) { + var self = this; + this.tokenService.getToken(function (token) { + $.ajax({ + type: 'POST', + url: self.baseUrl + '/ciphers/' + id + '/delete?access_token=' + token, + dataType: 'json', + success: success, + error: function (jqXHR, textStatus, errorThrown) { + handleError(error, jqXHR, textStatus, errorThrown); + } + }); + }); + }; + + // Helpers + function handleError(errorCallback, jqXHR, textStatus, errorThrown) { errorCallback(new ErrorResponse(jqXHR)); } diff --git a/src/services/cryptoService.js b/src/services/cryptoService.js index fcf9bb5a0d..4696c3c610 100644 --- a/src/services/cryptoService.js +++ b/src/services/cryptoService.js @@ -108,21 +108,16 @@ var ct = ctJson.match(/"ct":"([^"]*)"/)[1]; var iv = sjcl.codec.base64.fromBits(response.iv); - return iv + "|" + ct; + return new CipherString(iv + "|" + ct); }; - CryptoService.prototype.decrypt = function (encValue) { + CryptoService.prototype.decrypt = function (cipherString) { if (!this.getAes()) { throw 'AES encryption unavailable.'; } - var encPieces = encValue.split('|'); - if (encPieces.length !== 2) { - return ''; - } - - var ivBits = sjcl.codec.base64.toBits(encPieces[0]); - var ctBits = sjcl.codec.base64.toBits(encPieces[1]); + var ivBits = sjcl.codec.base64.toBits(cipherString.initializationVector); + var ctBits = sjcl.codec.base64.toBits(cipherString.cipherText); var decBits = sjcl.mode.cbc.decrypt(this.getAes(), ctBits, ivBits, null); return sjcl.codec.utf8String.fromBits(decBits); diff --git a/src/services/siteService.js b/src/services/siteService.js index 131225045a..dbf7fcb8e3 100644 --- a/src/services/siteService.js +++ b/src/services/siteService.js @@ -1,104 +1,131 @@ -function TokenService() { - +function SiteService(cryptoService, userService, apiService) { + this.cryptoService = cryptoService; + this.userService = userService; + this.apiService = apiService; }; !function () { - var _token; + var ciphersKey = 'ciphers_' + this.userService.userId; - TokenService.prototype.setToken = function (token, callback) { + SiteService.prototype.get = function (id, callback) { if (!callback || typeof callback !== 'function') { throw 'callback function required'; } - _token = token; - chrome.storage.local.set({ - 'authBearer': token - }, function () { - callback(); - }); - }; - - TokenService.prototype.getToken = function (callback) { - if (!callback || typeof callback !== 'function') { - throw 'callback function required'; - } - - if (_token) { - return callback(_token); - } - - chrome.storage.local.get('authBearer', function (obj) { - if (obj && obj.authBearer) { - _token = obj.authBearer; + chrome.storage.local.get(ciphersKey, function (obj) { + if (!obj) { + callback(null); } - return callback(_token); + var sites = obj[ciphersKey]; + if (id in sites) { + callback(new Site(sites[id])); + return; + } + + callback(null); }); }; - TokenService.prototype.clearToken = function (callback) { + SiteService.prototype.getAll = function (callback) { if (!callback || typeof callback !== 'function') { throw 'callback function required'; } - _token = null; - chrome.storage.local.remove('authBearer', function () { - callback(); + chrome.storage.local.get(ciphersKey, function (obj) { + if (!obj) { + callback([]); + } + + var sites = obj[ciphersKey]; + var response = []; + for (var id in sites) { + response.push(new Site(sites[id])); + } + + callback(response); }); }; - // jwthelper methods - // ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js - - TokenService.prototype.decodeToken = function (token) { - var parts = token.split('.'); - - if (parts.length !== 3) { - throw new Error('JWT must have 3 parts'); + SiteService.prototype.save = function (site, callback) { + if (!callback || typeof callback !== 'function') { + throw 'callback function required'; } - var decoded = urlBase64Decode(parts[1]); - if (!decoded) { - throw new Error('Cannot decode the token'); + var newRecord = site.id === null, + self = this; + + var request = new SiteRequest(site); + if (newRecord) { + self.apiService.postSite(request, apiSuccess, handleError); + } + else { + self.apiService.putSite(site.id, request, apiSuccess, handleError); } - return JSON.parse(decoded); + function apiSuccess(response) { + userService.getUserId(function (userId) { + var data = new SiteData(response, userId); + + chrome.storage.local.get(ciphersKey, function (obj) { + if (!obj) { + obj = {}; + obj[ciphersKey] = []; + } + + var sites = obj[ciphersKey]; + if (!newRecord && site.id in sites) { + sites[site.id] = data; + } + else { + sites.push(data); + site.id = data.id; + } + + obj[ciphersKey] = sites; + chrome.storage.local.set(obj, function () { + callback(site); + }); + }); + }); + } }; - TokenService.prototype.getTokenExpirationDate = function (token) { - var decoded = this.decodeToken(token); - - if (typeof decoded.exp === "undefined") { - return null; + SiteService.prototype.delete = function (id, callback) { + if (!callback || typeof callback !== 'function') { + throw 'callback function required'; } - var d = new Date(0); // The 0 here is the key, which sets the date to the epoch - d.setUTCSeconds(decoded.exp); + self.apiService.deleteCipher(id, apiSuccess, handleError); - return d; - }; + function apiSuccess(response) { + userService.getUserId(function (userId) { + chrome.storage.local.get(ciphersKey, function (obj) { + if (!obj) { + obj = {}; + obj[ciphersKey] = []; + } - TokenService.prototype.isTokenExpired = function (token, offsetSeconds) { - var d = this.getTokenExpirationDate(token); - offsetSeconds = offsetSeconds || 0; - if (d === null) { - return false; + var sites = obj[ciphersKey]; + if (id in sites) { + var index = sites.indexOf(sites[id]); + sites.splice(index, 1); + + obj[ciphersKey] = sites; + chrome.storage.local.set(obj, function () { + callback(); + }); + } + else { + callback(); + } + }); + }); } - - // Token expired? - return !(d.valueOf() > (new Date().valueOf() + (offsetSeconds * 1000))); }; - function urlBase64Decode(str) { - var output = str.replace(/-/g, '+').replace(/_/g, '/'); - switch (output.length % 4) { - case 0: { break; } - case 2: { output += '=='; break; } - case 3: { output += '='; break; } - default: { - throw 'Illegal base64url string!'; - } - } - return window.decodeURIComponent(escape(window.atob(output))); //polyfill https://github.com/davidchambers/Base64.js - }; + + function handleError() { + // TODO: check for unauth or forbidden and logout + } }(); diff --git a/src/services/userService.js b/src/services/userService.js index efedc377dd..9509d68424 100644 --- a/src/services/userService.js +++ b/src/services/userService.js @@ -6,6 +6,12 @@ !function () { var _userProfile = null; + UserService.prototype.getUserId = function (callback) { + this.getUserProfile(function (profile) { + callback(profile.id); + }); + }; + UserService.prototype.getUserProfile = function (callback) { if (!callback || typeof callback !== 'function') { throw 'callback function required';