From b05426f953ade55d7a9947484d79c5c4eb0ab6fb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 6 Nov 2017 10:58:32 -0500 Subject: [PATCH] convert cipher service to ts --- src/background.js | 5 +- src/models/data/cipherData.ts | 17 +- src/models/domain/attachment.ts | 2 +- src/models/domain/card.ts | 2 +- src/models/domain/cipher.ts | 2 +- src/models/domain/domain.ts | 5 +- src/models/domain/field.ts | 2 +- src/models/domain/folder.ts | 2 +- src/models/domain/identity.ts | 2 +- src/models/domain/login.ts | 2 +- src/models/response/cipherResponse.ts | 5 +- .../app/vault/vaultViewCipherController.js | 2 +- src/services/cipher.service.ts | 518 ++++++++++++++++++ 13 files changed, 543 insertions(+), 23 deletions(-) create mode 100644 src/services/cipher.service.ts diff --git a/src/background.js b/src/background.js index 52685dd5da..5b606eeb27 100644 --- a/src/background.js +++ b/src/background.js @@ -1,7 +1,7 @@ // Service imports import ApiService from './services/api.service'; import AppIdService from './services/appId.service'; -import CipherService from './services/cipherService.js'; +import CipherService from './services/cipher.service'; import ConstantsService from './services/constants.service'; import CryptoService from './services/crypto.service'; import EnvironmentService from './services/environment.service'; @@ -99,8 +99,7 @@ var bg_isBackground = true, window.bg_environmentService = bg_environmentService = new EnvironmentService(bg_apiService); window.bg_userService = bg_userService = new UserService(bg_tokenService); window.bg_settingsService = bg_settingsService = new SettingsService(bg_userService); - window.bg_cipherService = bg_cipherService = new CipherService(bg_cryptoService, bg_userService, bg_apiService, bg_settingsService, bg_utilsService, - bg_constantsService); + window.bg_cipherService = bg_cipherService = new CipherService(bg_cryptoService, bg_userService, bg_settingsService, bg_apiService); window.bg_folderService = bg_folderService = new FolderService(bg_cryptoService, bg_userService, bg_i18nService, bg_apiService); window.bg_lockService = bg_lockService = new LockService(bg_constantsService, bg_cryptoService, bg_folderService, bg_cipherService, bg_utilsService, setIcon, refreshBadgeAndMenu); diff --git a/src/models/data/cipherData.ts b/src/models/data/cipherData.ts index 175fb794fd..c18672351a 100644 --- a/src/models/data/cipherData.ts +++ b/src/models/data/cipherData.ts @@ -1,3 +1,5 @@ +import { CipherType } from '../../enums/cipherType.enum'; + import { AttachmentData } from './attachmentData'; import { CardData } from './cardData'; import { FieldData } from './fieldData'; @@ -16,7 +18,7 @@ class CipherData { organizationUseTotp: boolean; favorite: boolean; revisionDate: string; - type: number; // TODO: enum + type: CipherType; sizeName: string; name: string; notes: string; @@ -41,32 +43,31 @@ class CipherData { this.name = response.data.Name; this.notes = response.data.Notes; - const constantsService = chrome.extension.getBackgroundPage().bg_constantsService; // TODO: enum switch (this.type) { - case constantsService.cipherType.login: + case CipherType.Login: this.login = new LoginData(response.data); break; - case constantsService.cipherType.secureNote: + case CipherType.SecureNote: this.secureNote = new SecureNoteData(response.data); break; - case constantsService.cipherType.card: + case CipherType.Card: this.card = new CardData(response.data); break; - case constantsService.cipherType.identity: + case CipherType.Identity: this.identity = new IdentityData(response.data); break; default: break; } - if (response.data.Fields) { + if (response.data.Fields != null) { this.fields = []; for (const field of response.data.Fields) { this.fields.push(new FieldData(field)); } } - if (response.attachments) { + if (response.attachments != null) { this.attachments = []; for (const attachment of response.attachments) { this.attachments.push(new AttachmentData(attachment)); diff --git a/src/models/domain/attachment.ts b/src/models/domain/attachment.ts index c73e841ae8..d77152b007 100644 --- a/src/models/domain/attachment.ts +++ b/src/models/domain/attachment.ts @@ -33,7 +33,7 @@ class Attachment extends Domain { url: this.url, }; - return this.decryptObj(model, this, { + return this.decryptObj(model, { fileName: null, }, orgId); } diff --git a/src/models/domain/card.ts b/src/models/domain/card.ts index b30ad05384..3e42f7affb 100644 --- a/src/models/domain/card.ts +++ b/src/models/domain/card.ts @@ -28,7 +28,7 @@ class Card extends Domain { } decrypt(orgId: string): Promise { - return this.decryptObj({}, this, { + return this.decryptObj({}, { cardholderName: null, brand: null, number: null, diff --git a/src/models/domain/cipher.ts b/src/models/domain/cipher.ts index a0155999f4..f8def1b022 100644 --- a/src/models/domain/cipher.ts +++ b/src/models/domain/cipher.ts @@ -103,7 +103,7 @@ class Cipher extends Domain { fields: null as any[], }; - await this.decryptObj(model, this, { + await this.decryptObj(model, { name: null, notes: null, }, this.organizationId); diff --git a/src/models/domain/domain.ts b/src/models/domain/domain.ts index e8818a7dd9..cdb220776b 100644 --- a/src/models/domain/domain.ts +++ b/src/models/domain/domain.ts @@ -16,8 +16,10 @@ export default abstract class Domain { } } - protected async decryptObj(model: any, self: any, map: any, orgId: string) { + protected async decryptObj(model: any, map: any, orgId: string) { const promises = []; + const self: any = this; + for (const prop in map) { if (!map.hasOwnProperty(prop)) { continue; @@ -33,7 +35,6 @@ export default abstract class Domain { return null; }).then((val: any) => { model[theProp] = val; - return; }); promises.push(p); })(prop); diff --git a/src/models/domain/field.ts b/src/models/domain/field.ts index 5bc3b85fd8..decc78664f 100644 --- a/src/models/domain/field.ts +++ b/src/models/domain/field.ts @@ -28,7 +28,7 @@ class Field extends Domain { type: this.type, }; - return this.decryptObj(model, this, { + return this.decryptObj(model, { name: null, value: null, }, orgId); diff --git a/src/models/domain/folder.ts b/src/models/domain/folder.ts index 238f8cb807..180cd44e4f 100644 --- a/src/models/domain/folder.ts +++ b/src/models/domain/folder.ts @@ -24,7 +24,7 @@ class Folder extends Domain { id: this.id, }; - return this.decryptObj(model, this, { + return this.decryptObj(model, { name: null, }, null); } diff --git a/src/models/domain/identity.ts b/src/models/domain/identity.ts index 2a1c47d239..ede933ac30 100644 --- a/src/models/domain/identity.ts +++ b/src/models/domain/identity.ts @@ -52,7 +52,7 @@ class Identity extends Domain { } decrypt(orgId: string): Promise { - return this.decryptObj({}, this, { + return this.decryptObj({}, { title: null, firstName: null, middleName: null, diff --git a/src/models/domain/login.ts b/src/models/domain/login.ts index b6e0b0aae1..8ed1e1f7c7 100644 --- a/src/models/domain/login.ts +++ b/src/models/domain/login.ts @@ -24,7 +24,7 @@ class Login extends Domain { } decrypt(orgId: string): Promise { - return this.decryptObj({}, this, { + return this.decryptObj({}, { uri: null, username: null, password: null, diff --git a/src/models/response/cipherResponse.ts b/src/models/response/cipherResponse.ts index 28aea7876e..a99f744e65 100644 --- a/src/models/response/cipherResponse.ts +++ b/src/models/response/cipherResponse.ts @@ -10,7 +10,7 @@ class CipherResponse { organizationUseTotp: boolean; data: any; revisionDate: string; - attachments: AttachmentResponse[] = []; + attachments: AttachmentResponse[]; constructor(response: any) { this.id = response.Id; @@ -23,7 +23,8 @@ class CipherResponse { this.data = response.Data; this.revisionDate = response.RevisionDate; - if (response.Attachments) { + if (response.Attachments != null) { + this.attachments = []; for (const attachment of response.Attachments) { this.attachments.push(new AttachmentResponse(attachment)); } diff --git a/src/popup/app/vault/vaultViewCipherController.js b/src/popup/app/vault/vaultViewCipherController.js index 3c3bf95665..efbd84b6dc 100644 --- a/src/popup/app/vault/vaultViewCipherController.js +++ b/src/popup/app/vault/vaultViewCipherController.js @@ -43,7 +43,7 @@ angular } } - if (model.login.totp && (cipherObj.organizationUseTotp || tokenService.getPremium())) { + if (model.login && model.login.totp && (cipherObj.organizationUseTotp || tokenService.getPremium())) { totpUpdateCode(); totpTick(); diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts new file mode 100644 index 0000000000..cda1344997 --- /dev/null +++ b/src/services/cipher.service.ts @@ -0,0 +1,518 @@ +import { CipherType } from '../enums/cipherType.enum'; + +import { Cipher } from '../models/domain/cipher'; +import { CipherString } from '../models/domain/cipherString'; +import { Field } from '../models/domain/field'; +import SymmetricCryptoKey from '../models/domain/symmetricCryptoKey'; + +import { CipherData } from '../models/data/cipherData'; + +import { CipherRequest } from '../models/request/cipherRequest'; +import { CipherResponse } from '../models/response/cipherResponse'; +import { ErrorResponse } from '../models/response/errorResponse'; + +import ApiService from './api.service'; +import ConstantsService from './constants.service'; +import CryptoService from './crypto.service'; +import SettingsService from './settings.service'; +import UserService from './user.service'; +import UtilsService from './utils.service'; + +const Keys = { + ciphersPrefix: 'ciphers_', + localData: 'sitesLocalData', + neverDomains: 'neverDomains', +}; + +export default class CipherService { + static sortCiphersByLastUsed(a: any, b: any): number { + const aLastUsed = a.localData && a.localData.lastUsedDate ? a.localData.lastUsedDate as number : null; + const bLastUsed = b.localData && b.localData.lastUsedDate ? b.localData.lastUsedDate as number : null; + + if (aLastUsed != null && bLastUsed != null && aLastUsed < bLastUsed) { + return 1; + } + if (aLastUsed != null && bLastUsed == null) { + return -1; + } + + if (bLastUsed != null && aLastUsed != null && aLastUsed > bLastUsed) { + return -1; + } + if (bLastUsed != null && aLastUsed == null) { + return 1; + } + + return 0; + } + + static sortCiphersByLastUsedThenName(a: any, b: any): number { + const result = CipherService.sortCiphersByLastUsed(a, b); + if (result !== 0) { + return result; + } + + const nameA = (a.name + '_' + a.username).toUpperCase(); + const nameB = (b.name + '_' + b.username).toUpperCase(); + + if (nameA < nameB) { + return -1; + } + if (nameA > nameB) { + return 1; + } + + return 0; + } + + decryptedCipherCache: any[]; + + constructor(private cryptoService: CryptoService, private userService: UserService, + private settingsService: SettingsService, private apiService: ApiService) { + } + + clearCache(): void { + this.decryptedCipherCache = null; + } + + async encrypt(model: any): Promise { + const cipher = new Cipher(); + cipher.id = model.id; + cipher.folderId = model.folderId; + cipher.favorite = model.favorite; + cipher.organizationId = model.organizationId; + cipher.type = model.type; + + const key = await this.cryptoService.getOrgKey(cipher.organizationId); + await Promise.all([ + this.encryptObjProperty(model, cipher, { + name: null, + notes: null, + }, key), + this.encryptCipherData(model, cipher, key), + this.encryptFields(model.fields, key).then((fields) => { + cipher.fields = fields; + }), + ]); + + return cipher; + } + + async encryptFields(fieldsModel: any[], key: SymmetricCryptoKey): Promise { + if (!fieldsModel || !fieldsModel.length) { + return null; + } + + const self = this; + const encFields: Field[] = []; + await fieldsModel.reduce((promise, field) => { + return promise.then(() => { + return self.encryptField(field, key); + }).then((encField: Field) => { + encFields.push(encField); + }); + }, Promise.resolve()); + + return encFields; + } + + async encryptField(fieldModel: any, key: SymmetricCryptoKey): Promise { + const field = new Field(); + field.type = fieldModel.type; + + await this.encryptObjProperty(fieldModel, field, { + name: null, + value: null, + }, key); + + return field; + } + + async get(id: string): Promise { + const userId = await this.userService.getUserId(); + const localData = await UtilsService.getObjFromStorage(Keys.localData); + const ciphers = await UtilsService.getObjFromStorage<{ [id: string]: CipherData; }>( + Keys.ciphersPrefix + userId); + if (ciphers == null || !ciphers.hasOwnProperty(id)) { + return null; + } + + return new Cipher(ciphers[id], false, localData ? localData[id] : null); + } + + async getAll(): Promise { + const userId = await this.userService.getUserId(); + const localData = await UtilsService.getObjFromStorage(Keys.localData); + const ciphers = await UtilsService.getObjFromStorage<{ [id: string]: CipherData; }>( + Keys.ciphersPrefix + userId); + const response: Cipher[] = []; + for (const id in ciphers) { + if (ciphers.hasOwnProperty(id)) { + response.push(new Cipher(ciphers[id], false, localData ? localData[id] : null)); + } + } + return response; + } + + async getAllDecrypted(): Promise { + if (this.decryptedCipherCache != null) { + return this.decryptedCipherCache; + } + + const decCiphers: any[] = []; + const key = await this.cryptoService.getKey(); + if (key == null) { + throw new Error('No key.'); + } + + const promises = []; + const ciphers = await this.getAll(); + for (const cipher of ciphers) { + promises.push(cipher.decrypt().then((c: any) => { + decCiphers.push(c); + })); + } + + await Promise.all(promises); + this.decryptedCipherCache = decCiphers; + return this.decryptedCipherCache; + } + + async getAllDecryptedForFolder(folderId: string): Promise { + const ciphers = await this.getAllDecrypted(); + const ciphersToReturn = []; + + for (const cipher of ciphers) { + if (cipher.folderId === folderId) { + ciphersToReturn.push(cipher); + } + } + + return ciphersToReturn; + } + + async getAllDecryptedForDomain(domain: string, includeOtherTypes?: any[]): Promise { + if (domain == null && !includeOtherTypes) { + return Promise.resolve([]); + } + + const eqDomainsPromise = domain == null ? Promise.resolve([]) : + this.settingsService.getEquivalentDomains().then((eqDomains: any[][]) => { + let matches: any[] = []; + for (const eqDomain of eqDomains) { + if (eqDomain.length && eqDomain.indexOf(domain) >= 0) { + matches = matches.concat(eqDomain); + } + } + + if (!matches.length) { + matches.push(domain); + } + + return matches; + }); + + const result = await Promise.all([eqDomainsPromise, this.getAllDecrypted()]); + const matchingDomains = result[0]; + const ciphers = result[1]; + const ciphersToReturn = []; + + for (const cipher of ciphers) { + if (domain && cipher.type === CipherType.Login && cipher.login.domain && + matchingDomains.indexOf(cipher.login.domain) > -1) { + ciphersToReturn.push(cipher); + } else if (includeOtherTypes && includeOtherTypes.indexOf(cipher.type) > -1) { + ciphersToReturn.push(cipher); + } + } + + return ciphersToReturn; + } + + async getLastUsedForDomain(domain: string): Promise { + const ciphers = await this.getAllDecryptedForDomain(domain); + if (ciphers.length === 0) { + throw new Error('No ciphers.'); + } + + const sortedCiphers = ciphers.sort(CipherService.sortCiphersByLastUsed); + return sortedCiphers[0]; + } + + async updateLastUsedDate(id: string): Promise { + let ciphersLocalData = await UtilsService.getObjFromStorage(Keys.localData); + if (!ciphersLocalData) { + ciphersLocalData = {}; + } + + if (ciphersLocalData[id]) { + ciphersLocalData[id].lastUsedDate = new Date().getTime(); + } else { + ciphersLocalData[id] = { + lastUsedDate: new Date().getTime(), + }; + } + + await UtilsService.saveObjToStorage(Keys.localData, ciphersLocalData); + + if (this.decryptedCipherCache == null) { + return; + } + + for (const cached of this.decryptedCipherCache) { + if (cached.id === id) { + cached.localData = ciphersLocalData[id]; + break; + } + } + } + + async saveNeverDomain(domain: string): Promise { + if (domain == null) { + return; + } + + let domains = await UtilsService.getObjFromStorage<{ [id: string]: any; }>(Keys.neverDomains); + if (!domains) { + domains = {}; + } + domains[domain] = null; + await UtilsService.saveObjToStorage(Keys.neverDomains, domains); + } + + async saveWithServer(cipher: Cipher): Promise { + const request = new CipherRequest(cipher); + + let response: CipherResponse; + if (cipher.id == null) { + response = await this.apiService.postCipher(request); + cipher.id = response.id; + } else { + response = await this.apiService.putCipher(cipher.id, request); + } + + const userId = await this.userService.getUserId(); + const data = new CipherData(response, userId); + await this.upsert(data); + } + + saveAttachmentWithServer(cipher: Cipher, unencryptedFile: any): Promise { + const self = this; + + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsArrayBuffer(unencryptedFile); + + reader.onload = async (evt: any) => { + const key = await self.cryptoService.getOrgKey(cipher.organizationId); + const encFileName = await self.cryptoService.encrypt(unencryptedFile.name, key); + const encData = await self.cryptoService.encryptToBytes(evt.target.result, key); + + const fd = new FormData(); + const blob = new Blob([encData], { type: 'application/octet-stream' }); + fd.append('data', blob, encFileName.encryptedString); + + const response = await self.apiService.postCipherAttachment(cipher.id, fd); + // TODO: handle error response + const userId = await self.userService.getUserId(); + const data = new CipherData(response, userId); + this.upsert(data); + resolve(new Cipher(data)); + }; + + reader.onerror = (evt) => { + reject('Error reading file.'); + }; + }); + } + + async upsert(cipher: CipherData | CipherData[]): Promise { + const userId = await this.userService.getUserId(); + let ciphers = await UtilsService.getObjFromStorage<{ [id: string]: CipherData; }>( + Keys.ciphersPrefix + userId); + if (ciphers == null) { + ciphers = {}; + } + + if (cipher instanceof CipherData) { + const c = cipher as CipherData; + ciphers[c.id] = c; + } else { + for (const c of (cipher as CipherData[])) { + ciphers[c.id] = c; + } + } + + await UtilsService.saveObjToStorage(Keys.ciphersPrefix + userId, ciphers); + this.decryptedCipherCache = null; + } + + async replace(ciphers: CipherData[]): Promise { + const userId = await this.userService.getUserId(); + await UtilsService.saveObjToStorage(Keys.ciphersPrefix + userId, ciphers); + this.decryptedCipherCache = null; + } + + async clear(userId: string): Promise { + await UtilsService.removeFromStorage(Keys.ciphersPrefix + userId); + this.decryptedCipherCache = null; + } + + async delete(id: string | string[]): Promise { + const userId = await this.userService.getUserId(); + const ciphers = await UtilsService.getObjFromStorage<{ [id: string]: CipherData; }>( + Keys.ciphersPrefix + userId); + if (ciphers == null) { + return; + } + + if (typeof id === 'string') { + const i = id as string; + delete ciphers[id]; + } else { + for (const i of (id as string[])) { + delete ciphers[i]; + } + } + + await UtilsService.saveObjToStorage(Keys.ciphersPrefix + userId, ciphers); + this.decryptedCipherCache = null; + } + + async deleteWithServer(id: string): Promise { + await this.apiService.deleteCipher(id); + await this.delete(id); + } + + async deleteAttachment(id: string, attachmentId: string): Promise { + const userId = await this.userService.getUserId(); + const ciphers = await UtilsService.getObjFromStorage<{ [id: string]: CipherData; }>( + Keys.ciphersPrefix + userId); + + if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[id].attachments == null) { + return; + } + + for (let i = 0; i < ciphers[id].attachments.length; i++) { + if (ciphers[id].attachments[i].id === attachmentId) { + ciphers[id].attachments.splice(i, 1); + } + } + + await UtilsService.saveObjToStorage(Keys.ciphersPrefix + userId, ciphers); + this.decryptedCipherCache = null; + } + + async deleteAttachmentWithServer(id: string, attachmentId: string): Promise { + await this.apiService.deleteCipherAttachment(id, attachmentId); + await this.deleteAttachment(id, attachmentId); + // TODO: handle error + } + + // TODO: remove in favor of static refs + + sortCiphersByLastUsed(a: any, b: any): number { + return CipherService.sortCiphersByLastUsed(a, b); + } + + sortCiphersByLastUsedThenName(a: any, b: any): number { + return CipherService.sortCiphersByLastUsedThenName(a, b); + } + + // Helpers + + private encryptObjProperty(model: any, obj: any, map: any, key: SymmetricCryptoKey): Promise { + const promises = []; + const self = this; + + for (const prop in map) { + if (!map.hasOwnProperty(prop)) { + continue; + } + + // tslint:disable-next-line + (function (theProp, theObj) { + const p = Promise.resolve().then(() => { + const modelProp = model[(map[theProp] || theProp)]; + if (modelProp && modelProp !== '') { + return self.cryptoService.encrypt(modelProp, key); + } + return null; + }).then((val: CipherString) => { + theObj[theProp] = val; + }); + promises.push(p); + })(prop, obj); + } + + return Promise.all(promises); + } + + private encryptCipherData(cipher: Cipher, model: any, key: SymmetricCryptoKey): Promise { + switch (cipher.type) { + case CipherType.Login: + model.login = {}; + return this.encryptObjProperty(cipher.login, model.login, { + uri: null, + username: null, + password: null, + totp: null, + }, key); + case CipherType.SecureNote: + model.secureNote = { + type: cipher.secureNote.type, + }; + return Promise.resolve(); + case CipherType.Card: + model.card = {}; + return this.encryptObjProperty(cipher.card, model.card, { + cardholderName: null, + brand: null, + number: null, + expMonth: null, + expYear: null, + code: null, + }, key); + case CipherType.Identity: + model.identity = {}; + return this.encryptObjProperty(cipher.identity, model.identity, { + title: null, + firstName: null, + middleName: null, + lastName: null, + address1: null, + address2: null, + address3: null, + city: null, + state: null, + postalCode: null, + country: null, + company: null, + email: null, + phone: null, + ssn: null, + username: null, + passportNumber: null, + licenseNumber: null, + }, key); + default: + throw new Error('Unknown cipher type.'); + } + } + + private handleErrorMessage(error: ErrorResponse, reject: Function): void { + if (error.validationErrors) { + for (const key in error.validationErrors) { + if (!error.validationErrors.hasOwnProperty(key)) { + continue; + } + if (error.validationErrors[key].length) { + reject(error.validationErrors[key][0]); + return; + } + } + } + reject(error.message); + return; + } +}