diff --git a/package.json b/package.json index 572fa374f4f..c7017da888c 100644 --- a/package.json +++ b/package.json @@ -30,5 +30,10 @@ "tslint": "^5.8.0", "typedoc": "^0.9.0", "typescript": "^2.6.2" + }, + "dependencies": { + "node-forge": "0.7.1", + "@types/node-forge": "0.7.1", + "@types/webcrypto": "0.0.28" } } diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts new file mode 100644 index 00000000000..10b4e1ac4e3 --- /dev/null +++ b/src/abstractions/crypto.service.ts @@ -0,0 +1,28 @@ +import { CipherString } from '../models/domain/cipherString'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; + +import { ProfileOrganizationResponse } from '../models/response/profileOrganizationResponse'; + +export interface CryptoService { + setKey(key: SymmetricCryptoKey): Promise; + setKeyHash(keyHash: string): Promise<{}>; + setEncKey(encKey: string): Promise<{}>; + setEncPrivateKey(encPrivateKey: string): Promise<{}>; + setOrgKeys(orgs: ProfileOrganizationResponse[]): Promise<{}>; + getKey(): Promise; + getKeyHash(): Promise; + getEncKey(): Promise; + getPrivateKey(): Promise; + getOrgKeys(): Promise>; + getOrgKey(orgId: string): Promise; + clearKeys(): Promise; + toggleKey(): Promise; + makeKey(password: string, salt: string): SymmetricCryptoKey; + hashPassword(password: string, key: SymmetricCryptoKey): Promise; + makeEncKey(key: SymmetricCryptoKey): Promise; + encrypt(plainValue: string | Uint8Array, key?: SymmetricCryptoKey, plainValueEncoding?: string): Promise; + encryptToBytes(plainValue: ArrayBuffer, key?: SymmetricCryptoKey): Promise; + decrypt(cipherString: CipherString, key?: SymmetricCryptoKey, outputEncoding?: string): Promise; + decryptFromBytes(encBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise; + rsaDecrypt(encValue: string): Promise; +} diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index 664cbb0dcf2..6191317b07e 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -1,3 +1,4 @@ +export { CryptoService } from './crypto.service'; export { MessagingService } from './messaging.service'; export { PlatformUtilsService } from './platformUtils.service'; export { StorageService } from './storage.service'; diff --git a/src/index.ts b/src/index.ts index 9d88a216d81..2edf7e05e4f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,9 @@ import * as Abstractions from './abstractions'; import * as Enums from './enums'; +import * as Data from './models/data'; +import * as Domain from './models/domain'; +import * as Request from './models/request'; +import * as Response from './models/response'; import * as Services from './services'; -export { Abstractions, Enums, Services }; +export { Abstractions, Enums, Data, Domain, Request, Response, Services }; diff --git a/src/models/data/attachmentData.ts b/src/models/data/attachmentData.ts new file mode 100644 index 00000000000..2ff3ab423c8 --- /dev/null +++ b/src/models/data/attachmentData.ts @@ -0,0 +1,20 @@ +import { AttachmentResponse } from '../response/attachmentResponse'; + +class AttachmentData { + id: string; + url: string; + fileName: string; + size: number; + sizeName: string; + + constructor(response: AttachmentResponse) { + this.id = response.id; + this.url = response.url; + this.fileName = response.fileName; + this.size = response.size; + this.sizeName = response.sizeName; + } +} + +export { AttachmentData }; +(window as any).AttachmentData = AttachmentData; diff --git a/src/models/data/cardData.ts b/src/models/data/cardData.ts new file mode 100644 index 00000000000..f0b9f63f2ec --- /dev/null +++ b/src/models/data/cardData.ts @@ -0,0 +1,20 @@ +class CardData { + cardholderName: string; + brand: string; + number: string; + expMonth: string; + expYear: string; + code: string; + + constructor(data: any) { + this.cardholderName = data.CardholderName; + this.brand = data.Brand; + this.number = data.Number; + this.expMonth = data.ExpMonth; + this.expYear = data.ExpYear; + this.code = data.Code; + } +} + +export { CardData }; +(window as any).CardData = CardData; diff --git a/src/models/data/cipherData.ts b/src/models/data/cipherData.ts new file mode 100644 index 00000000000..a0917ed314f --- /dev/null +++ b/src/models/data/cipherData.ts @@ -0,0 +1,87 @@ +import { CipherType } from '../../enums/cipherType'; + +import { AttachmentData } from './attachmentData'; +import { CardData } from './cardData'; +import { FieldData } from './fieldData'; +import { IdentityData } from './identityData'; +import { LoginData } from './loginData'; +import { SecureNoteData } from './secureNoteData'; + +import { CipherResponse } from '../response/cipherResponse'; + +class CipherData { + id: string; + organizationId: string; + folderId: string; + userId: string; + edit: boolean; + organizationUseTotp: boolean; + favorite: boolean; + revisionDate: string; + type: CipherType; + sizeName: string; + name: string; + notes: string; + login?: LoginData; + secureNote?: SecureNoteData; + card?: CardData; + identity?: IdentityData; + fields?: FieldData[]; + attachments?: AttachmentData[]; + collectionIds?: string[]; + + constructor(response: CipherResponse, userId: string, collectionIds?: string[]) { + this.id = response.id; + this.organizationId = response.organizationId; + this.folderId = response.folderId; + this.userId = userId; + this.edit = response.edit; + this.organizationUseTotp = response.organizationUseTotp; + this.favorite = response.favorite; + this.revisionDate = response.revisionDate; + this.type = response.type; + + if (collectionIds != null) { + this.collectionIds = collectionIds; + } else { + this.collectionIds = response.collectionIds; + } + + this.name = response.data.Name; + this.notes = response.data.Notes; + + switch (this.type) { + case CipherType.Login: + this.login = new LoginData(response.data); + break; + case CipherType.SecureNote: + this.secureNote = new SecureNoteData(response.data); + break; + case CipherType.Card: + this.card = new CardData(response.data); + break; + case CipherType.Identity: + this.identity = new IdentityData(response.data); + break; + default: + break; + } + + if (response.data.Fields != null) { + this.fields = []; + response.data.Fields.forEach((field: any) => { + this.fields.push(new FieldData(field)); + }); + } + + if (response.attachments != null) { + this.attachments = []; + response.attachments.forEach((attachment) => { + this.attachments.push(new AttachmentData(attachment)); + }); + } + } +} + +export { CipherData }; +(window as any).CipherData = CipherData; diff --git a/src/models/data/collectionData.ts b/src/models/data/collectionData.ts new file mode 100644 index 00000000000..f2d5fc9f03c --- /dev/null +++ b/src/models/data/collectionData.ts @@ -0,0 +1,16 @@ +import { CollectionResponse } from '../response/collectionResponse'; + +class CollectionData { + id: string; + organizationId: string; + name: string; + + constructor(response: CollectionResponse) { + this.id = response.id; + this.organizationId = response.organizationId; + this.name = response.name; + } +} + +export { CollectionData }; +(window as any).CollectionData = CollectionData; diff --git a/src/models/data/fieldData.ts b/src/models/data/fieldData.ts new file mode 100644 index 00000000000..920d1c56fa8 --- /dev/null +++ b/src/models/data/fieldData.ts @@ -0,0 +1,16 @@ +import { FieldType } from '../../enums/fieldType'; + +class FieldData { + type: FieldType; + name: string; + value: string; + + constructor(response: any) { + this.type = response.Type; + this.name = response.Name; + this.value = response.Value; + } +} + +export { FieldData }; +(window as any).FieldData = FieldData; diff --git a/src/models/data/folderData.ts b/src/models/data/folderData.ts new file mode 100644 index 00000000000..6f03781cc70 --- /dev/null +++ b/src/models/data/folderData.ts @@ -0,0 +1,18 @@ +import { FolderResponse } from '../response/folderResponse'; + +class FolderData { + id: string; + userId: string; + name: string; + revisionDate: string; + + constructor(response: FolderResponse, userId: string) { + this.userId = userId; + this.name = response.name; + this.id = response.id; + this.revisionDate = response.revisionDate; + } +} + +export { FolderData }; +(window as any).FolderData = FolderData; diff --git a/src/models/data/identityData.ts b/src/models/data/identityData.ts new file mode 100644 index 00000000000..ee849166eec --- /dev/null +++ b/src/models/data/identityData.ts @@ -0,0 +1,44 @@ +class IdentityData { + title: string; + firstName: string; + middleName: string; + lastName: string; + address1: string; + address2: string; + address3: string; + city: string; + state: string; + postalCode: string; + country: string; + company: string; + email: string; + phone: string; + ssn: string; + username: string; + passportNumber: string; + licenseNumber: string; + + constructor(data: any) { + this.title = data.Title; + this.firstName = data.FirstName; + this.middleName = data.MiddleName; + this.lastName = data.LastName; + this.address1 = data.Address1; + this.address2 = data.Address2; + this.address3 = data.Address3; + this.city = data.City; + this.state = data.State; + this.postalCode = data.PostalCode; + this.country = data.Country; + this.company = data.Company; + this.email = data.Email; + this.phone = data.Phone; + this.ssn = data.SSN; + this.username = data.Username; + this.passportNumber = data.PassportNumber; + this.licenseNumber = data.LicenseNumber; + } +} + +export { IdentityData }; +(window as any).IdentityData = IdentityData; diff --git a/src/models/data/index.ts b/src/models/data/index.ts new file mode 100644 index 00000000000..7a5a86df2ed --- /dev/null +++ b/src/models/data/index.ts @@ -0,0 +1,9 @@ +export { AttachmentData } from './attachmentData'; +export { CardData } from './cardData'; +export { CipherData } from './cipherData'; +export { CollectionData } from './collectionData'; +export { FieldData } from './fieldData'; +export { FolderData } from './folderData'; +export { IdentityData } from './identityData'; +export { LoginData } from './loginData'; +export { SecureNoteData } from './secureNoteData'; diff --git a/src/models/data/loginData.ts b/src/models/data/loginData.ts new file mode 100644 index 00000000000..de0aecc133f --- /dev/null +++ b/src/models/data/loginData.ts @@ -0,0 +1,16 @@ +class LoginData { + uri: string; + username: string; + password: string; + totp: string; + + constructor(data: any) { + this.uri = data.Uri; + this.username = data.Username; + this.password = data.Password; + this.totp = data.Totp; + } +} + +export { LoginData }; +(window as any).LoginData = LoginData; diff --git a/src/models/data/secureNoteData.ts b/src/models/data/secureNoteData.ts new file mode 100644 index 00000000000..bf861a4a60f --- /dev/null +++ b/src/models/data/secureNoteData.ts @@ -0,0 +1,12 @@ +import { SecureNoteType } from '../../enums/secureNoteType'; + +class SecureNoteData { + type: SecureNoteType; + + constructor(data: any) { + this.type = data.Type; + } +} + +export { SecureNoteData }; +(window as any).SecureNoteData = SecureNoteData; diff --git a/src/models/domain/attachment.ts b/src/models/domain/attachment.ts new file mode 100644 index 00000000000..d77152b0077 --- /dev/null +++ b/src/models/domain/attachment.ts @@ -0,0 +1,43 @@ +import { AttachmentData } from '../data/attachmentData'; + +import { CipherString } from './cipherString'; +import Domain from './domain'; + +class Attachment extends Domain { + id: string; + url: string; + size: number; + sizeName: string; + fileName: CipherString; + + constructor(obj?: AttachmentData, alreadyEncrypted: boolean = false) { + super(); + if (obj == null) { + return; + } + + this.size = obj.size; + this.buildDomainModel(this, obj, { + id: null, + url: null, + sizeName: null, + fileName: null, + }, alreadyEncrypted, ['id', 'url', 'sizeName']); + } + + decrypt(orgId: string): Promise { + const model = { + id: this.id, + size: this.size, + sizeName: this.sizeName, + url: this.url, + }; + + return this.decryptObj(model, { + fileName: null, + }, orgId); + } +} + +export { Attachment }; +(window as any).Attachment = Attachment; diff --git a/src/models/domain/card.ts b/src/models/domain/card.ts new file mode 100644 index 00000000000..3e42f7affba --- /dev/null +++ b/src/models/domain/card.ts @@ -0,0 +1,43 @@ +import { CardData } from '../data/cardData'; + +import { CipherString } from './cipherString'; +import Domain from './domain'; + +class Card extends Domain { + cardholderName: CipherString; + brand: CipherString; + number: CipherString; + expMonth: CipherString; + expYear: CipherString; + code: CipherString; + + constructor(obj?: CardData, alreadyEncrypted: boolean = false) { + super(); + if (obj == null) { + return; + } + + this.buildDomainModel(this, obj, { + cardholderName: null, + brand: null, + number: null, + expMonth: null, + expYear: null, + code: null, + }, alreadyEncrypted, []); + } + + decrypt(orgId: string): Promise { + return this.decryptObj({}, { + cardholderName: null, + brand: null, + number: null, + expMonth: null, + expYear: null, + code: null, + }, orgId); + } +} + +export { Card }; +(window as any).Card = Card; diff --git a/src/models/domain/cipher.ts b/src/models/domain/cipher.ts new file mode 100644 index 00000000000..5c3c1076d98 --- /dev/null +++ b/src/models/domain/cipher.ts @@ -0,0 +1,192 @@ +import { CipherType } from '../../enums/cipherType'; + +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +import { CipherData } from '../data/cipherData'; + +import { Attachment } from './attachment'; +import { Card } from './card'; +import { CipherString } from './cipherString'; +import Domain from './domain'; +import { Field } from './field'; +import { Identity } from './identity'; +import { Login } from './login'; +import { SecureNote } from './secureNote'; + +class Cipher extends Domain { + id: string; + organizationId: string; + folderId: string; + name: CipherString; + notes: CipherString; + type: CipherType; + favorite: boolean; + organizationUseTotp: boolean; + edit: boolean; + localData: any; + login: Login; + identity: Identity; + card: Card; + secureNote: SecureNote; + attachments: Attachment[]; + fields: Field[]; + collectionIds: string[]; + + constructor(obj?: CipherData, alreadyEncrypted: boolean = false, localData: any = null) { + super(); + if (obj == null) { + return; + } + + this.buildDomainModel(this, obj, { + id: null, + organizationId: null, + folderId: null, + name: null, + notes: null, + }, alreadyEncrypted, ['id', 'organizationId', 'folderId']); + + this.type = obj.type; + this.favorite = obj.favorite; + this.organizationUseTotp = obj.organizationUseTotp; + this.edit = obj.edit; + this.collectionIds = obj.collectionIds; + this.localData = localData; + + switch (this.type) { + case CipherType.Login: + this.login = new Login(obj.login, alreadyEncrypted); + break; + case CipherType.SecureNote: + this.secureNote = new SecureNote(obj.secureNote, alreadyEncrypted); + break; + case CipherType.Card: + this.card = new Card(obj.card, alreadyEncrypted); + break; + case CipherType.Identity: + this.identity = new Identity(obj.identity, alreadyEncrypted); + break; + default: + break; + } + + if (obj.attachments != null) { + this.attachments = []; + obj.attachments.forEach((attachment) => { + this.attachments.push(new Attachment(attachment, alreadyEncrypted)); + }); + } else { + this.attachments = null; + } + + if (obj.fields != null) { + this.fields = []; + obj.fields.forEach((field) => { + this.fields.push(new Field(field, alreadyEncrypted)); + }); + } else { + this.fields = null; + } + } + + async decrypt(): Promise { + const model = { + id: this.id, + organizationId: this.organizationId, + folderId: this.folderId, + favorite: this.favorite, + type: this.type, + localData: this.localData, + login: null as any, + card: null as any, + identity: null as any, + secureNote: null as any, + subTitle: null as string, + attachments: null as any[], + fields: null as any[], + collectionIds: this.collectionIds, + }; + + await this.decryptObj(model, { + name: null, + notes: null, + }, this.organizationId); + + switch (this.type) { + case CipherType.Login: + model.login = await this.login.decrypt(this.organizationId); + model.subTitle = model.login.username; + if (model.login.uri) { + const containerService = (window as any).BitwardenContainerService; + if (containerService) { + const platformUtilsService: PlatformUtilsService = + containerService.getPlatformUtilsService(); + model.login.domain = platformUtilsService.getDomain(model.login.uri); + } else { + throw new Error('window.BitwardenContainerService not initialized.'); + } + } + break; + case CipherType.SecureNote: + model.secureNote = await this.secureNote.decrypt(this.organizationId); + model.subTitle = null; + break; + case CipherType.Card: + model.card = await this.card.decrypt(this.organizationId); + model.subTitle = model.card.brand; + if (model.card.number && model.card.number.length >= 4) { + if (model.subTitle !== '') { + model.subTitle += ', '; + } + model.subTitle += ('*' + model.card.number.substr(model.card.number.length - 4)); + } + break; + case CipherType.Identity: + model.identity = await this.identity.decrypt(this.organizationId); + model.subTitle = ''; + if (model.identity.firstName) { + model.subTitle = model.identity.firstName; + } + if (model.identity.lastName) { + if (model.subTitle !== '') { + model.subTitle += ' '; + } + model.subTitle += model.identity.lastName; + } + break; + default: + break; + } + + const orgId = this.organizationId; + + if (this.attachments != null && this.attachments.length > 0) { + const attachments: any[] = []; + await this.attachments.reduce((promise, attachment) => { + return promise.then(() => { + return attachment.decrypt(orgId); + }).then((decAttachment) => { + attachments.push(decAttachment); + }); + }, Promise.resolve()); + model.attachments = attachments; + } + + if (this.fields != null && this.fields.length > 0) { + const fields: any[] = []; + await this.fields.reduce((promise, field) => { + return promise.then(() => { + return field.decrypt(orgId); + }).then((decField) => { + fields.push(decField); + }); + }, Promise.resolve()); + model.fields = fields; + } + + return model; + } +} + +export { Cipher }; +(window as any).Cipher = Cipher; diff --git a/src/models/domain/cipherString.ts b/src/models/domain/cipherString.ts new file mode 100644 index 00000000000..eb37e08df05 --- /dev/null +++ b/src/models/domain/cipherString.ts @@ -0,0 +1,116 @@ +import { EncryptionType } from '../../enums/encryptionType'; + +import { CryptoService } from '../../abstractions/crypto.service'; + +class CipherString { + encryptedString?: string; + encryptionType?: EncryptionType; + decryptedValue?: string; + cipherText?: string; + initializationVector?: string; + mac?: string; + + constructor(encryptedStringOrType: string | EncryptionType, ct?: string, iv?: string, mac?: string) { + if (ct != null) { + // ct and header + const encType = encryptedStringOrType as EncryptionType; + this.encryptedString = encType + '.' + ct; + + // iv + if (iv != null) { + this.encryptedString += ('|' + iv); + } + + // mac + if (mac != null) { + this.encryptedString += ('|' + mac); + } + + this.encryptionType = encType; + this.cipherText = ct; + this.initializationVector = iv; + this.mac = mac; + + return; + } + + this.encryptedString = encryptedStringOrType as string; + if (!this.encryptedString) { + return; + } + + const headerPieces = this.encryptedString.split('.'); + let encPieces: string[] = null; + + if (headerPieces.length === 2) { + try { + this.encryptionType = parseInt(headerPieces[0], null); + encPieces = headerPieces[1].split('|'); + } catch (e) { + return; + } + } else { + encPieces = this.encryptedString.split('|'); + this.encryptionType = encPieces.length === 3 ? EncryptionType.AesCbc128_HmacSha256_B64 : + EncryptionType.AesCbc256_B64; + } + + switch (this.encryptionType) { + case EncryptionType.AesCbc128_HmacSha256_B64: + case EncryptionType.AesCbc256_HmacSha256_B64: + if (encPieces.length !== 3) { + return; + } + + this.initializationVector = encPieces[0]; + this.cipherText = encPieces[1]; + this.mac = encPieces[2]; + break; + case EncryptionType.AesCbc256_B64: + if (encPieces.length !== 2) { + return; + } + + this.initializationVector = encPieces[0]; + this.cipherText = encPieces[1]; + break; + case EncryptionType.Rsa2048_OaepSha256_B64: + case EncryptionType.Rsa2048_OaepSha1_B64: + if (encPieces.length !== 1) { + return; + } + + this.cipherText = encPieces[0]; + break; + default: + return; + } + } + + decrypt(orgId: string): Promise { + if (this.decryptedValue) { + return Promise.resolve(this.decryptedValue); + } + + let cryptoService: CryptoService; + const containerService = (window as any).BitwardenContainerService; + if (containerService) { + cryptoService = containerService.getCryptoService(); + } else { + throw new Error('window.BitwardenContainerService not initialized.'); + } + + return cryptoService.getOrgKey(orgId).then((orgKey: any) => { + return cryptoService.decrypt(this, orgKey); + }).then((decValue: any) => { + this.decryptedValue = decValue; + return this.decryptedValue; + }).catch(() => { + this.decryptedValue = '[error: cannot decrypt]'; + return this.decryptedValue; + }); + } +} + +export { CipherString }; +(window as any).CipherString = CipherString; diff --git a/src/models/domain/collection.ts b/src/models/domain/collection.ts new file mode 100644 index 00000000000..0a5079e53a9 --- /dev/null +++ b/src/models/domain/collection.ts @@ -0,0 +1,37 @@ +import { CollectionData } from '../data/collectionData'; + +import { CipherString } from './cipherString'; +import Domain from './domain'; + +class Collection extends Domain { + id: string; + organizationId: string; + name: CipherString; + + constructor(obj?: CollectionData, alreadyEncrypted: boolean = false) { + super(); + if (obj == null) { + return; + } + + this.buildDomainModel(this, obj, { + id: null, + organizationId: null, + name: null, + }, alreadyEncrypted, ['id', 'organizationId']); + } + + decrypt(): Promise { + const model = { + id: this.id, + organizationId: this.organizationId, + }; + + return this.decryptObj(model, { + name: null, + }, this.organizationId); + } +} + +export { Collection }; +(window as any).Collection = Collection; diff --git a/src/models/domain/domain.ts b/src/models/domain/domain.ts new file mode 100644 index 00000000000..cdb220776b2 --- /dev/null +++ b/src/models/domain/domain.ts @@ -0,0 +1,46 @@ +import { CipherString } from '../domain/cipherString'; + +export default abstract class Domain { + protected buildDomainModel(model: any, obj: any, map: any, alreadyEncrypted: boolean, notEncList: any[] = []) { + for (const prop in map) { + if (!map.hasOwnProperty(prop)) { + continue; + } + + const objProp = obj[(map[prop] || prop)]; + if (alreadyEncrypted === true || notEncList.indexOf(prop) > -1) { + model[prop] = objProp ? objProp : null; + } else { + model[prop] = objProp ? new CipherString(objProp) : null; + } + } + } + + 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; + } + + // tslint:disable-next-line + (function (theProp) { + const p = Promise.resolve().then(() => { + const mapProp = map[theProp] || theProp; + if (self[mapProp]) { + return self[mapProp].decrypt(orgId); + } + return null; + }).then((val: any) => { + model[theProp] = val; + }); + promises.push(p); + })(prop); + } + + await Promise.all(promises); + return model; + } +} diff --git a/src/models/domain/encryptedObject.ts b/src/models/domain/encryptedObject.ts new file mode 100644 index 00000000000..e28911e3843 --- /dev/null +++ b/src/models/domain/encryptedObject.ts @@ -0,0 +1,8 @@ +import { SymmetricCryptoKey } from './symmetricCryptoKey'; + +export class EncryptedObject { + iv: Uint8Array; + ct: Uint8Array; + mac: Uint8Array; + key: SymmetricCryptoKey; +} diff --git a/src/models/domain/environmentUrls.ts b/src/models/domain/environmentUrls.ts new file mode 100644 index 00000000000..868cd432188 --- /dev/null +++ b/src/models/domain/environmentUrls.ts @@ -0,0 +1,5 @@ +export class EnvironmentUrls { + base: string; + api: string; + identity: string; +} diff --git a/src/models/domain/field.ts b/src/models/domain/field.ts new file mode 100644 index 00000000000..c806e2dd121 --- /dev/null +++ b/src/models/domain/field.ts @@ -0,0 +1,39 @@ +import { FieldType } from '../../enums/fieldType'; + +import { FieldData } from '../data/fieldData'; + +import { CipherString } from './cipherString'; +import Domain from './domain'; + +class Field extends Domain { + name: CipherString; + vault: CipherString; + type: FieldType; + + constructor(obj?: FieldData, alreadyEncrypted: boolean = false) { + super(); + if (obj == null) { + return; + } + + this.type = obj.type; + this.buildDomainModel(this, obj, { + name: null, + value: null, + }, alreadyEncrypted, []); + } + + decrypt(orgId: string): Promise { + const model = { + type: this.type, + }; + + return this.decryptObj(model, { + name: null, + value: null, + }, orgId); + } +} + +export { Field }; +(window as any).Field = Field; diff --git a/src/models/domain/folder.ts b/src/models/domain/folder.ts new file mode 100644 index 00000000000..180cd44e4f3 --- /dev/null +++ b/src/models/domain/folder.ts @@ -0,0 +1,34 @@ +import { FolderData } from '../data/folderData'; + +import { CipherString } from './cipherString'; +import Domain from './domain'; + +class Folder extends Domain { + id: string; + name: CipherString; + + constructor(obj?: FolderData, alreadyEncrypted: boolean = false) { + super(); + if (obj == null) { + return; + } + + this.buildDomainModel(this, obj, { + id: null, + name: null, + }, alreadyEncrypted, ['id']); + } + + decrypt(): Promise { + const model = { + id: this.id, + }; + + return this.decryptObj(model, { + name: null, + }, null); + } +} + +export { Folder }; +(window as any).Folder = Folder; diff --git a/src/models/domain/identity.ts b/src/models/domain/identity.ts new file mode 100644 index 00000000000..ede933ac309 --- /dev/null +++ b/src/models/domain/identity.ts @@ -0,0 +1,79 @@ +import { IdentityData } from '../data/identityData'; + +import { CipherString } from './cipherString'; +import Domain from './domain'; + +class Identity extends Domain { + title: CipherString; + firstName: CipherString; + middleName: CipherString; + lastName: CipherString; + address1: CipherString; + address2: CipherString; + address3: CipherString; + city: CipherString; + state: CipherString; + postalCode: CipherString; + country: CipherString; + company: CipherString; + email: CipherString; + phone: CipherString; + ssn: CipherString; + username: CipherString; + passportNumber: CipherString; + licenseNumber: CipherString; + + constructor(obj?: IdentityData, alreadyEncrypted: boolean = false) { + super(); + if (obj == null) { + return; + } + + this.buildDomainModel(this, obj, { + 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, + }, alreadyEncrypted, []); + } + + decrypt(orgId: string): Promise { + return this.decryptObj({}, { + 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, + }, orgId); + } +} + +export { Identity }; +(window as any).Identity = Identity; diff --git a/src/models/domain/index.ts b/src/models/domain/index.ts new file mode 100644 index 00000000000..698585a3c9b --- /dev/null +++ b/src/models/domain/index.ts @@ -0,0 +1,15 @@ +export { Attachment } from './attachment'; +export { Card } from './card'; +export { Cipher } from './cipher'; +export { CipherString } from './cipherString'; +export { Collection } from './collection'; +export { EncryptedObject } from './encryptedObject'; +export { EnvironmentUrls } from './environmentUrls'; +export { Field } from './field'; +export { Folder } from './folder'; +export { Identity } from './identity'; +export { Login } from './login'; +export { PasswordHistory } from './passwordHistory'; +export { SecureNote } from './secureNote'; +export { SymmetricCryptoKey } from './symmetricCryptoKey'; +export { SymmetricCryptoKeyBuffers } from './symmetricCryptoKeyBuffers'; diff --git a/src/models/domain/login.ts b/src/models/domain/login.ts new file mode 100644 index 00000000000..8ed1e1f7c7c --- /dev/null +++ b/src/models/domain/login.ts @@ -0,0 +1,37 @@ +import { LoginData } from '../data/loginData'; + +import { CipherString } from './cipherString'; +import Domain from './domain'; + +class Login extends Domain { + uri: CipherString; + username: CipherString; + password: CipherString; + totp: CipherString; + + constructor(obj?: LoginData, alreadyEncrypted: boolean = false) { + super(); + if (obj == null) { + return; + } + + this.buildDomainModel(this, obj, { + uri: null, + username: null, + password: null, + totp: null, + }, alreadyEncrypted, []); + } + + decrypt(orgId: string): Promise { + return this.decryptObj({}, { + uri: null, + username: null, + password: null, + totp: null, + }, orgId); + } +} + +export { Login }; +(window as any).Login = Login; diff --git a/src/models/domain/passwordHistory.ts b/src/models/domain/passwordHistory.ts new file mode 100644 index 00000000000..35e7892dcd7 --- /dev/null +++ b/src/models/domain/passwordHistory.ts @@ -0,0 +1,9 @@ +export class PasswordHistory { + password: string; + date: number; + + constructor(password: string, date: number) { + this.password = password; + this.date = date; + } +} diff --git a/src/models/domain/secureNote.ts b/src/models/domain/secureNote.ts new file mode 100644 index 00000000000..46609a5b70a --- /dev/null +++ b/src/models/domain/secureNote.ts @@ -0,0 +1,27 @@ +import { SecureNoteType } from '../../enums/secureNoteType'; + +import { SecureNoteData } from '../data/secureNoteData'; + +import Domain from './domain'; + +class SecureNote extends Domain { + type: SecureNoteType; + + constructor(obj?: SecureNoteData, alreadyEncrypted: boolean = false) { + super(); + if (obj == null) { + return; + } + + this.type = obj.type; + } + + decrypt(orgId: string): any { + return { + type: this.type, + }; + } +} + +export { SecureNote }; +(window as any).SecureNote = SecureNote; diff --git a/src/models/domain/symmetricCryptoKey.ts b/src/models/domain/symmetricCryptoKey.ts new file mode 100644 index 00000000000..093685f0533 --- /dev/null +++ b/src/models/domain/symmetricCryptoKey.ts @@ -0,0 +1,80 @@ +import * as forge from 'node-forge'; + +import { EncryptionType } from '../../enums/encryptionType'; + +import { SymmetricCryptoKeyBuffers } from './symmetricCryptoKeyBuffers'; + +import { UtilsService } from '../../services/utils.service'; + +export class SymmetricCryptoKey { + key: string; + keyB64: string; + encKey: string; + macKey: string; + encType: EncryptionType; + keyBuf: SymmetricCryptoKeyBuffers; + + constructor(keyBytes: string, b64KeyBytes?: boolean, encType?: EncryptionType) { + if (b64KeyBytes) { + keyBytes = forge.util.decode64(keyBytes); + } + + if (!keyBytes) { + throw new Error('Must provide keyBytes'); + } + + const buffer = (forge as any).util.createBuffer(keyBytes); + if (!buffer || buffer.length() === 0) { + throw new Error('Couldn\'t make buffer'); + } + + const bufferLength: number = buffer.length(); + + if (encType == null) { + if (bufferLength === 32) { + encType = EncryptionType.AesCbc256_B64; + } else if (bufferLength === 64) { + encType = EncryptionType.AesCbc256_HmacSha256_B64; + } else { + throw new Error('Unable to determine encType.'); + } + } + + this.key = keyBytes; + this.keyB64 = forge.util.encode64(keyBytes); + this.encType = encType; + + if (encType === EncryptionType.AesCbc256_B64 && bufferLength === 32) { + this.encKey = keyBytes; + this.macKey = null; + } else if (encType === EncryptionType.AesCbc128_HmacSha256_B64 && bufferLength === 32) { + this.encKey = buffer.getBytes(16); // first half + this.macKey = buffer.getBytes(16); // second half + } else if (encType === EncryptionType.AesCbc256_HmacSha256_B64 && bufferLength === 64) { + this.encKey = buffer.getBytes(32); // first half + this.macKey = buffer.getBytes(32); // second half + } else { + throw new Error('Unsupported encType/key length.'); + } + } + + getBuffers() { + if (this.keyBuf) { + return this.keyBuf; + } + + const key = UtilsService.fromB64ToArray(this.keyB64); + const keys = new SymmetricCryptoKeyBuffers(key.buffer); + + if (this.macKey) { + keys.encKey = key.slice(0, key.length / 2).buffer; + keys.macKey = key.slice(key.length / 2).buffer; + } else { + keys.encKey = key.buffer; + keys.macKey = null; + } + + this.keyBuf = keys; + return this.keyBuf; + } +} diff --git a/src/models/domain/symmetricCryptoKeyBuffers.ts b/src/models/domain/symmetricCryptoKeyBuffers.ts new file mode 100644 index 00000000000..f27d8aed2e2 --- /dev/null +++ b/src/models/domain/symmetricCryptoKeyBuffers.ts @@ -0,0 +1,9 @@ +export class SymmetricCryptoKeyBuffers { + key: ArrayBuffer; + encKey?: ArrayBuffer; + macKey?: ArrayBuffer; + + constructor(key: ArrayBuffer) { + this.key = key; + } +} diff --git a/src/models/request/cipherRequest.ts b/src/models/request/cipherRequest.ts new file mode 100644 index 00000000000..4829b20d270 --- /dev/null +++ b/src/models/request/cipherRequest.ts @@ -0,0 +1,89 @@ +import { CipherType } from '../../enums/cipherType'; + +class CipherRequest { + type: CipherType; + folderId: string; + organizationId: string; + name: string; + notes: string; + favorite: boolean; + login: any; + secureNote: any; + card: any; + identity: any; + fields: any[]; + + constructor(cipher: any) { + this.type = cipher.type; + this.folderId = cipher.folderId; + this.organizationId = cipher.organizationId; + this.name = cipher.name ? cipher.name.encryptedString : null; + this.notes = cipher.notes ? cipher.notes.encryptedString : null; + this.favorite = cipher.favorite; + + switch (this.type) { + case CipherType.Login: + this.login = { + uri: cipher.login.uri ? cipher.login.uri.encryptedString : null, + username: cipher.login.username ? cipher.login.username.encryptedString : null, + password: cipher.login.password ? cipher.login.password.encryptedString : null, + totp: cipher.login.totp ? cipher.login.totp.encryptedString : null, + }; + break; + case CipherType.SecureNote: + this.secureNote = { + type: cipher.secureNote.type, + }; + break; + case CipherType.Card: + this.card = { + cardholderName: cipher.card.cardholderName ? cipher.card.cardholderName.encryptedString : null, + brand: cipher.card.brand ? cipher.card.brand.encryptedString : null, + number: cipher.card.number ? cipher.card.number.encryptedString : null, + expMonth: cipher.card.expMonth ? cipher.card.expMonth.encryptedString : null, + expYear: cipher.card.expYear ? cipher.card.expYear.encryptedString : null, + code: cipher.card.code ? cipher.card.code.encryptedString : null, + }; + break; + case CipherType.Identity: + this.identity = { + title: cipher.identity.title ? cipher.identity.title.encryptedString : null, + firstName: cipher.identity.firstName ? cipher.identity.firstName.encryptedString : null, + middleName: cipher.identity.middleName ? cipher.identity.middleName.encryptedString : null, + lastName: cipher.identity.lastName ? cipher.identity.lastName.encryptedString : null, + address1: cipher.identity.address1 ? cipher.identity.address1.encryptedString : null, + address2: cipher.identity.address2 ? cipher.identity.address2.encryptedString : null, + address3: cipher.identity.address3 ? cipher.identity.address3.encryptedString : null, + city: cipher.identity.city ? cipher.identity.city.encryptedString : null, + state: cipher.identity.state ? cipher.identity.state.encryptedString : null, + postalCode: cipher.identity.postalCode ? cipher.identity.postalCode.encryptedString : null, + country: cipher.identity.country ? cipher.identity.country.encryptedString : null, + company: cipher.identity.company ? cipher.identity.company.encryptedString : null, + email: cipher.identity.email ? cipher.identity.email.encryptedString : null, + phone: cipher.identity.phone ? cipher.identity.phone.encryptedString : null, + ssn: cipher.identity.ssn ? cipher.identity.ssn.encryptedString : null, + username: cipher.identity.username ? cipher.identity.username.encryptedString : null, + passportNumber: cipher.identity.passportNumber ? + cipher.identity.passportNumber.encryptedString : null, + licenseNumber: cipher.identity.licenseNumber ? cipher.identity.licenseNumber.encryptedString : null, + }; + break; + default: + break; + } + + if (cipher.fields) { + this.fields = []; + cipher.fields.forEach((field: any) => { + this.fields.push({ + type: field.type, + name: field.name ? field.name.encryptedString : null, + value: field.value ? field.value.encryptedString : null, + }); + }); + } + } +} + +export { CipherRequest }; +(window as any).CipherRequest = CipherRequest; diff --git a/src/models/request/deviceRequest.ts b/src/models/request/deviceRequest.ts new file mode 100644 index 00000000000..5aaec5c77e8 --- /dev/null +++ b/src/models/request/deviceRequest.ts @@ -0,0 +1,20 @@ +import { DeviceType } from '../../enums/deviceType'; + +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +class DeviceRequest { + type: DeviceType; + name: string; + identifier: string; + pushToken?: string; + + constructor(appId: string, platformUtilsService: PlatformUtilsService) { + this.type = platformUtilsService.getDevice(); + this.name = platformUtilsService.getDeviceString(); + this.identifier = appId; + this.pushToken = null; + } +} + +export { DeviceRequest }; +(window as any).DeviceRequest = DeviceRequest; diff --git a/src/models/request/deviceTokenRequest.ts b/src/models/request/deviceTokenRequest.ts new file mode 100644 index 00000000000..69ef20bb9a7 --- /dev/null +++ b/src/models/request/deviceTokenRequest.ts @@ -0,0 +1,10 @@ +class DeviceTokenRequest { + pushToken: string; + + constructor() { + this.pushToken = null; + } +} + +export { DeviceTokenRequest }; +(window as any).DeviceTokenRequest = DeviceTokenRequest; diff --git a/src/models/request/folderRequest.ts b/src/models/request/folderRequest.ts new file mode 100644 index 00000000000..7ff9794b184 --- /dev/null +++ b/src/models/request/folderRequest.ts @@ -0,0 +1,12 @@ +import { Folder } from '../domain/folder'; + +class FolderRequest { + name: string; + + constructor(folder: Folder) { + this.name = folder.name ? folder.name.encryptedString : null; + } +} + +export { FolderRequest }; +(window as any).FolderRequest = FolderRequest; diff --git a/src/models/request/index.ts b/src/models/request/index.ts new file mode 100644 index 00000000000..51acd28f4c2 --- /dev/null +++ b/src/models/request/index.ts @@ -0,0 +1,8 @@ +export { CipherRequest } from './cipherRequest'; +export { DeviceRequest } from './deviceRequest'; +export { DeviceTokenRequest } from './deviceTokenRequest'; +export { FolderRequest } from './folderRequest'; +export { PasswordHintRequest } from './passwordHintRequest'; +export { RegisterRequest } from './registerRequest'; +export { TokenRequest } from './tokenRequest'; +export { TwoFactorEmailRequest } from './twoFactorEmailRequest'; diff --git a/src/models/request/passwordHintRequest.ts b/src/models/request/passwordHintRequest.ts new file mode 100644 index 00000000000..4feb92944bf --- /dev/null +++ b/src/models/request/passwordHintRequest.ts @@ -0,0 +1,10 @@ +class PasswordHintRequest { + email: string; + + constructor(email: string) { + this.email = email; + } +} + +export { PasswordHintRequest }; +(window as any).PasswordHintRequest = PasswordHintRequest; diff --git a/src/models/request/registerRequest.ts b/src/models/request/registerRequest.ts new file mode 100644 index 00000000000..4b80fb7884f --- /dev/null +++ b/src/models/request/registerRequest.ts @@ -0,0 +1,18 @@ +class RegisterRequest { + name: string; + email: string; + masterPasswordHash: string; + masterPasswordHint: string; + key: string; + + constructor(email: string, masterPasswordHash: string, masterPasswordHint: string, key: string) { + this.name = null; + this.email = email; + this.masterPasswordHash = masterPasswordHash; + this.masterPasswordHint = masterPasswordHint ? masterPasswordHint : null; + this.key = key; + } +} + +export { RegisterRequest }; +(window as any).RegisterRequest = RegisterRequest; diff --git a/src/models/request/tokenRequest.ts b/src/models/request/tokenRequest.ts new file mode 100644 index 00000000000..4cf75646155 --- /dev/null +++ b/src/models/request/tokenRequest.ts @@ -0,0 +1,49 @@ +import { DeviceRequest } from './deviceRequest'; + +class TokenRequest { + email: string; + masterPasswordHash: string; + token: string; + provider: number; + remember: boolean; + device?: DeviceRequest; + + constructor(email: string, masterPasswordHash: string, provider: number, + token: string, remember: boolean, device?: DeviceRequest) { + this.email = email; + this.masterPasswordHash = masterPasswordHash; + this.token = token; + this.provider = provider; + this.remember = remember; + this.device = device != null ? device : null; + } + + toIdentityToken() { + const obj: any = { + grant_type: 'password', + username: this.email, + password: this.masterPasswordHash, + scope: 'api offline_access', + client_id: 'browser', + }; + + if (this.device) { + obj.deviceType = this.device.type; + obj.deviceIdentifier = this.device.identifier; + obj.deviceName = this.device.name; + // no push tokens for browser apps yet + // obj.devicePushToken = this.device.pushToken; + } + + if (this.token && this.provider !== null && (typeof this.provider !== 'undefined')) { + obj.twoFactorToken = this.token; + obj.twoFactorProvider = this.provider; + obj.twoFactorRemember = this.remember ? '1' : '0'; + } + + return obj; + } +} + +export { TokenRequest }; +(window as any).TokenRequest = TokenRequest; diff --git a/src/models/request/twoFactorEmailRequest.ts b/src/models/request/twoFactorEmailRequest.ts new file mode 100644 index 00000000000..d540b08ecf1 --- /dev/null +++ b/src/models/request/twoFactorEmailRequest.ts @@ -0,0 +1,12 @@ +class TwoFactorEmailRequest { + email: string; + masterPasswordHash: string; + + constructor(email: string, masterPasswordHash: string) { + this.email = email; + this.masterPasswordHash = masterPasswordHash; + } +} + +export { TwoFactorEmailRequest }; +(window as any).TwoFactorEmailRequest = TwoFactorEmailRequest; diff --git a/src/models/response/attachmentResponse.ts b/src/models/response/attachmentResponse.ts new file mode 100644 index 00000000000..df5138650cf --- /dev/null +++ b/src/models/response/attachmentResponse.ts @@ -0,0 +1,18 @@ +class AttachmentResponse { + id: string; + url: string; + fileName: string; + size: number; + sizeName: string; + + constructor(response: any) { + this.id = response.Id; + this.url = response.Url; + this.fileName = response.FileName; + this.size = response.Size; + this.sizeName = response.SizeName; + } +} + +export { AttachmentResponse }; +(window as any).AttachmentResponse = AttachmentResponse; diff --git a/src/models/response/cipherResponse.ts b/src/models/response/cipherResponse.ts new file mode 100644 index 00000000000..b2e597e23a3 --- /dev/null +++ b/src/models/response/cipherResponse.ts @@ -0,0 +1,44 @@ +import { AttachmentResponse } from './attachmentResponse'; + +class CipherResponse { + id: string; + organizationId: string; + folderId: string; + type: number; + favorite: boolean; + edit: boolean; + organizationUseTotp: boolean; + data: any; + revisionDate: string; + attachments: AttachmentResponse[]; + collectionIds: string[]; + + constructor(response: any) { + this.id = response.Id; + this.organizationId = response.OrganizationId; + this.folderId = response.FolderId; + this.type = response.Type; + this.favorite = response.Favorite; + this.edit = response.Edit; + this.organizationUseTotp = response.OrganizationUseTotp; + this.data = response.Data; + this.revisionDate = response.RevisionDate; + + if (response.Attachments != null) { + this.attachments = []; + response.Attachments.forEach((attachment: any) => { + this.attachments.push(new AttachmentResponse(attachment)); + }); + } + + if (response.CollectionIds) { + this.collectionIds = []; + response.CollectionIds.forEach((id: string) => { + this.collectionIds.push(id); + }); + } + } +} + +export { CipherResponse }; +(window as any).CipherResponse = CipherResponse; diff --git a/src/models/response/collectionResponse.ts b/src/models/response/collectionResponse.ts new file mode 100644 index 00000000000..8b0247720db --- /dev/null +++ b/src/models/response/collectionResponse.ts @@ -0,0 +1,14 @@ +class CollectionResponse { + id: string; + organizationId: string; + name: string; + + constructor(response: any) { + this.id = response.Id; + this.organizationId = response.OrganizationId; + this.name = response.Name; + } +} + +export { CollectionResponse }; +(window as any).CollectionResponse = CollectionResponse; diff --git a/src/models/response/deviceResponse.ts b/src/models/response/deviceResponse.ts new file mode 100644 index 00000000000..5e76635d161 --- /dev/null +++ b/src/models/response/deviceResponse.ts @@ -0,0 +1,20 @@ +import { DeviceType } from '../../enums/deviceType'; + +class DeviceResponse { + id: string; + name: number; + identifier: string; + type: DeviceType; + creationDate: string; + + constructor(response: any) { + this.id = response.Id; + this.name = response.Name; + this.identifier = response.Identifier; + this.type = response.Type; + this.creationDate = response.CreationDate; + } +} + +export { DeviceResponse }; +(window as any).DeviceResponse = DeviceResponse; diff --git a/src/models/response/domainsResponse.ts b/src/models/response/domainsResponse.ts new file mode 100644 index 00000000000..d65077147bc --- /dev/null +++ b/src/models/response/domainsResponse.ts @@ -0,0 +1,20 @@ +import { GlobalDomainResponse } from './globalDomainResponse'; + +class DomainsResponse { + equivalentDomains: string[][]; + globalEquivalentDomains: GlobalDomainResponse[] = []; + + constructor(response: any) { + this.equivalentDomains = response.EquivalentDomains; + + this.globalEquivalentDomains = []; + if (response.GlobalEquivalentDomains) { + response.GlobalEquivalentDomains.forEach((domain: any) => { + this.globalEquivalentDomains.push(new GlobalDomainResponse(domain)); + }); + } + } +} + +export { DomainsResponse }; +(window as any).DomainsResponse = DomainsResponse; diff --git a/src/models/response/errorResponse.ts b/src/models/response/errorResponse.ts new file mode 100644 index 00000000000..4d976932cd2 --- /dev/null +++ b/src/models/response/errorResponse.ts @@ -0,0 +1,37 @@ +class ErrorResponse { + message: string; + validationErrors: { [key: string]: string[]; }; + statusCode: number; + + constructor(response: any, status: number, identityResponse?: boolean) { + let errorModel = null; + if (identityResponse && response && response.ErrorModel) { + errorModel = response.ErrorModel; + } else if (response) { + errorModel = response; + } + + if (errorModel) { + this.message = errorModel.Message; + this.validationErrors = errorModel.ValidationErrors; + } + this.statusCode = status; + } + + getSingleMessage(): string { + if (this.validationErrors) { + for (const key in this.validationErrors) { + if (!this.validationErrors.hasOwnProperty(key)) { + continue; + } + if (this.validationErrors[key].length) { + return this.validationErrors[key][0]; + } + } + } + return this.message; + } +} + +export { ErrorResponse }; +(window as any).ErrorResponse = ErrorResponse; diff --git a/src/models/response/folderResponse.ts b/src/models/response/folderResponse.ts new file mode 100644 index 00000000000..c5ff0ada70a --- /dev/null +++ b/src/models/response/folderResponse.ts @@ -0,0 +1,14 @@ +class FolderResponse { + id: string; + name: string; + revisionDate: string; + + constructor(response: any) { + this.id = response.Id; + this.name = response.Name; + this.revisionDate = response.RevisionDate; + } +} + +export { FolderResponse }; +(window as any).FolderResponse = FolderResponse; diff --git a/src/models/response/globalDomainResponse.ts b/src/models/response/globalDomainResponse.ts new file mode 100644 index 00000000000..8e9a45df11e --- /dev/null +++ b/src/models/response/globalDomainResponse.ts @@ -0,0 +1,14 @@ +class GlobalDomainResponse { + type: number; + domains: string[]; + excluded: number[]; + + constructor(response: any) { + this.type = response.Type; + this.domains = response.Domains; + this.excluded = response.Excluded; + } +} + +export { GlobalDomainResponse }; +(window as any).GlobalDomainResponse = GlobalDomainResponse; diff --git a/src/models/response/identityTokenResponse.ts b/src/models/response/identityTokenResponse.ts new file mode 100644 index 00000000000..2d188707c05 --- /dev/null +++ b/src/models/response/identityTokenResponse.ts @@ -0,0 +1,24 @@ +class IdentityTokenResponse { + accessToken: string; + expiresIn: number; + refreshToken: string; + tokenType: string; + + privateKey: string; + key: string; + twoFactorToken: string; + + constructor(response: any) { + this.accessToken = response.access_token; + this.expiresIn = response.expires_in; + this.refreshToken = response.refresh_token; + this.tokenType = response.token_type; + + this.privateKey = response.PrivateKey; + this.key = response.Key; + this.twoFactorToken = response.TwoFactorToken; + } +} + +export { IdentityTokenResponse }; +(window as any).IdentityTokenResponse = IdentityTokenResponse; diff --git a/src/models/response/index.ts b/src/models/response/index.ts new file mode 100644 index 00000000000..00f85d119d8 --- /dev/null +++ b/src/models/response/index.ts @@ -0,0 +1,14 @@ +export { AttachmentResponse } from './attachmentResponse'; +export { CipherResponse } from './cipherResponse'; +export { CollectionResponse } from './collectionResponse'; +export { DeviceResponse } from './deviceResponse'; +export { DomainsResponse } from './domainsResponse'; +export { ErrorResponse } from './errorResponse'; +export { FolderResponse } from './folderResponse'; +export { GlobalDomainResponse } from './globalDomainResponse'; +export { IdentityTokenResponse } from './identityTokenResponse'; +export { KeysResponse } from './keysResponse'; +export { ListResponse } from './listResponse'; +export { ProfileOrganizationResponse } from './profileOrganizationResponse'; +export { ProfileResponse } from './profileResponse'; +export { SyncResponse } from './syncResponse'; diff --git a/src/models/response/keysResponse.ts b/src/models/response/keysResponse.ts new file mode 100644 index 00000000000..cb96dad51e4 --- /dev/null +++ b/src/models/response/keysResponse.ts @@ -0,0 +1,12 @@ +class KeysResponse { + privateKey: string; + publicKey: string; + + constructor(response: any) { + this.privateKey = response.PrivateKey; + this.publicKey = response.PublicKey; + } +} + +export { KeysResponse }; +(window as any).KeysResponse = KeysResponse; diff --git a/src/models/response/listResponse.ts b/src/models/response/listResponse.ts new file mode 100644 index 00000000000..9cf5455ada8 --- /dev/null +++ b/src/models/response/listResponse.ts @@ -0,0 +1,10 @@ +class ListResponse { + data: any; + + constructor(data: any) { + this.data = data; + } +} + +export { ListResponse }; +(window as any).ListResponse = ListResponse; diff --git a/src/models/response/profileOrganizationResponse.ts b/src/models/response/profileOrganizationResponse.ts new file mode 100644 index 00000000000..484857745a0 --- /dev/null +++ b/src/models/response/profileOrganizationResponse.ts @@ -0,0 +1,30 @@ +class ProfileOrganizationResponse { + id: string; + name: string; + useGroups: boolean; + useDirectory: boolean; + useTotp: boolean; + seats: number; + maxCollections: number; + maxStorageGb?: number; + key: string; + status: number; // TODO: map to enum + type: number; // TODO: map to enum + + constructor(response: any) { + this.id = response.Id; + this.name = response.Name; + this.useGroups = response.UseGroups; + this.useDirectory = response.UseDirectory; + this.useTotp = response.UseTotp; + this.seats = response.Seats; + this.maxCollections = response.MaxCollections; + this.maxStorageGb = response.MaxStorageGb; + this.key = response.Key; + this.status = response.Status; + this.type = response.Type; + } +} + +export { ProfileOrganizationResponse }; +(window as any).ProfileOrganizationResponse = ProfileOrganizationResponse; diff --git a/src/models/response/profileResponse.ts b/src/models/response/profileResponse.ts new file mode 100644 index 00000000000..69e4f3e9cb8 --- /dev/null +++ b/src/models/response/profileResponse.ts @@ -0,0 +1,39 @@ +import { ProfileOrganizationResponse } from './profileOrganizationResponse'; + +class ProfileResponse { + id: string; + name: string; + email: string; + emailVerified: boolean; + masterPasswordHint: string; + premium: boolean; + culture: string; + twoFactorEnabled: boolean; + key: string; + privateKey: string; + securityStamp: string; + organizations: ProfileOrganizationResponse[] = []; + + constructor(response: any) { + this.id = response.Id; + this.name = response.Name; + this.email = response.Email; + this.emailVerified = response.EmailVerified; + this.masterPasswordHint = response.MasterPasswordHint; + this.premium = response.Premium; + this.culture = response.Culture; + this.twoFactorEnabled = response.TwoFactorEnabled; + this.key = response.Key; + this.privateKey = response.PrivateKey; + this.securityStamp = response.SecurityStamp; + + if (response.Organizations) { + response.Organizations.forEach((org: any) => { + this.organizations.push(new ProfileOrganizationResponse(org)); + }); + } + } +} + +export { ProfileResponse }; +(window as any).ProfileResponse = ProfileResponse; diff --git a/src/models/response/syncResponse.ts b/src/models/response/syncResponse.ts new file mode 100644 index 00000000000..8acdc5202dc --- /dev/null +++ b/src/models/response/syncResponse.ts @@ -0,0 +1,44 @@ +import { CipherResponse } from './cipherResponse'; +import { CollectionResponse } from './collectionResponse'; +import { DomainsResponse } from './domainsResponse'; +import { FolderResponse } from './folderResponse'; +import { ProfileResponse } from './profileResponse'; + +class SyncResponse { + profile?: ProfileResponse; + folders: FolderResponse[] = []; + collections: CollectionResponse[] = []; + ciphers: CipherResponse[] = []; + domains?: DomainsResponse; + + constructor(response: any) { + if (response.Profile) { + this.profile = new ProfileResponse(response.Profile); + } + + if (response.Folders) { + response.Folders.forEach((folder: any) => { + this.folders.push(new FolderResponse(folder)); + }); + } + + if (response.Collections) { + response.Collections.forEach((collection: any) => { + this.collections.push(new CollectionResponse(collection)); + }); + } + + if (response.Ciphers) { + response.Ciphers.forEach((cipher: any) => { + this.ciphers.push(new CipherResponse(cipher)); + }); + } + + if (response.Domains) { + this.domains = new DomainsResponse(response.Domains); + } + } +} + +export { SyncResponse }; +(window as any).SyncResponse = SyncResponse; diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts new file mode 100644 index 00000000000..30206c902c4 --- /dev/null +++ b/src/services/constants.service.ts @@ -0,0 +1,116 @@ +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; + +export class ConstantsService { + static readonly environmentUrlsKey: string = 'environmentUrls'; + static readonly disableGaKey: string = 'disableGa'; + static readonly disableAddLoginNotificationKey: string = 'disableAddLoginNotification'; + static readonly disableContextMenuItemKey: string = 'disableContextMenuItem'; + static readonly disableFaviconKey: string = 'disableFavicon'; + static readonly disableAutoTotpCopyKey: string = 'disableAutoTotpCopy'; + static readonly enableAutoFillOnPageLoadKey: string = 'enableAutoFillOnPageLoad'; + static readonly lockOptionKey: string = 'lockOption'; + static readonly lastActiveKey: string = 'lastActive'; + + // TODO: remove these instance properties once all references are reading from the static properties + readonly environmentUrlsKey: string = 'environmentUrls'; + readonly disableGaKey: string = 'disableGa'; + readonly disableAddLoginNotificationKey: string = 'disableAddLoginNotification'; + readonly disableContextMenuItemKey: string = 'disableContextMenuItem'; + readonly disableFaviconKey: string = 'disableFavicon'; + readonly disableAutoTotpCopyKey: string = 'disableAutoTotpCopy'; + readonly enableAutoFillOnPageLoadKey: string = 'enableAutoFillOnPageLoad'; + readonly lockOptionKey: string = 'lockOption'; + readonly lastActiveKey: string = 'lastActive'; + + // TODO: Convert these objects to enums + readonly encType: any = { + AesCbc256_B64: 0, + AesCbc128_HmacSha256_B64: 1, + AesCbc256_HmacSha256_B64: 2, + Rsa2048_OaepSha256_B64: 3, + Rsa2048_OaepSha1_B64: 4, + Rsa2048_OaepSha256_HmacSha256_B64: 5, + Rsa2048_OaepSha1_HmacSha256_B64: 6, + }; + + readonly cipherType: any = { + login: 1, + secureNote: 2, + card: 3, + identity: 4, + }; + + readonly fieldType: any = { + text: 0, + hidden: 1, + boolean: 2, + }; + + readonly twoFactorProvider: any = { + u2f: 4, + yubikey: 3, + duo: 2, + authenticator: 0, + email: 1, + remember: 5, + }; + + twoFactorProviderInfo: any[]; + + constructor(i18nService: any, platformUtilsService: PlatformUtilsService) { + if (platformUtilsService.isEdge()) { + // delay for i18n fetch + setTimeout(() => { + this.bootstrap(i18nService); + }, 1000); + } else { + this.bootstrap(i18nService); + } + } + + private bootstrap(i18nService: any) { + this.twoFactorProviderInfo = [ + { + type: 0, + name: i18nService.authenticatorAppTitle, + description: i18nService.authenticatorAppDesc, + active: true, + free: true, + displayOrder: 0, + priority: 1, + }, + { + type: 3, + name: i18nService.yubiKeyTitle, + description: i18nService.yubiKeyDesc, + active: true, + displayOrder: 1, + priority: 3, + }, + { + type: 2, + name: 'Duo', + description: i18nService.duoDesc, + active: true, + displayOrder: 2, + priority: 2, + }, + { + type: 4, + name: i18nService.u2fTitle, + description: i18nService.u2fDesc, + active: true, + displayOrder: 3, + priority: 4, + }, + { + type: 1, + name: i18nService.emailTitle, + description: i18nService.emailDesc, + active: true, + displayOrder: 4, + priority: 0, + }, + ]; + } +} diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts new file mode 100644 index 00000000000..adce2e7911e --- /dev/null +++ b/src/services/crypto.service.ts @@ -0,0 +1,602 @@ +import * as forge from 'node-forge'; + +import { EncryptionType } from '../enums/encryptionType'; + +import { CipherString } from '../models/domain/cipherString'; +import { EncryptedObject } from '../models/domain/encryptedObject'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; +import { ProfileOrganizationResponse } from '../models/response/profileOrganizationResponse'; + +import { CryptoService as CryptoServiceInterface } from '../abstractions/crypto.service'; +import { StorageService as StorageServiceInterface } from '../abstractions/storage.service'; + +import { ConstantsService } from './constants.service'; +import { UtilsService } from './utils.service'; + +const Keys = { + key: 'key', + encOrgKeys: 'encOrgKeys', + encPrivateKey: 'encPrivateKey', + encKey: 'encKey', + keyHash: 'keyHash', +}; + +const SigningAlgorithm = { + name: 'HMAC', + hash: { name: 'SHA-256' }, +}; + +const AesAlgorithm = { + name: 'AES-CBC', +}; + +const Crypto = window.crypto; +const Subtle = Crypto.subtle; + +export class CryptoService implements CryptoServiceInterface { + private key: SymmetricCryptoKey; + private encKey: SymmetricCryptoKey; + private legacyEtmKey: SymmetricCryptoKey; + private keyHash: string; + private privateKey: ArrayBuffer; + private orgKeys: Map; + + constructor(private storageService: StorageServiceInterface, + private secureStorageService: StorageServiceInterface) { + } + + async setKey(key: SymmetricCryptoKey): Promise { + this.key = key; + + const option = await this.storageService.get(ConstantsService.lockOptionKey); + if (option != null) { + // if we have a lock option set, we do not store the key + return; + } + + return this.secureStorageService.save(Keys.key, key.keyB64); + } + + setKeyHash(keyHash: string): Promise<{}> { + this.keyHash = keyHash; + return this.storageService.save(Keys.keyHash, keyHash); + } + + async setEncKey(encKey: string): Promise<{}> { + if (encKey == null) { + return; + } + await this.storageService.save(Keys.encKey, encKey); + this.encKey = null; + } + + async setEncPrivateKey(encPrivateKey: string): Promise<{}> { + if (encPrivateKey == null) { + return; + } + + await this.storageService.save(Keys.encPrivateKey, encPrivateKey); + this.privateKey = null; + } + + setOrgKeys(orgs: ProfileOrganizationResponse[]): Promise<{}> { + const orgKeys: any = {}; + orgs.forEach((org) => { + orgKeys[org.id] = org.key; + }); + + return this.storageService.save(Keys.encOrgKeys, orgKeys); + } + + async getKey(): Promise { + if (this.key != null) { + return this.key; + } + + const option = await this.storageService.get(ConstantsService.lockOptionKey); + if (option != null) { + return null; + } + + const key = await this.secureStorageService.get(Keys.key); + if (key) { + this.key = new SymmetricCryptoKey(key, true); + } + + return key == null ? null : this.key; + } + + getKeyHash(): Promise { + if (this.keyHash != null) { + return Promise.resolve(this.keyHash); + } + + return this.storageService.get(Keys.keyHash); + } + + async getEncKey(): Promise { + if (this.encKey != null) { + return this.encKey; + } + + const encKey = await this.storageService.get(Keys.encKey); + if (encKey == null) { + return null; + } + + const key = await this.getKey(); + if (key == null) { + return null; + } + + const decEncKey = await this.decrypt(new CipherString(encKey), key, 'raw'); + if (decEncKey == null) { + return null; + } + + this.encKey = new SymmetricCryptoKey(decEncKey); + return this.encKey; + } + + async getPrivateKey(): Promise { + if (this.privateKey != null) { + return this.privateKey; + } + + const encPrivateKey = await this.storageService.get(Keys.encPrivateKey); + if (encPrivateKey == null) { + return null; + } + + const privateKey = await this.decrypt(new CipherString(encPrivateKey), null, 'raw'); + const privateKeyB64 = forge.util.encode64(privateKey); + this.privateKey = UtilsService.fromB64ToArray(privateKeyB64).buffer; + return this.privateKey; + } + + async getOrgKeys(): Promise> { + if (this.orgKeys != null && this.orgKeys.size > 0) { + return this.orgKeys; + } + + const encOrgKeys = await this.storageService.get(Keys.encOrgKeys); + if (!encOrgKeys) { + return null; + } + + const orgKeys: Map = new Map(); + let setKey = false; + + for (const orgId in encOrgKeys) { + if (!encOrgKeys.hasOwnProperty(orgId)) { + continue; + } + + const decValueB64 = await this.rsaDecrypt(encOrgKeys[orgId]); + orgKeys.set(orgId, new SymmetricCryptoKey(decValueB64, true)); + setKey = true; + } + + if (setKey) { + this.orgKeys = orgKeys; + } + + return this.orgKeys; + } + + async getOrgKey(orgId: string): Promise { + if (orgId == null) { + return null; + } + + const orgKeys = await this.getOrgKeys(); + if (orgKeys == null || !orgKeys.has(orgId)) { + return null; + } + + return orgKeys.get(orgId); + } + + clearKey(): Promise { + this.key = this.legacyEtmKey = null; + return this.secureStorageService.remove(Keys.key); + } + + clearKeyHash(): Promise { + this.keyHash = null; + return this.storageService.remove(Keys.keyHash); + } + + clearEncKey(memoryOnly?: boolean): Promise { + this.encKey = null; + if (memoryOnly) { + return Promise.resolve(); + } + return this.storageService.remove(Keys.encKey); + } + + clearPrivateKey(memoryOnly?: boolean): Promise { + this.privateKey = null; + if (memoryOnly) { + return Promise.resolve(); + } + return this.storageService.remove(Keys.encPrivateKey); + } + + clearOrgKeys(memoryOnly?: boolean): Promise { + this.orgKeys = null; + if (memoryOnly) { + return Promise.resolve(); + } + return this.storageService.remove(Keys.encOrgKeys); + } + + clearKeys(): Promise { + return Promise.all([ + this.clearKey(), + this.clearKeyHash(), + this.clearOrgKeys(), + this.clearEncKey(), + this.clearPrivateKey(), + ]); + } + + async toggleKey(): Promise { + const key = await this.getKey(); + const option = await this.storageService.get(ConstantsService.lockOptionKey); + if (option != null || option === 0) { + // if we have a lock option set, clear the key + await this.clearKey(); + this.key = key; + return; + } + + await this.setKey(key); + } + + makeKey(password: string, salt: string): SymmetricCryptoKey { + const keyBytes: string = (forge as any).pbkdf2(forge.util.encodeUtf8(password), forge.util.encodeUtf8(salt), + 5000, 256 / 8, 'sha256'); + return new SymmetricCryptoKey(keyBytes); + } + + async hashPassword(password: string, key: SymmetricCryptoKey): Promise { + const storedKey = await this.getKey(); + key = key || storedKey; + if (!password || !key) { + throw new Error('Invalid parameters.'); + } + + const hashBits = (forge as any).pbkdf2(key.key, forge.util.encodeUtf8(password), 1, 256 / 8, 'sha256'); + return forge.util.encode64(hashBits); + } + + makeEncKey(key: SymmetricCryptoKey): Promise { + const bytes = new Uint8Array(512 / 8); + Crypto.getRandomValues(bytes); + return this.encrypt(bytes, key, 'raw'); + } + + async encrypt(plainValue: string | Uint8Array, key?: SymmetricCryptoKey, + plainValueEncoding: string = 'utf8'): Promise { + if (!plainValue) { + return Promise.resolve(null); + } + + let plainValueArr: Uint8Array; + if (plainValueEncoding === 'utf8') { + plainValueArr = UtilsService.fromUtf8ToArray(plainValue as string); + } else { + plainValueArr = plainValue as Uint8Array; + } + + const encValue = await this.aesEncrypt(plainValueArr.buffer, key); + const iv = UtilsService.fromBufferToB64(encValue.iv.buffer); + const ct = UtilsService.fromBufferToB64(encValue.ct.buffer); + const mac = encValue.mac ? UtilsService.fromBufferToB64(encValue.mac.buffer) : null; + return new CipherString(encValue.key.encType, iv, ct, mac); + } + + async encryptToBytes(plainValue: ArrayBuffer, key?: SymmetricCryptoKey): Promise { + const encValue = await this.aesEncrypt(plainValue, key); + let macLen = 0; + if (encValue.mac) { + macLen = encValue.mac.length; + } + + const encBytes = new Uint8Array(1 + encValue.iv.length + macLen + encValue.ct.length); + encBytes.set([encValue.key.encType]); + encBytes.set(encValue.iv, 1); + if (encValue.mac) { + encBytes.set(encValue.mac, 1 + encValue.iv.length); + } + + encBytes.set(encValue.ct, 1 + encValue.iv.length + macLen); + return encBytes.buffer; + } + + async decrypt(cipherString: CipherString, key?: SymmetricCryptoKey, + outputEncoding: string = 'utf8'): Promise { + const ivBytes: string = forge.util.decode64(cipherString.initializationVector); + const ctBytes: string = forge.util.decode64(cipherString.cipherText); + const macBytes: string = cipherString.mac ? forge.util.decode64(cipherString.mac) : null; + const decipher = await this.aesDecrypt(cipherString.encryptionType, ctBytes, ivBytes, macBytes, key); + if (!decipher) { + return null; + } + + if (outputEncoding === 'utf8') { + return decipher.output.toString('utf8'); + } else { + return decipher.output.getBytes(); + } + } + + async decryptFromBytes(encBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise { + if (!encBuf) { + throw new Error('no encBuf.'); + } + + const encBytes = new Uint8Array(encBuf); + const encType = encBytes[0]; + let ctBytes: Uint8Array = null; + let ivBytes: Uint8Array = null; + let macBytes: Uint8Array = null; + + switch (encType) { + case EncryptionType.AesCbc128_HmacSha256_B64: + case EncryptionType.AesCbc256_HmacSha256_B64: + if (encBytes.length <= 49) { // 1 + 16 + 32 + ctLength + return null; + } + + ivBytes = encBytes.slice(1, 17); + macBytes = encBytes.slice(17, 49); + ctBytes = encBytes.slice(49); + break; + case EncryptionType.AesCbc256_B64: + if (encBytes.length <= 17) { // 1 + 16 + ctLength + return null; + } + + ivBytes = encBytes.slice(1, 17); + ctBytes = encBytes.slice(17); + break; + default: + return null; + } + + return await this.aesDecryptWC(encType, ctBytes.buffer, ivBytes.buffer, macBytes ? macBytes.buffer : null, key); + } + + async rsaDecrypt(encValue: string): Promise { + const headerPieces = encValue.split('.'); + let encType: EncryptionType = null; + let encPieces: string[]; + + if (headerPieces.length === 1) { + encType = EncryptionType.Rsa2048_OaepSha256_B64; + encPieces = [headerPieces[0]]; + } else if (headerPieces.length === 2) { + try { + encType = parseInt(headerPieces[0], null); + encPieces = headerPieces[1].split('|'); + } catch (e) { } + } + + switch (encType) { + case EncryptionType.Rsa2048_OaepSha256_B64: + case EncryptionType.Rsa2048_OaepSha1_B64: + if (encPieces.length !== 1) { + throw new Error('Invalid cipher format.'); + } + break; + case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: + case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: + if (encPieces.length !== 2) { + throw new Error('Invalid cipher format.'); + } + break; + default: + throw new Error('encType unavailable.'); + } + + if (encPieces == null || encPieces.length <= 0) { + throw new Error('encPieces unavailable.'); + } + + const key = await this.getEncKey(); + if (key != null && key.macKey != null && encPieces.length > 1) { + const ctBytes: string = forge.util.decode64(encPieces[0]); + const macBytes: string = forge.util.decode64(encPieces[1]); + const computedMacBytes = await this.computeMac(ctBytes, key.macKey, false); + const macsEqual = await this.macsEqual(key.macKey, macBytes, computedMacBytes); + if (!macsEqual) { + throw new Error('MAC failed.'); + } + } + + const privateKeyBytes = await this.getPrivateKey(); + if (!privateKeyBytes) { + throw new Error('No private key.'); + } + + let rsaAlgorithm: any = null; + switch (encType) { + case EncryptionType.Rsa2048_OaepSha256_B64: + case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: + rsaAlgorithm = { + name: 'RSA-OAEP', + hash: { name: 'SHA-256' }, + }; + break; + case EncryptionType.Rsa2048_OaepSha1_B64: + case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: + rsaAlgorithm = { + name: 'RSA-OAEP', + hash: { name: 'SHA-1' }, + }; + break; + default: + throw new Error('encType unavailable.'); + } + + const privateKey = await Subtle.importKey('pkcs8', privateKeyBytes, rsaAlgorithm, false, ['decrypt']); + const ctArr = UtilsService.fromB64ToArray(encPieces[0]); + const decBytes = await Subtle.decrypt(rsaAlgorithm, privateKey, ctArr.buffer); + const b64DecValue = UtilsService.fromBufferToB64(decBytes); + return b64DecValue; + } + + // Helpers + + private async aesEncrypt(plainValue: ArrayBuffer, key: SymmetricCryptoKey): Promise { + const obj = new EncryptedObject(); + obj.key = await this.getKeyForEncryption(key); + const keyBuf = obj.key.getBuffers(); + + obj.iv = new Uint8Array(16); + Crypto.getRandomValues(obj.iv); + + const encKey = await Subtle.importKey('raw', keyBuf.encKey, AesAlgorithm, false, ['encrypt']); + const encValue = await Subtle.encrypt({ name: 'AES-CBC', iv: obj.iv }, encKey, plainValue); + obj.ct = new Uint8Array(encValue); + + if (keyBuf.macKey) { + const data = new Uint8Array(obj.iv.length + obj.ct.length); + data.set(obj.iv, 0); + data.set(obj.ct, obj.iv.length); + const mac = await this.computeMacWC(data.buffer, keyBuf.macKey); + obj.mac = new Uint8Array(mac); + } + + return obj; + } + + private async aesDecrypt(encType: EncryptionType, ctBytes: string, ivBytes: string, macBytes: string, + key: SymmetricCryptoKey): Promise { + const keyForEnc = await this.getKeyForEncryption(key); + const theKey = this.resolveLegacyKey(encType, keyForEnc); + + if (encType !== theKey.encType) { + // tslint:disable-next-line + console.error('encType unavailable.'); + return null; + } + + if (theKey.macKey != null && macBytes != null) { + const computedMacBytes = this.computeMac(ivBytes + ctBytes, theKey.macKey, false); + if (!this.macsEqual(theKey.macKey, computedMacBytes, macBytes)) { + // tslint:disable-next-line + console.error('MAC failed.'); + return null; + } + } + + const ctBuffer = (forge as any).util.createBuffer(ctBytes); + const decipher = (forge as any).cipher.createDecipher('AES-CBC', theKey.encKey); + decipher.start({ iv: ivBytes }); + decipher.update(ctBuffer); + decipher.finish(); + + return decipher; + } + + private async aesDecryptWC(encType: EncryptionType, ctBuf: ArrayBuffer, ivBuf: ArrayBuffer, + macBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise { + const theKey = await this.getKeyForEncryption(key); + const keyBuf = theKey.getBuffers(); + const encKey = await Subtle.importKey('raw', keyBuf.encKey, AesAlgorithm, false, ['decrypt']); + if (!keyBuf.macKey || !macBuf) { + return null; + } + + const data = new Uint8Array(ivBuf.byteLength + ctBuf.byteLength); + data.set(new Uint8Array(ivBuf), 0); + data.set(new Uint8Array(ctBuf), ivBuf.byteLength); + const computedMacBuf = await this.computeMacWC(data.buffer, keyBuf.macKey); + if (computedMacBuf === null) { + return null; + } + + const macsMatch = await this.macsEqualWC(keyBuf.macKey, macBuf, computedMacBuf); + if (macsMatch === false) { + // tslint:disable-next-line + console.error('MAC failed.'); + return null; + } + + return await Subtle.decrypt({ name: 'AES-CBC', iv: ivBuf }, encKey, ctBuf); + } + + private computeMac(dataBytes: string, macKey: string, b64Output: boolean): string { + const hmac = (forge as any).hmac.create(); + hmac.start('sha256', macKey); + hmac.update(dataBytes); + const mac = hmac.digest(); + return b64Output ? forge.util.encode64(mac.getBytes()) : mac.getBytes(); + } + + private async computeMacWC(dataBuf: ArrayBuffer, macKeyBuf: ArrayBuffer): Promise { + const key = await Subtle.importKey('raw', macKeyBuf, SigningAlgorithm, false, ['sign']); + return await Subtle.sign(SigningAlgorithm, key, dataBuf); + } + + // 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/ + private macsEqual(macKey: string, mac1: string, mac2: string): boolean { + const hmac = (forge as any).hmac.create(); + + hmac.start('sha256', macKey); + hmac.update(mac1); + const mac1Bytes = hmac.digest().getBytes(); + + hmac.start(null, null); + hmac.update(mac2); + const mac2Bytes = hmac.digest().getBytes(); + + return mac1Bytes === mac2Bytes; + } + + private async macsEqualWC(macKeyBuf: ArrayBuffer, mac1Buf: ArrayBuffer, mac2Buf: ArrayBuffer): Promise { + const macKey = await Subtle.importKey('raw', macKeyBuf, SigningAlgorithm, false, ['sign']); + const mac1 = await Subtle.sign(SigningAlgorithm, macKey, mac1Buf); + const mac2 = await Subtle.sign(SigningAlgorithm, macKey, mac2Buf); + + if (mac1.byteLength !== mac2.byteLength) { + return false; + } + + const arr1 = new Uint8Array(mac1); + const arr2 = new Uint8Array(mac2); + + for (let i = 0; i < arr2.length; i++) { + if (arr1[i] !== arr2[i]) { + return false; + } + } + + return true; + } + + private async getKeyForEncryption(key?: SymmetricCryptoKey): Promise { + if (key) { + return key; + } + + const encKey = await this.getEncKey(); + return encKey || (await this.getKey()); + } + + private resolveLegacyKey(encType: EncryptionType, key: SymmetricCryptoKey): SymmetricCryptoKey { + if (encType === EncryptionType.AesCbc128_HmacSha256_B64 && + key.encType === EncryptionType.AesCbc256_B64) { + // Old encrypt-then-mac scheme, make a new key + this.legacyEtmKey = this.legacyEtmKey || + new SymmetricCryptoKey(key.key, false, EncryptionType.AesCbc128_HmacSha256_B64); + return this.legacyEtmKey; + } + + return key; + } +} diff --git a/src/services/index.ts b/src/services/index.ts index f93851e07e5..81f2bb9e3d8 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1 +1,3 @@ +export { ConstantsService } from './crypto.service'; +export { CryptoService } from './crypto.service'; export { UtilsService } from './utils.service'; diff --git a/tsconfig.json b/tsconfig.json index 60f0d3494f6..e647a0bd85a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,11 +12,7 @@ "outDir": "dist/es", "typeRoots": [ "node_modules/@types" - ], - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "noUnusedLocals": true, - "noUnusedParameters": true + ] }, "include": [ "src"