From ace8bd5e85274d123c2336ff51ace051745c4fa2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 3 Jan 2018 21:04:06 -0500 Subject: [PATCH 0001/1626] first commit --- README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000000..5399b6ed7d --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# jslib From a95632294fe5f67e024a020e22db2d9e13cdfc58 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 3 Jan 2018 21:20:41 -0500 Subject: [PATCH 0002/1626] copy common ts code over from browser repo --- src/enums/browserType.enum.ts | 8 + src/enums/cipherType.enum.ts | 6 + src/enums/encryptionType.enum.ts | 9 + src/enums/fieldType.enum.ts | 5 + src/enums/secureNoteType.enum.ts | 3 + src/models/data/attachmentData.ts | 20 + src/models/data/cardData.ts | 20 + src/models/data/cipherData.ts | 87 ++ src/models/data/collectionData.ts | 16 + src/models/data/fieldData.ts | 16 + src/models/data/folderData.ts | 18 + src/models/data/identityData.ts | 44 + src/models/data/loginData.ts | 16 + src/models/data/secureNoteData.ts | 12 + src/models/domain/attachment.ts | 43 + src/models/domain/autofillField.ts | 22 + src/models/domain/autofillForm.ts | 7 + src/models/domain/autofillPageDetails.ts | 13 + src/models/domain/autofillScript.ts | 12 + src/models/domain/card.ts | 43 + src/models/domain/cipher.ts | 192 ++++ src/models/domain/cipherString.ts | 115 +++ src/models/domain/collection.ts | 37 + src/models/domain/domain.ts | 46 + src/models/domain/encryptedObject.ts | 8 + src/models/domain/environmentUrls.ts | 5 + src/models/domain/field.ts | 39 + src/models/domain/folder.ts | 34 + src/models/domain/identity.ts | 79 ++ src/models/domain/login.ts | 37 + src/models/domain/passwordHistory.ts | 9 + src/models/domain/secureNote.ts | 27 + src/models/domain/symmetricCryptoKey.ts | 80 ++ .../domain/symmetricCryptoKeyBuffers.ts | 9 + src/models/request/cipherRequest.ts | 89 ++ src/models/request/deviceRequest.ts | 19 + src/models/request/deviceTokenRequest.ts | 10 + src/models/request/folderRequest.ts | 12 + src/models/request/passwordHintRequest.ts | 10 + src/models/request/registerRequest.ts | 18 + src/models/request/tokenRequest.ts | 49 + src/models/request/twoFactorEmailRequest.ts | 12 + src/models/response/attachmentResponse.ts | 18 + src/models/response/cipherResponse.ts | 44 + src/models/response/collectionResponse.ts | 14 + src/models/response/deviceResponse.ts | 20 + src/models/response/domainsResponse.ts | 20 + src/models/response/errorResponse.ts | 37 + src/models/response/folderResponse.ts | 14 + src/models/response/globalDomainResponse.ts | 14 + src/models/response/identityTokenResponse.ts | 24 + src/models/response/keysResponse.ts | 12 + src/models/response/listResponse.ts | 10 + .../response/profileOrganizationResponse.ts | 30 + src/models/response/profileResponse.ts | 39 + src/models/response/syncResponse.ts | 44 + src/services/abstractions/crypto.service.ts | 28 + src/services/abstractions/utils.service.ts | 22 + src/services/api.service.ts | 454 +++++++++ src/services/appId.service.ts | 31 + src/services/autofill.service.ts | 872 ++++++++++++++++++ src/services/cipher.service.ts | 514 +++++++++++ src/services/collection.service.ts | 124 +++ src/services/constants.service.ts | 116 +++ src/services/crypto.service.ts | 597 ++++++++++++ src/services/environment.service.ts | 87 ++ src/services/folder.service.ts | 161 ++++ src/services/i18n.service.ts | 28 + src/services/lock.service.ts | 89 ++ src/services/passwordGeneration.service.ts | 237 +++++ src/services/settings.service.ts | 61 ++ src/services/sync.service.ts | 180 ++++ src/services/token.service.ts | 170 ++++ src/services/totp.service.ts | 113 +++ src/services/user.service.ts | 79 ++ src/services/utils.service.spec.ts | 113 +++ src/services/utils.service.ts | 407 ++++++++ 77 files changed, 6179 insertions(+) create mode 100644 src/enums/browserType.enum.ts create mode 100644 src/enums/cipherType.enum.ts create mode 100644 src/enums/encryptionType.enum.ts create mode 100644 src/enums/fieldType.enum.ts create mode 100644 src/enums/secureNoteType.enum.ts create mode 100644 src/models/data/attachmentData.ts create mode 100644 src/models/data/cardData.ts create mode 100644 src/models/data/cipherData.ts create mode 100644 src/models/data/collectionData.ts create mode 100644 src/models/data/fieldData.ts create mode 100644 src/models/data/folderData.ts create mode 100644 src/models/data/identityData.ts create mode 100644 src/models/data/loginData.ts create mode 100644 src/models/data/secureNoteData.ts create mode 100644 src/models/domain/attachment.ts create mode 100644 src/models/domain/autofillField.ts create mode 100644 src/models/domain/autofillForm.ts create mode 100644 src/models/domain/autofillPageDetails.ts create mode 100644 src/models/domain/autofillScript.ts create mode 100644 src/models/domain/card.ts create mode 100644 src/models/domain/cipher.ts create mode 100644 src/models/domain/cipherString.ts create mode 100644 src/models/domain/collection.ts create mode 100644 src/models/domain/domain.ts create mode 100644 src/models/domain/encryptedObject.ts create mode 100644 src/models/domain/environmentUrls.ts create mode 100644 src/models/domain/field.ts create mode 100644 src/models/domain/folder.ts create mode 100644 src/models/domain/identity.ts create mode 100644 src/models/domain/login.ts create mode 100644 src/models/domain/passwordHistory.ts create mode 100644 src/models/domain/secureNote.ts create mode 100644 src/models/domain/symmetricCryptoKey.ts create mode 100644 src/models/domain/symmetricCryptoKeyBuffers.ts create mode 100644 src/models/request/cipherRequest.ts create mode 100644 src/models/request/deviceRequest.ts create mode 100644 src/models/request/deviceTokenRequest.ts create mode 100644 src/models/request/folderRequest.ts create mode 100644 src/models/request/passwordHintRequest.ts create mode 100644 src/models/request/registerRequest.ts create mode 100644 src/models/request/tokenRequest.ts create mode 100644 src/models/request/twoFactorEmailRequest.ts create mode 100644 src/models/response/attachmentResponse.ts create mode 100644 src/models/response/cipherResponse.ts create mode 100644 src/models/response/collectionResponse.ts create mode 100644 src/models/response/deviceResponse.ts create mode 100644 src/models/response/domainsResponse.ts create mode 100644 src/models/response/errorResponse.ts create mode 100644 src/models/response/folderResponse.ts create mode 100644 src/models/response/globalDomainResponse.ts create mode 100644 src/models/response/identityTokenResponse.ts create mode 100644 src/models/response/keysResponse.ts create mode 100644 src/models/response/listResponse.ts create mode 100644 src/models/response/profileOrganizationResponse.ts create mode 100644 src/models/response/profileResponse.ts create mode 100644 src/models/response/syncResponse.ts create mode 100644 src/services/abstractions/crypto.service.ts create mode 100644 src/services/abstractions/utils.service.ts create mode 100644 src/services/api.service.ts create mode 100644 src/services/appId.service.ts create mode 100644 src/services/autofill.service.ts create mode 100644 src/services/cipher.service.ts create mode 100644 src/services/collection.service.ts create mode 100644 src/services/constants.service.ts create mode 100644 src/services/crypto.service.ts create mode 100644 src/services/environment.service.ts create mode 100644 src/services/folder.service.ts create mode 100644 src/services/i18n.service.ts create mode 100644 src/services/lock.service.ts create mode 100644 src/services/passwordGeneration.service.ts create mode 100644 src/services/settings.service.ts create mode 100644 src/services/sync.service.ts create mode 100644 src/services/token.service.ts create mode 100644 src/services/totp.service.ts create mode 100644 src/services/user.service.ts create mode 100644 src/services/utils.service.spec.ts create mode 100644 src/services/utils.service.ts diff --git a/src/enums/browserType.enum.ts b/src/enums/browserType.enum.ts new file mode 100644 index 0000000000..64da6fb16b --- /dev/null +++ b/src/enums/browserType.enum.ts @@ -0,0 +1,8 @@ +export enum BrowserType { + Chrome = 2, + Firefox = 3, + Opera = 4, + Edge = 5, + Vivaldi = 19, + Safari = 20, +} diff --git a/src/enums/cipherType.enum.ts b/src/enums/cipherType.enum.ts new file mode 100644 index 0000000000..c081fb2d8c --- /dev/null +++ b/src/enums/cipherType.enum.ts @@ -0,0 +1,6 @@ +export enum CipherType { + Login = 1, + SecureNote = 2, + Card = 3, + Identity = 4, +} diff --git a/src/enums/encryptionType.enum.ts b/src/enums/encryptionType.enum.ts new file mode 100644 index 0000000000..7a0caa6606 --- /dev/null +++ b/src/enums/encryptionType.enum.ts @@ -0,0 +1,9 @@ +export enum EncryptionType { + 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, +} diff --git a/src/enums/fieldType.enum.ts b/src/enums/fieldType.enum.ts new file mode 100644 index 0000000000..c28b26c1da --- /dev/null +++ b/src/enums/fieldType.enum.ts @@ -0,0 +1,5 @@ +export enum FieldType { + Text = 0, + Hidden = 1, + Boolean = 2, +} diff --git a/src/enums/secureNoteType.enum.ts b/src/enums/secureNoteType.enum.ts new file mode 100644 index 0000000000..c7f3e44a78 --- /dev/null +++ b/src/enums/secureNoteType.enum.ts @@ -0,0 +1,3 @@ +export enum SecureNoteType { + Generic = 0, +} diff --git a/src/models/data/attachmentData.ts b/src/models/data/attachmentData.ts new file mode 100644 index 0000000000..2ff3ab423c --- /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 0000000000..f0b9f63f2e --- /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 0000000000..413612122f --- /dev/null +++ b/src/models/data/cipherData.ts @@ -0,0 +1,87 @@ +import { CipherType } from '../../enums/cipherType.enum'; + +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 0000000000..f2d5fc9f03 --- /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 0000000000..4914bb6ef9 --- /dev/null +++ b/src/models/data/fieldData.ts @@ -0,0 +1,16 @@ +import { FieldType } from '../../enums/fieldType.enum'; + +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 0000000000..6f03781cc7 --- /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 0000000000..ee849166ee --- /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/loginData.ts b/src/models/data/loginData.ts new file mode 100644 index 0000000000..de0aecc133 --- /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 0000000000..ccfc9bd614 --- /dev/null +++ b/src/models/data/secureNoteData.ts @@ -0,0 +1,12 @@ +import { SecureNoteType } from '../../enums/secureNoteType.enum'; + +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 0000000000..d77152b007 --- /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/autofillField.ts b/src/models/domain/autofillField.ts new file mode 100644 index 0000000000..dfa6cdc770 --- /dev/null +++ b/src/models/domain/autofillField.ts @@ -0,0 +1,22 @@ +export default class AutofillField { + opid: string; + elementNumber: number; + visible: boolean; + viewable: boolean; + htmlID: string; + htmlName: string; + htmlClass: string; + 'label-left': string; + 'label-right': string; + 'label-top': string; + 'label-tag': string; + placeholder: string; + type: string; + value: string; + disabled: boolean; + readonly: boolean; + onePasswordFieldType: string; + form: string; + autoCompleteType: string; + selectInfo: any; +} diff --git a/src/models/domain/autofillForm.ts b/src/models/domain/autofillForm.ts new file mode 100644 index 0000000000..2d7fc4800b --- /dev/null +++ b/src/models/domain/autofillForm.ts @@ -0,0 +1,7 @@ +export default class AutofillForm { + opid: string; + htmlName: string; + htmlID: string; + htmlAction: string; + htmlMethod: string; +} diff --git a/src/models/domain/autofillPageDetails.ts b/src/models/domain/autofillPageDetails.ts new file mode 100644 index 0000000000..70f922bbe6 --- /dev/null +++ b/src/models/domain/autofillPageDetails.ts @@ -0,0 +1,13 @@ +import AutofillField from './autofillField'; +import AutofillForm from './autofillForm'; + +export default class AutofillPageDetails { + documentUUID: string; + title: string; + url: string; + documentUrl: string; + tabUrl: string; + forms: { [id: string]: AutofillForm; }; + fields: AutofillField[]; + collectedTimestamp: number; +} diff --git a/src/models/domain/autofillScript.ts b/src/models/domain/autofillScript.ts new file mode 100644 index 0000000000..875e620ea8 --- /dev/null +++ b/src/models/domain/autofillScript.ts @@ -0,0 +1,12 @@ +export default class AutofillScript { + script: string[][] = []; + documentUUID: any = {}; + properties: any = {}; + options: any = {}; + metadata: any = {}; + autosubmit: any = null; + + constructor(documentUUID: string) { + this.documentUUID = documentUUID; + } +} diff --git a/src/models/domain/card.ts b/src/models/domain/card.ts new file mode 100644 index 0000000000..3e42f7affb --- /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 0000000000..5d51c82828 --- /dev/null +++ b/src/models/domain/cipher.ts @@ -0,0 +1,192 @@ +import { CipherType } from '../../enums/cipherType.enum'; + +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'; + +import { UtilsService } from '../../services/abstractions/utils.service'; + +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[]; + + private utilsService: UtilsService; + + 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) { + if (this.utilsService == null) { + this.utilsService = chrome.extension.getBackgroundPage() + .bitwardenMain.utilsService as UtilsService; + } + + model.login.domain = this.utilsService.getDomain(model.login.uri); + } + 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 0000000000..61aec8ca6f --- /dev/null +++ b/src/models/domain/cipherString.ts @@ -0,0 +1,115 @@ +import { EncryptionType } from '../../enums/encryptionType.enum'; +import { CryptoService } from '../../services/abstractions/crypto.service'; + +class CipherString { + encryptedString?: string; + encryptionType?: EncryptionType; + decryptedValue?: string; + cipherText?: string; + initializationVector?: string; + mac?: string; + + private cryptoService: CryptoService; + + 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) { + if (this.decryptedValue) { + return Promise.resolve(this.decryptedValue); + } + + const self = this; + if (this.cryptoService == null) { + this.cryptoService = chrome.extension.getBackgroundPage() + .bitwardenMain.cryptoService as CryptoService; + } + + return this.cryptoService.getOrgKey(orgId).then((orgKey: any) => { + return self.cryptoService.decrypt(self, orgKey); + }).then((decValue: any) => { + self.decryptedValue = decValue; + return self.decryptedValue; + }).catch(() => { + self.decryptedValue = '[error: cannot decrypt]'; + return self.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 0000000000..0a5079e53a --- /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 0000000000..cdb220776b --- /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 0000000000..668c30e262 --- /dev/null +++ b/src/models/domain/encryptedObject.ts @@ -0,0 +1,8 @@ +import SymmetricCryptoKey from './symmetricCryptoKey'; + +export default 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 0000000000..0eec60a114 --- /dev/null +++ b/src/models/domain/environmentUrls.ts @@ -0,0 +1,5 @@ +export default 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 0000000000..decc78664f --- /dev/null +++ b/src/models/domain/field.ts @@ -0,0 +1,39 @@ +import { FieldType } from '../../enums/fieldType.enum'; + +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 0000000000..180cd44e4f --- /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 0000000000..ede933ac30 --- /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/login.ts b/src/models/domain/login.ts new file mode 100644 index 0000000000..8ed1e1f7c7 --- /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 0000000000..fc4eb56688 --- /dev/null +++ b/src/models/domain/passwordHistory.ts @@ -0,0 +1,9 @@ +export default 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 0000000000..2c742f8ea8 --- /dev/null +++ b/src/models/domain/secureNote.ts @@ -0,0 +1,27 @@ +import { SecureNoteType } from '../../enums/secureNoteType.enum'; + +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 0000000000..bf50795a02 --- /dev/null +++ b/src/models/domain/symmetricCryptoKey.ts @@ -0,0 +1,80 @@ +import * as forge from 'node-forge'; + +import { EncryptionType } from '../../enums/encryptionType.enum'; + +import SymmetricCryptoKeyBuffers from './symmetricCryptoKeyBuffers'; + +import UtilsService from '../../services/utils.service'; + +export default 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 0000000000..5a378ad246 --- /dev/null +++ b/src/models/domain/symmetricCryptoKeyBuffers.ts @@ -0,0 +1,9 @@ +export default 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 0000000000..659ee5be2d --- /dev/null +++ b/src/models/request/cipherRequest.ts @@ -0,0 +1,89 @@ +import { CipherType } from '../../enums/cipherType.enum'; + +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 0000000000..928a46557f --- /dev/null +++ b/src/models/request/deviceRequest.ts @@ -0,0 +1,19 @@ +import { BrowserType } from '../../enums/browserType.enum'; +import { UtilsService } from '../../services/abstractions/utils.service'; + +class DeviceRequest { + type: BrowserType; + name: string; + identifier: string; + pushToken?: string; + + constructor(appId: string, utilsService: UtilsService) { + this.type = utilsService.getBrowser(); + this.name = utilsService.getBrowserString(); + 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 0000000000..69ef20bb9a --- /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 0000000000..7ff9794b18 --- /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/passwordHintRequest.ts b/src/models/request/passwordHintRequest.ts new file mode 100644 index 0000000000..4feb92944b --- /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 0000000000..4b80fb7884 --- /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 0000000000..4cf7564615 --- /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 0000000000..d540b08ecf --- /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 0000000000..df5138650c --- /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 0000000000..b2e597e23a --- /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 0000000000..8b0247720d --- /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 0000000000..63f69fe3b3 --- /dev/null +++ b/src/models/response/deviceResponse.ts @@ -0,0 +1,20 @@ +import { BrowserType } from '../../enums/browserType.enum'; + +class DeviceResponse { + id: string; + name: number; + identifier: string; + type: BrowserType; + 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 0000000000..d65077147b --- /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 0000000000..4d976932cd --- /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 0000000000..c5ff0ada70 --- /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 0000000000..8e9a45df11 --- /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 0000000000..2d188707c0 --- /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/keysResponse.ts b/src/models/response/keysResponse.ts new file mode 100644 index 0000000000..cb96dad51e --- /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 0000000000..9cf5455ada --- /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 0000000000..484857745a --- /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 0000000000..69e4f3e9cb --- /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 0000000000..8acdc5202d --- /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/abstractions/crypto.service.ts b/src/services/abstractions/crypto.service.ts new file mode 100644 index 0000000000..1fd413d34f --- /dev/null +++ b/src/services/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/services/abstractions/utils.service.ts b/src/services/abstractions/utils.service.ts new file mode 100644 index 0000000000..e52eb44fec --- /dev/null +++ b/src/services/abstractions/utils.service.ts @@ -0,0 +1,22 @@ +import { BrowserType } from '../../enums/browserType.enum'; + +export interface UtilsService { + getBrowser(): BrowserType; + getBrowserString(): string; + isFirefox(): boolean; + isChrome(): boolean; + isEdge(): boolean; + isOpera(): boolean; + analyticsId(): string; + initListSectionItemListeners(doc: Document, angular: any): void; + copyToClipboard(text: string, doc?: Document): void; + getDomain(uriString: string): string; + getHostname(uriString: string): string; + inSidebar(theWindow: Window): boolean; + inTab(theWindow: Window): boolean; + inPopout(theWindow: Window): boolean; + inPopup(theWindow: Window): boolean; + saveObjToStorage(key: string, obj: any): Promise; + removeFromStorage(key: string): Promise; + getObjFromStorage(key: string): Promise; +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts new file mode 100644 index 0000000000..d97ec6c47a --- /dev/null +++ b/src/services/api.service.ts @@ -0,0 +1,454 @@ +import AppIdService from './appId.service'; +import ConstantsService from './constants.service'; +import TokenService from './token.service'; +import UtilsService from './utils.service'; + +import EnvironmentUrls from '../models/domain/environmentUrls'; + +import { CipherRequest } from '../models/request/cipherRequest'; +import { DeviceRequest } from '../models/request/deviceRequest'; +import { DeviceTokenRequest } from '../models/request/deviceTokenRequest'; +import { FolderRequest } from '../models/request/folderRequest'; +import { PasswordHintRequest } from '../models/request/passwordHintRequest'; +import { RegisterRequest } from '../models/request/registerRequest'; +import { TokenRequest } from '../models/request/tokenRequest'; +import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; + +import { AttachmentResponse } from '../models/response/attachmentResponse'; +import { CipherResponse } from '../models/response/cipherResponse'; +import { DeviceResponse } from '../models/response/deviceResponse'; +import { DomainsResponse } from '../models/response/domainsResponse'; +import { ErrorResponse } from '../models/response/errorResponse'; +import { FolderResponse } from '../models/response/folderResponse'; +import { GlobalDomainResponse } from '../models/response/globalDomainResponse'; +import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; +import { KeysResponse } from '../models/response/keysResponse'; +import { ListResponse } from '../models/response/listResponse'; +import { ProfileOrganizationResponse } from '../models/response/profileOrganizationResponse'; +import { ProfileResponse } from '../models/response/profileResponse'; +import { SyncResponse } from '../models/response/syncResponse'; + +export default class ApiService { + urlsSet: boolean = false; + baseUrl: string; + identityBaseUrl: string; + logoutCallback: Function; + + constructor(private tokenService: TokenService, private utilsService: UtilsService, + logoutCallback: Function) { + this.logoutCallback = logoutCallback; + } + + setUrls(urls: EnvironmentUrls) { + this.urlsSet = true; + + if (urls.base != null) { + this.baseUrl = urls.base + '/api'; + this.identityBaseUrl = urls.base + '/identity'; + return; + } + + if (urls.api != null && urls.identity != null) { + this.baseUrl = urls.api; + this.identityBaseUrl = urls.identity; + return; + } + + /* tslint:disable */ + // Desktop + //this.baseUrl = 'http://localhost:4000'; + //this.identityBaseUrl = 'http://localhost:33656'; + + // Desktop HTTPS + //this.baseUrl = 'https://localhost:44377'; + //this.identityBaseUrl = 'https://localhost:44392'; + + // Desktop external + //this.baseUrl = 'http://192.168.1.3:4000'; + //this.identityBaseUrl = 'http://192.168.1.3:33656'; + + // Preview + //this.baseUrl = 'https://preview-api.bitwarden.com'; + //this.identityBaseUrl = 'https://preview-identity.bitwarden.com'; + + // Production + this.baseUrl = 'https://api.bitwarden.com'; + this.identityBaseUrl = 'https://identity.bitwarden.com'; + /* tslint:enable */ + } + + // Auth APIs + + async postIdentityToken(request: TokenRequest): Promise { + const response = await fetch(new Request(this.identityBaseUrl + '/connect/token', { + body: this.qsStringify(request.toIdentityToken()), + cache: 'no-cache', + headers: new Headers({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', + 'Accept': 'application/json', + 'Device-Type': this.utilsService.getBrowser().toString(), + }), + method: 'POST', + })); + + let responseJson: any = null; + const typeHeader = response.headers.get('content-type'); + if (typeHeader != null && typeHeader.indexOf('application/json') > -1) { + responseJson = await response.json(); + } + + if (responseJson != null) { + if (response.status === 200) { + return new IdentityTokenResponse(responseJson); + } else if (response.status === 400 && responseJson.TwoFactorProviders2 && + Object.keys(responseJson.TwoFactorProviders2).length) { + await this.tokenService.clearTwoFactorToken(request.email); + return responseJson.TwoFactorProviders2; + } + } + + return Promise.reject(new ErrorResponse(responseJson, response.status, true)); + } + + async refreshIdentityToken(): Promise { + try { + await this.doRefreshToken(); + } catch (e) { + return Promise.reject(null); + } + } + + // Two Factor APIs + + async postTwoFactorEmail(request: TwoFactorEmailRequest): Promise { + const response = await fetch(new Request(this.baseUrl + '/two-factor/send-email-login', { + body: JSON.stringify(request), + cache: 'no-cache', + headers: new Headers({ + 'Content-Type': 'application/json; charset=utf-8', + 'Device-Type': this.utilsService.getBrowser().toString(), + }), + method: 'POST', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + // Account APIs + + async getAccountRevisionDate(): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/accounts/revision-date', { + cache: 'no-cache', + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Device-Type': this.utilsService.getBrowser().toString(), + }), + })); + + if (response.status === 200) { + return (await response.json() as number); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async postPasswordHint(request: PasswordHintRequest): Promise { + const response = await fetch(new Request(this.baseUrl + '/accounts/password-hint', { + body: JSON.stringify(request), + cache: 'no-cache', + headers: new Headers({ + 'Content-Type': 'application/json; charset=utf-8', + 'Device-Type': this.utilsService.getBrowser().toString(), + }), + method: 'POST', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async postRegister(request: RegisterRequest): Promise { + const response = await fetch(new Request(this.baseUrl + '/accounts/register', { + body: JSON.stringify(request), + cache: 'no-cache', + headers: new Headers({ + 'Content-Type': 'application/json; charset=utf-8', + 'Device-Type': this.utilsService.getBrowser().toString(), + }), + method: 'POST', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + // Folder APIs + + async postFolder(request: FolderRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/folders', { + body: JSON.stringify(request), + cache: 'no-cache', + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Content-Type': 'application/json; charset=utf-8', + 'Device-Type': this.utilsService.getBrowser().toString(), + }), + method: 'POST', + })); + + if (response.status === 200) { + const responseJson = await response.json(); + return new FolderResponse(responseJson); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async putFolder(id: string, request: FolderRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/folders/' + id, { + body: JSON.stringify(request), + cache: 'no-cache', + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Content-Type': 'application/json; charset=utf-8', + 'Device-Type': this.utilsService.getBrowser().toString(), + }), + method: 'PUT', + })); + + if (response.status === 200) { + const responseJson = await response.json(); + return new FolderResponse(responseJson); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async deleteFolder(id: string): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/folders/' + id, { + cache: 'no-cache', + headers: new Headers({ + 'Authorization': authHeader, + 'Device-Type': this.utilsService.getBrowser().toString(), + }), + method: 'DELETE', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + // Cipher APIs + + async postCipher(request: CipherRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers', { + body: JSON.stringify(request), + cache: 'no-cache', + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Content-Type': 'application/json; charset=utf-8', + 'Device-Type': this.utilsService.getBrowser().toString(), + }), + method: 'POST', + })); + + if (response.status === 200) { + const responseJson = await response.json(); + return new CipherResponse(responseJson); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async putCipher(id: string, request: CipherRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { + body: JSON.stringify(request), + cache: 'no-cache', + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Content-Type': 'application/json; charset=utf-8', + 'Device-Type': this.utilsService.getBrowser().toString(), + }), + method: 'PUT', + })); + + if (response.status === 200) { + const responseJson = await response.json(); + return new CipherResponse(responseJson); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async deleteCipher(id: string): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { + cache: 'no-cache', + headers: new Headers({ + 'Authorization': authHeader, + 'Device-Type': this.utilsService.getBrowser().toString(), + }), + method: 'DELETE', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + // Attachments APIs + + async postCipherAttachment(id: string, data: FormData): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment', { + body: data, + cache: 'no-cache', + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Device-Type': this.utilsService.getBrowser().toString(), + }), + method: 'POST', + })); + + if (response.status === 200) { + const responseJson = await response.json(); + return new CipherResponse(responseJson); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async deleteCipherAttachment(id: string, attachmentId: string): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment/' + attachmentId, { + cache: 'no-cache', + headers: new Headers({ + 'Authorization': authHeader, + 'Device-Type': this.utilsService.getBrowser().toString(), + }), + method: 'DELETE', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + // Sync APIs + + async getSync(): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/sync', { + cache: 'no-cache', + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Device-Type': this.utilsService.getBrowser().toString(), + }), + })); + + if (response.status === 200) { + const responseJson = await response.json(); + return new SyncResponse(responseJson); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + // Helpers + + private async handleError(response: Response, tokenError: boolean): Promise { + if ((tokenError && response.status === 400) || response.status === 401 || response.status === 403) { + this.logoutCallback(true); + return null; + } + + let responseJson: any = null; + const typeHeader = response.headers.get('content-type'); + if (typeHeader != null && typeHeader.indexOf('application/json') > -1) { + responseJson = await response.json(); + } + + return new ErrorResponse(responseJson, response.status, tokenError); + } + + private async handleTokenState(): Promise { + let accessToken: string; + if (this.tokenService.tokenNeedsRefresh()) { + const tokenResponse = await this.doRefreshToken(); + accessToken = tokenResponse.accessToken; + } else { + accessToken = await this.tokenService.getToken(); + } + + return 'Bearer ' + accessToken; + } + + private async doRefreshToken(): Promise { + const refreshToken = await this.tokenService.getRefreshToken(); + if (refreshToken == null || refreshToken === '') { + throw new Error(); + } + + const response = await fetch(new Request(this.identityBaseUrl + '/connect/token', { + body: this.qsStringify({ + grant_type: 'refresh_token', + client_id: 'browser', + refresh_token: refreshToken, + }), + cache: 'no-cache', + headers: new Headers({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', + 'Accept': 'application/json', + 'Device-Type': this.utilsService.getBrowser().toString(), + }), + method: 'POST', + })); + + if (response.status === 200) { + const responseJson = await response.json(); + const tokenResponse = new IdentityTokenResponse(responseJson); + await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken); + return tokenResponse; + } else { + const error = await this.handleError(response, true); + return Promise.reject(error); + } + } + + private qsStringify(params: any): string { + return Object.keys(params).map((key) => { + return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); + }).join('&'); + } +} diff --git a/src/services/appId.service.ts b/src/services/appId.service.ts new file mode 100644 index 0000000000..d4e0dce5cb --- /dev/null +++ b/src/services/appId.service.ts @@ -0,0 +1,31 @@ +import UtilsService from './utils.service'; + +export default class AppIdService { + static getAppId(): Promise { + return AppIdService.makeAndGetAppId('appId'); + } + + static getAnonymousAppId(): Promise { + return AppIdService.makeAndGetAppId('anonymousAppId'); + } + + private static async makeAndGetAppId(key: string) { + const existingId = await UtilsService.getObjFromStorage(key); + if (existingId != null) { + return existingId; + } + + const guid = UtilsService.newGuid(); + await UtilsService.saveObjToStorage(key, guid); + return guid; + } + + // TODO: remove these in favor of static methods + getAppId(): Promise { + return AppIdService.getAppId(); + } + + getAnonymousAppId(): Promise { + return AppIdService.getAnonymousAppId(); + } +} diff --git a/src/services/autofill.service.ts b/src/services/autofill.service.ts new file mode 100644 index 0000000000..2b2593bffe --- /dev/null +++ b/src/services/autofill.service.ts @@ -0,0 +1,872 @@ +import { CipherType } from '../enums/cipherType.enum'; +import { FieldType } from '../enums/fieldType.enum'; + +import AutofillField from '../models/domain/autofillField'; +import AutofillPageDetails from '../models/domain/autofillPageDetails'; +import AutofillScript from '../models/domain/autofillScript'; + +import CipherService from './cipher.service'; +import TokenService from './token.service'; +import TotpService from './totp.service'; +import UtilsService from './utils.service'; + +const CardAttributes: string[] = ['autoCompleteType', 'data-stripe', 'htmlName', 'htmlID', 'label-tag', + 'placeholder', 'label-left', 'label-top']; + +const IdentityAttributes: string[] = ['autoCompleteType', 'data-stripe', 'htmlName', 'htmlID', 'label-tag', + 'placeholder', 'label-left', 'label-top']; + +const UsernameFieldNames: string[] = [ + // English + 'username', 'user name', 'email', 'email address', 'e-mail', 'e-mail address', 'userid', 'user id', + // German + 'benutzername', 'benutzer name', 'email adresse', 'e-mail adresse', 'benutzerid', 'benutzer id']; + +/* tslint:disable */ +const IsoCountries: { [id: string]: string; } = { + afghanistan: "AF", "aland islands": "AX", albania: "AL", algeria: "DZ", "american samoa": "AS", andorra: "AD", + angola: "AO", anguilla: "AI", antarctica: "AQ", "antigua and barbuda": "AG", argentina: "AR", armenia: "AM", + aruba: "AW", australia: "AU", austria: "AT", azerbaijan: "AZ", bahamas: "BS", bahrain: "BH", bangladesh: "BD", + barbados: "BB", belarus: "BY", belgium: "BE", belize: "BZ", benin: "BJ", bermuda: "BM", bhutan: "BT", bolivia: "BO", + "bosnia and herzegovina": "BA", botswana: "BW", "bouvet island": "BV", brazil: "BR", + "british indian ocean territory": "IO", "brunei darussalam": "BN", bulgaria: "BG", "burkina faso": "BF", burundi: "BI", + cambodia: "KH", cameroon: "CM", canada: "CA", "cape verde": "CV", "cayman islands": "KY", + "central african republic": "CF", chad: "TD", chile: "CL", china: "CN", "christmas island": "CX", + "cocos (keeling) islands": "CC", colombia: "CO", comoros: "KM", congo: "CG", "congo, democratic republic": "CD", + "cook islands": "CK", "costa rica": "CR", "cote d'ivoire": "CI", croatia: "HR", cuba: "CU", cyprus: "CY", + "czech republic": "CZ", denmark: "DK", djibouti: "DJ", dominica: "DM", "dominican republic": "DO", ecuador: "EC", + egypt: "EG", "el salvador": "SV", "equatorial guinea": "GQ", eritrea: "ER", estonia: "EE", ethiopia: "ET", + "falkland islands": "FK", "faroe islands": "FO", fiji: "FJ", finland: "FI", france: "FR", "french guiana": "GF", + "french polynesia": "PF", "french southern territories": "TF", gabon: "GA", gambia: "GM", georgia: "GE", germany: "DE", + ghana: "GH", gibraltar: "GI", greece: "GR", greenland: "GL", grenada: "GD", guadeloupe: "GP", guam: "GU", + guatemala: "GT", guernsey: "GG", guinea: "GN", "guinea-bissau": "GW", guyana: "GY", haiti: "HT", + "heard island & mcdonald islands": "HM", "holy see (vatican city state)": "VA", honduras: "HN", "hong kong": "HK", + hungary: "HU", iceland: "IS", india: "IN", indonesia: "ID", "iran, islamic republic of": "IR", iraq: "IQ", + ireland: "IE", "isle of man": "IM", israel: "IL", italy: "IT", jamaica: "JM", japan: "JP", jersey: "JE", + jordan: "JO", kazakhstan: "KZ", kenya: "KE", kiribati: "KI", "republic of korea": "KR", "south korea": "KR", + "democratic people's republic of korea": "KP", "north korea": "KP", kuwait: "KW", kyrgyzstan: "KG", + "lao people's democratic republic": "LA", latvia: "LV", lebanon: "LB", lesotho: "LS", liberia: "LR", + "libyan arab jamahiriya": "LY", liechtenstein: "LI", lithuania: "LT", luxembourg: "LU", macao: "MO", macedonia: "MK", + madagascar: "MG", malawi: "MW", malaysia: "MY", maldives: "MV", mali: "ML", malta: "MT", "marshall islands": "MH", + martinique: "MQ", mauritania: "MR", mauritius: "MU", mayotte: "YT", mexico: "MX", + "micronesia, federated states of": "FM", moldova: "MD", monaco: "MC", mongolia: "MN", montenegro: "ME", montserrat: "MS", + morocco: "MA", mozambique: "MZ", myanmar: "MM", namibia: "NA", nauru: "NR", nepal: "NP", netherlands: "NL", + "netherlands antilles": "AN", "new caledonia": "NC", "new zealand": "NZ", nicaragua: "NI", niger: "NE", nigeria: "NG", + niue: "NU", "norfolk island": "NF", "northern mariana islands": "MP", norway: "NO", oman: "OM", pakistan: "PK", + palau: "PW", "palestinian territory, occupied": "PS", panama: "PA", "papua new guinea": "PG", paraguay: "PY", peru: "PE", + philippines: "PH", pitcairn: "PN", poland: "PL", portugal: "PT", "puerto rico": "PR", qatar: "QA", reunion: "RE", + romania: "RO", "russian federation": "RU", rwanda: "RW", "saint barthelemy": "BL", "saint helena": "SH", + "saint kitts and nevis": "KN", "saint lucia": "LC", "saint martin": "MF", "saint pierre and miquelon": "PM", + "saint vincent and grenadines": "VC", samoa: "WS", "san marino": "SM", "sao tome and principe": "ST", + "saudi arabia": "SA", senegal: "SN", serbia: "RS", seychelles: "SC", "sierra leone": "SL", singapore: "SG", + slovakia: "SK", slovenia: "SI", "solomon islands": "SB", somalia: "SO", "south africa": "ZA", + "south georgia and sandwich isl.": "GS", spain: "ES", "sri lanka": "LK", sudan: "SD", suriname: "SR", + "svalbard and jan mayen": "SJ", swaziland: "SZ", sweden: "SE", switzerland: "CH", "syrian arab republic": "SY", + taiwan: "TW", tajikistan: "TJ", tanzania: "TZ", thailand: "TH", "timor-leste": "TL", togo: "TG", tokelau: "TK", + tonga: "TO", "trinidad and tobago": "TT", tunisia: "TN", turkey: "TR", turkmenistan: "TM", + "turks and caicos islands": "TC", tuvalu: "TV", uganda: "UG", ukraine: "UA", "united arab emirates": "AE", + "united kingdom": "GB", "united states": "US", "united states outlying islands": "UM", uruguay: "UY", + uzbekistan: "UZ", vanuatu: "VU", venezuela: "VE", vietnam: "VN", "virgin islands, british": "VG", + "virgin islands, u.s.": "VI", "wallis and futuna": "WF", "western sahara": "EH", yemen: "YE", zambia: "ZM", + zimbabwe: "ZW", +}; + +const IsoStates: { [id: string]: string; } = { + alabama: 'AL', alaska: 'AK', 'american samoa': 'AS', arizona: 'AZ', arkansas: 'AR', california: 'CA', colorado: 'CO', + connecticut: 'CT', delaware: 'DE', 'district of columbia': 'DC', 'federated states of micronesia': 'FM', florida: 'FL', + georgia: 'GA', guam: 'GU', hawaii: 'HI', idaho: 'ID', illinois: 'IL', indiana: 'IN', iowa: 'IA', kansas: 'KS', + kentucky: 'KY', louisiana: 'LA', maine: 'ME', 'marshall islands': 'MH', maryland: 'MD', massachusetts: 'MA', + michigan: 'MI', minnesota: 'MN', mississippi: 'MS', missouri: 'MO', montana: 'MT', nebraska: 'NE', nevada: 'NV', + 'new hampshire': 'NH', 'new jersey': 'NJ', 'new mexico': 'NM', 'new york': 'NY', 'north carolina': 'NC', + 'north dakota': 'ND', 'northern mariana islands': 'MP', ohio: 'OH', oklahoma: 'OK', oregon: 'OR', palau: 'PW', + pennsylvania: 'PA', 'puerto rico': 'PR', 'rhode island': 'RI', 'south carolina': 'SC', 'south dakota': 'SD', + tennessee: 'TN', texas: 'TX', utah: 'UT', vermont: 'VT', 'virgin islands': 'VI', virginia: 'VA', washington: 'WA', + 'west virginia': 'WV', wisconsin: 'WI', wyoming: 'WY', +}; + +var IsoProvinces: { [id: string]: string; } = { + alberta: 'AB', 'british columbia': 'BC', manitoba: 'MB', 'new brunswick': 'NB', 'newfoundland and labrador': 'NL', + 'nova scotia': 'NS', ontario: 'ON', 'prince edward island': 'PE', quebec: 'QC', saskatchewan: 'SK', +}; +/* tslint:enable */ + +export default class AutofillService { + constructor(public cipherService: CipherService, public tokenService: TokenService, + public totpService: TotpService, public utilsService: UtilsService) { + } + + getFormsWithPasswordFields(pageDetails: AutofillPageDetails): any[] { + const formData: any[] = []; + + const passwordFields = this.loadPasswordFields(pageDetails, true); + if (passwordFields.length === 0) { + return formData; + } + + for (const formKey in pageDetails.forms) { + if (!pageDetails.forms.hasOwnProperty(formKey)) { + continue; + } + + for (let i = 0; i < passwordFields.length; i++) { + const pf = passwordFields[i]; + if (formKey !== pf.form) { + continue; + } + + let uf = this.findUsernameField(pageDetails, pf, false, false); + if (uf == null) { + // not able to find any viewable username fields. maybe there are some "hidden" ones? + uf = this.findUsernameField(pageDetails, pf, true, false); + } + + formData.push({ + form: pageDetails.forms[formKey], + password: pf, + username: uf, + }); + break; + } + } + + return formData; + } + + async doAutoFill(options: any) { + let totpPromise: Promise = null; + const tab = await this.getActiveTab(); + if (!tab || !options.cipher || !options.pageDetails || !options.pageDetails.length) { + throw new Error('Nothing to auto-fill.'); + } + + let didAutofill = false; + options.pageDetails.forEach((pd: any) => { + // make sure we're still on correct tab + if (pd.tab.id !== tab.id || pd.tab.url !== tab.url) { + return; + } + + const fillScript = this.generateFillScript(pd.details, { + skipUsernameOnlyFill: options.skipUsernameOnlyFill || false, + cipher: options.cipher, + }); + + if (!fillScript || !fillScript.script || !fillScript.script.length) { + return; + } + + didAutofill = true; + if (!options.skipLastUsed) { + this.cipherService.updateLastUsedDate(options.cipher.id); + } + + chrome.tabs.sendMessage(tab.id, { + command: 'fillForm', + // tslint:disable-next-line + fillScript: fillScript, + }, { frameId: pd.frameId }); + + if (options.cipher.type !== CipherType.Login || totpPromise || + (options.fromBackground && this.utilsService.isFirefox()) || options.skipTotp || + !options.cipher.login.totp || !this.tokenService.getPremium()) { + return; + } + + totpPromise = this.totpService.isAutoCopyEnabled().then((enabled) => { + if (enabled) { + return this.totpService.getCode(options.cipher.login.totp); + } + + return null; + }).then((code: string) => { + if (code) { + UtilsService.copyToClipboard(code); + } + + return code; + }); + }); + + if (didAutofill) { + if (totpPromise != null) { + const totpCode = await totpPromise; + return totpCode; + } else { + return null; + } + } else { + throw new Error('Did not auto-fill.'); + } + } + + async doAutoFillForLastUsedLogin(pageDetails: any, fromCommand: boolean) { + const tab = await this.getActiveTab(); + if (!tab || !tab.url) { + return; + } + + const tabDomain = UtilsService.getDomain(tab.url); + if (tabDomain == null) { + return; + } + + const lastUsedCipher = await this.cipherService.getLastUsedForDomain(tabDomain); + if (!lastUsedCipher) { + return; + } + + await this.doAutoFill({ + cipher: lastUsedCipher, + // tslint:disable-next-line + pageDetails: pageDetails, + fromBackground: true, + skipTotp: !fromCommand, + skipLastUsed: true, + skipUsernameOnlyFill: !fromCommand, + }); + } + + // Helpers + + private getActiveTab(): Promise { + return new Promise((resolve, reject) => { + chrome.tabs.query({ active: true, currentWindow: true }, (tabs: any[]) => { + if (tabs.length === 0) { + reject('No tab found.'); + } else { + resolve(tabs[0]); + } + }); + }); + } + + private generateFillScript(pageDetails: AutofillPageDetails, options: any): AutofillScript { + if (!pageDetails || !options.cipher) { + return null; + } + + let fillScript = new AutofillScript(pageDetails.documentUUID); + const filledFields: { [id: string]: AutofillField; } = {}; + const fields = options.cipher.fields; + + if (fields && fields.length) { + const fieldNames: string[] = []; + + fields.forEach((f: any) => { + if (this.hasValue(f.name)) { + fieldNames.push(f.name.toLowerCase()); + } else { + fieldNames.push(null); + } + }); + + pageDetails.fields.forEach((field: any) => { + if (filledFields.hasOwnProperty(field.opid) || !field.viewable) { + return; + } + + const matchingIndex = this.findMatchingFieldIndex(field, fieldNames); + if (matchingIndex > -1) { + let val = fields[matchingIndex].value; + if (val == null && fields[matchingIndex].type === FieldType.Boolean) { + val = 'false'; + } + + filledFields[field.opid] = field; + fillScript.script.push(['click_on_opid', field.opid]); + fillScript.script.push(['fill_by_opid', field.opid, val]); + } + }); + } + + switch (options.cipher.type) { + case CipherType.Login: + fillScript = this.generateLoginFillScript(fillScript, pageDetails, filledFields, options); + break; + case CipherType.Card: + fillScript = this.generateCardFillScript(fillScript, pageDetails, filledFields, options); + break; + case CipherType.Identity: + fillScript = this.generateIdentityFillScript(fillScript, pageDetails, filledFields, options); + break; + default: + return null; + } + + return fillScript; + } + + private generateLoginFillScript(fillScript: AutofillScript, pageDetails: any, + filledFields: { [id: string]: AutofillField; }, options: any): AutofillScript { + if (!options.cipher.login) { + return null; + } + + const passwords: AutofillField[] = []; + const usernames: AutofillField[] = []; + let pf: AutofillField = null; + let username: AutofillField = null; + const login = options.cipher.login; + + if (!login.password || login.password === '') { + // No password for this login. Maybe they just wanted to auto-fill some custom fields? + fillScript = this.setFillScriptForFocus(filledFields, fillScript); + return fillScript; + } + + let passwordFields = this.loadPasswordFields(pageDetails, false); + if (!passwordFields.length) { + // not able to find any viewable password fields. maybe there are some "hidden" ones? + passwordFields = this.loadPasswordFields(pageDetails, true); + } + + for (const formKey in pageDetails.forms) { + if (!pageDetails.forms.hasOwnProperty(formKey)) { + continue; + } + + const passwordFieldsForForm: AutofillField[] = []; + passwordFields.forEach((passField) => { + if (formKey === passField.form) { + passwordFieldsForForm.push(passField); + } + }); + + passwordFields.forEach((passField) => { + pf = passField; + passwords.push(pf); + + if (login.username) { + username = this.findUsernameField(pageDetails, pf, false, false); + + if (!username) { + // not able to find any viewable username fields. maybe there are some "hidden" ones? + username = this.findUsernameField(pageDetails, pf, true, false); + } + + if (username) { + usernames.push(username); + } + } + }); + } + + if (passwordFields.length && !passwords.length) { + // The page does not have any forms with password fields. Use the first password field on the page and the + // input field just before it as the username. + + pf = passwordFields[0]; + passwords.push(pf); + + if (login.username && pf.elementNumber > 0) { + username = this.findUsernameField(pageDetails, pf, false, true); + + if (!username) { + // not able to find any viewable username fields. maybe there are some "hidden" ones? + username = this.findUsernameField(pageDetails, pf, true, true); + } + + if (username) { + usernames.push(username); + } + } + } + + if (!passwordFields.length && !options.skipUsernameOnlyFill) { + // No password fields on this page. Let's try to just fuzzy fill the username. + pageDetails.fields.forEach((f: any) => { + if (f.viewable && (f.type === 'text' || f.type === 'email' || f.type === 'tel') && + this.fieldIsFuzzyMatch(f, UsernameFieldNames)) { + usernames.push(f); + } + }); + } + + usernames.forEach((u) => { + if (filledFields.hasOwnProperty(u.opid)) { + return; + } + + filledFields[u.opid] = u; + fillScript.script.push(['click_on_opid', u.opid]); + fillScript.script.push(['fill_by_opid', u.opid, login.username]); + }); + + passwords.forEach((p) => { + if (filledFields.hasOwnProperty(p.opid)) { + return; + } + + filledFields[p.opid] = p; + fillScript.script.push(['click_on_opid', p.opid]); + fillScript.script.push(['fill_by_opid', p.opid, login.password]); + }); + + fillScript = this.setFillScriptForFocus(filledFields, fillScript); + return fillScript; + } + + private generateCardFillScript(fillScript: AutofillScript, pageDetails: any, + filledFields: { [id: string]: AutofillField; }, options: any): AutofillScript { + if (!options.cipher.card) { + return null; + } + + const fillFields: { [id: string]: AutofillField; } = {}; + + pageDetails.fields.forEach((f: any) => { + CardAttributes.forEach((attr) => { + if (!f.hasOwnProperty(attr) || !f[attr] || !f.viewable) { + return; + } + + // ref https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill + // ref https://developers.google.com/web/fundamentals/design-and-ux/input/forms/ + if (!fillFields.cardholderName && this.isFieldMatch(f[attr], + ['cc-name', 'card-name', 'cardholder-name', 'cardholder', 'name'], + ['cc-name', 'card-name', 'cardholder-name', 'cardholder'])) { + fillFields.cardholderName = f; + } else if (!fillFields.number && this.isFieldMatch(f[attr], + ['cc-number', 'cc-num', 'card-number', 'card-num', 'number'], + ['cc-number', 'cc-num', 'card-number', 'card-num'])) { + fillFields.number = f; + } else if (!fillFields.exp && this.isFieldMatch(f[attr], + ['cc-exp', 'card-exp', 'cc-expiration', 'card-expiration', 'cc-ex', 'card-ex'], + [])) { + fillFields.exp = f; + } else if (!fillFields.expMonth && this.isFieldMatch(f[attr], + ['exp-month', 'cc-exp-month', 'cc-month', 'card-month', 'cc-mo', 'card-mo', 'exp-mo', + 'card-exp-mo', 'cc-exp-mo', 'card-expiration-month', 'expiration-month', + 'cc-mm', 'card-mm', 'card-exp-mm', 'cc-exp-mm', 'exp-mm'])) { + fillFields.expMonth = f; + } else if (!fillFields.expYear && this.isFieldMatch(f[attr], + ['exp-year', 'cc-exp-year', 'cc-year', 'card-year', 'cc-yr', 'card-yr', 'exp-yr', + 'card-exp-yr', 'cc-exp-yr', 'card-expiration-year', 'expiration-year', + 'cc-yy', 'card-yy', 'card-exp-yy', 'cc-exp-yy', 'exp-yy', + 'cc-yyyy', 'card-yyyy', 'card-exp-yyyy', 'cc-exp-yyyy'])) { + fillFields.expYear = f; + } else if (!fillFields.code && this.isFieldMatch(f[attr], + ['cvv', 'cvc', 'cvv2', 'cc-csc', 'cc-cvv', 'card-csc', 'card-cvv', 'cvd', + 'cid', 'cvc2', 'cnv', 'cvn2', 'cc-code', 'card-code'])) { + fillFields.code = f; + } else if (!fillFields.brand && this.isFieldMatch(f[attr], + ['cc-type', 'card-type', 'card-brand', 'cc-brand'])) { + fillFields.brand = f; + } + }); + }); + + const card = options.cipher.card; + this.makeScriptAction(fillScript, card, fillFields, filledFields, 'cardholderName'); + this.makeScriptAction(fillScript, card, fillFields, filledFields, 'number'); + this.makeScriptAction(fillScript, card, fillFields, filledFields, 'expYear'); + this.makeScriptAction(fillScript, card, fillFields, filledFields, 'code'); + this.makeScriptAction(fillScript, card, fillFields, filledFields, 'brand'); + + if (fillFields.expMonth && this.hasValue(card.expMonth)) { + let expMonth = card.expMonth; + + if (fillFields.expMonth.selectInfo && fillFields.expMonth.selectInfo.options) { + let index: number = null; + if (fillFields.expMonth.selectInfo.options.length === 12) { + index = parseInt(card.expMonth, null) - 1; + } else if (fillFields.expMonth.selectInfo.options.length === 13) { + index = parseInt(card.expMonth, null); + } + + if (index != null) { + const option = fillFields.expMonth.selectInfo.options[index]; + if (option.length > 1) { + expMonth = option[1]; + } + } + } + + filledFields[fillFields.expMonth.opid] = fillFields.expMonth; + fillScript.script.push(['click_on_opid', fillFields.expMonth.opid]); + fillScript.script.push(['fill_by_opid', fillFields.expMonth.opid, expMonth]); + } + + if (fillFields.exp && this.hasValue(card.expMonth) && this.hasValue(card.expYear)) { + let year = card.expYear; + if (year.length === 2) { + year = '20' + year; + } + + const exp = year + '-' + ('0' + card.expMonth).slice(-2); + this.makeScriptActionWithValue(fillScript, exp, fillFields.exp, filledFields); + } + + return fillScript; + } + + private generateIdentityFillScript(fillScript: AutofillScript, pageDetails: any, + filledFields: { [id: string]: AutofillField; }, options: any): AutofillScript { + if (!options.cipher.identity) { + return null; + } + + const fillFields: { [id: string]: AutofillField; } = {}; + + pageDetails.fields.forEach((f: any) => { + IdentityAttributes.forEach((attr) => { + if (!f.hasOwnProperty(attr) || !f[attr] || !f.viewable) { + return; + } + + // ref https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill + // ref https://developers.google.com/web/fundamentals/design-and-ux/input/forms/ + if (!fillFields.name && this.isFieldMatch(f[attr], + ['name', 'full-name', 'your-name'], ['full-name', 'your-name'])) { + fillFields.name = f; + } else if (!fillFields.firstName && this.isFieldMatch(f[attr], + ['f-name', 'first-name', 'given-name', 'first-n'])) { + fillFields.firstName = f; + } else if (!fillFields.middleName && this.isFieldMatch(f[attr], + ['m-name', 'middle-name', 'additional-name', 'middle-initial', 'middle-n', 'middle-i'])) { + fillFields.middleName = f; + } else if (!fillFields.lastName && this.isFieldMatch(f[attr], + ['l-name', 'last-name', 's-name', 'surname', 'family-name', 'family-n', 'last-n'])) { + fillFields.lastName = f; + } else if (!fillFields.title && this.isFieldMatch(f[attr], + ['honorific-prefix', 'prefix', 'title'])) { + fillFields.title = f; + } else if (!fillFields.email && this.isFieldMatch(f[attr], + ['e-mail', 'email-address'])) { + fillFields.email = f; + } else if (!fillFields.address && this.isFieldMatch(f[attr], + ['address', 'street-address', 'addr'], [])) { + fillFields.address = f; + } else if (!fillFields.address1 && this.isFieldMatch(f[attr], + ['address-1', 'address-line-1', 'addr-1'])) { + fillFields.address1 = f; + } else if (!fillFields.address2 && this.isFieldMatch(f[attr], + ['address-2', 'address-line-2', 'addr-2'])) { + fillFields.address2 = f; + } else if (!fillFields.address3 && this.isFieldMatch(f[attr], + ['address-3', 'address-line-3', 'addr-3'])) { + fillFields.address3 = f; + } else if (!fillFields.postalCode && this.isFieldMatch(f[attr], + ['postal', 'zip', 'zip2', 'zip-code', 'postal-code', 'post-code', 'address-zip', + 'address-postal', 'address-code', 'address-postal-code', 'address-zip-code'])) { + fillFields.postalCode = f; + } else if (!fillFields.city && this.isFieldMatch(f[attr], + ['city', 'town', 'address-level-2', 'address-city', 'address-town'])) { + fillFields.city = f; + } else if (!fillFields.state && this.isFieldMatch(f[attr], + ['state', 'province', 'provence', 'address-level-1', 'address-state', + 'address-province'])) { + fillFields.state = f; + } else if (!fillFields.country && this.isFieldMatch(f[attr], + ['country', 'country-code', 'country-name', 'address-country', 'address-country-name', + 'address-country-code'])) { + fillFields.country = f; + } else if (!fillFields.phone && this.isFieldMatch(f[attr], + ['phone', 'mobile', 'mobile-phone', 'tel', 'telephone', 'phone-number'])) { + fillFields.phone = f; + } else if (!fillFields.username && this.isFieldMatch(f[attr], + ['user-name', 'user-id', 'screen-name'])) { + fillFields.username = f; + } else if (!fillFields.company && this.isFieldMatch(f[attr], + ['company', 'company-name', 'organization', 'organization-name'])) { + fillFields.company = f; + } + }); + }); + + const identity = options.cipher.identity; + this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'title'); + this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'firstName'); + this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'middleName'); + this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'lastName'); + this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'address1'); + this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'address2'); + this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'address3'); + this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'city'); + this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'postalCode'); + this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'company'); + this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'email'); + this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'phone'); + this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'username'); + + let filledState = false; + if (fillFields.state && identity.state && identity.state.length > 2) { + const stateLower = identity.state.toLowerCase(); + const isoState = IsoStates[stateLower] || IsoProvinces[stateLower]; + if (isoState) { + filledState = true; + this.makeScriptActionWithValue(fillScript, isoState, fillFields.state, filledFields); + } + } + + if (!filledState) { + this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'state'); + } + + let filledCountry = false; + if (fillFields.country && identity.country && identity.country.length > 2) { + const countryLower = identity.country.toLowerCase(); + const isoCountry = IsoCountries[countryLower]; + if (isoCountry) { + filledCountry = true; + this.makeScriptActionWithValue(fillScript, isoCountry, fillFields.country, filledFields); + } + } + + if (!filledCountry) { + this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'country'); + } + + if (fillFields.name && (identity.firstName || identity.lastName)) { + let fullName = ''; + if (this.hasValue(identity.firstName)) { + fullName = identity.firstName; + } + if (this.hasValue(identity.middleName)) { + if (fullName !== '') { + fullName += ' '; + } + fullName += identity.middleName; + } + if (this.hasValue(identity.lastName)) { + if (fullName !== '') { + fullName += ' '; + } + fullName += identity.lastName; + } + + this.makeScriptActionWithValue(fillScript, fullName, fillFields.name, filledFields); + } + + if (fillFields.address && this.hasValue(identity.address1)) { + let address = ''; + if (this.hasValue(identity.address1)) { + address = identity.address1; + } + if (this.hasValue(identity.address2)) { + if (address !== '') { + address += ', '; + } + address += identity.address2; + } + if (this.hasValue(identity.address3)) { + if (address !== '') { + address += ', '; + } + address += identity.address3; + } + + this.makeScriptActionWithValue(fillScript, address, fillFields.address, filledFields); + } + + return fillScript; + } + + private isFieldMatch(value: string, options: string[], containsOptions?: string[]): boolean { + value = value.trim().toLowerCase().replace(/[^a-zA-Z]+/g, ''); + for (let i = 0; i < options.length; i++) { + let option = options[i]; + const checkValueContains = containsOptions == null || containsOptions.indexOf(option) > -1; + option = option.replace(/-/g, ''); + if (value === option || (checkValueContains && value.indexOf(option) > -1)) { + return true; + } + } + + return false; + } + + private makeScriptAction(fillScript: AutofillScript, cipherData: any, fillFields: { [id: string]: AutofillField; }, + filledFields: { [id: string]: AutofillField; }, dataProp: string, fieldProp?: string) { + fieldProp = fieldProp || dataProp; + this.makeScriptActionWithValue(fillScript, cipherData[dataProp], fillFields[fieldProp], filledFields); + } + + private makeScriptActionWithValue(fillScript: AutofillScript, dataValue: any, field: AutofillField, + filledFields: { [id: string]: AutofillField; }) { + + let doFill = false; + if (this.hasValue(dataValue) && field) { + if (field.type === 'select-one' && field.selectInfo && field.selectInfo.options) { + for (let i = 0; i < field.selectInfo.options.length; i++) { + const option = field.selectInfo.options[i]; + for (let j = 0; j < option.length; j++) { + if (option[j].toLowerCase() === dataValue.toLowerCase()) { + doFill = true; + if (option.length > 1) { + dataValue = option[1]; + } + break; + } + } + + if (doFill) { + break; + } + } + } else { + doFill = true; + } + } + + if (doFill) { + filledFields[field.opid] = field; + fillScript.script.push(['click_on_opid', field.opid]); + fillScript.script.push(['fill_by_opid', field.opid, dataValue]); + } + } + + private loadPasswordFields(pageDetails: AutofillPageDetails, canBeHidden: boolean) { + const arr: AutofillField[] = []; + pageDetails.fields.forEach((f) => { + if (!f.disabled && !f.readonly && f.type === 'password' && (canBeHidden || f.viewable)) { + arr.push(f); + } + }); + + return arr; + } + + private findUsernameField(pageDetails: AutofillPageDetails, passwordField: AutofillField, canBeHidden: boolean, + withoutForm: boolean) { + let usernameField: AutofillField = null; + for (let i = 0; i < pageDetails.fields.length; i++) { + const f = pageDetails.fields[i]; + if (f.elementNumber >= passwordField.elementNumber) { + break; + } + + if (!f.disabled && !f.readonly && + (withoutForm || f.form === passwordField.form) && (canBeHidden || f.viewable) && + (f.type === 'text' || f.type === 'email' || f.type === 'tel')) { + usernameField = f; + + if (this.findMatchingFieldIndex(f, UsernameFieldNames) > -1) { + // We found an exact match. No need to keep looking. + break; + } + } + } + + return usernameField; + } + + private findMatchingFieldIndex(field: AutofillField, names: string[]): number { + for (let i = 0; i < names.length; i++) { + if (this.fieldPropertyIsMatch(field, 'htmlID', names[i])) { + return i; + } + if (this.fieldPropertyIsMatch(field, 'htmlName', names[i])) { + return i; + } + if (this.fieldPropertyIsMatch(field, 'label-tag', names[i])) { + return i; + } + if (this.fieldPropertyIsMatch(field, 'placeholder', names[i])) { + return i; + } + } + + return -1; + } + + private fieldPropertyIsMatch(field: any, property: string, name: string): boolean { + let fieldVal = field[property] as string; + if (!this.hasValue(fieldVal)) { + return false; + } + + fieldVal = fieldVal.trim().replace(/(?:\r\n|\r|\n)/g, ''); + if (name.startsWith('regex=')) { + try { + const regexParts = name.split('=', 2); + if (regexParts.length === 2) { + const regex = new RegExp(regexParts[1], 'i'); + return regex.test(fieldVal); + } + } catch (e) { } + } else if (name.startsWith('csv=')) { + const csvParts = name.split('=', 2); + if (csvParts.length === 2) { + const csvVals = csvParts[1].split(','); + for (let i = 0; i < csvVals.length; i++) { + const val = csvVals[i]; + if (val != null && val.trim().toLowerCase() === fieldVal.toLowerCase()) { + return true; + } + } + return false; + } + } + + return fieldVal.toLowerCase() === name; + } + + private fieldIsFuzzyMatch(field: AutofillField, names: string[]): boolean { + if (this.hasValue(field.htmlID) && this.fuzzyMatch(names, field.htmlID)) { + return true; + } + if (this.hasValue(field.htmlName) && this.fuzzyMatch(names, field.htmlName)) { + return true; + } + if (this.hasValue(field['label-tag']) && this.fuzzyMatch(names, field['label-tag'])) { + return true; + } + if (this.hasValue(field.placeholder) && this.fuzzyMatch(names, field.placeholder)) { + return true; + } + if (this.hasValue(field['label-left']) && this.fuzzyMatch(names, field['label-left'])) { + return true; + } + if (this.hasValue(field['label-top']) && this.fuzzyMatch(names, field['label-top'])) { + return true; + } + + return false; + } + + private fuzzyMatch(options: string[], value: string): boolean { + if (options == null || options.length === 0 || value == null || value === '') { + return false; + } + + value = value.replace(/(?:\r\n|\r|\n)/g, '').trim().toLowerCase(); + + for (let i = 0; i < options.length; i++) { + if (value.indexOf(options[i]) > -1) { + return true; + } + } + + return false; + } + + private hasValue(str: string): boolean { + return str && str !== ''; + } + + private setFillScriptForFocus(filledFields: { [id: string]: AutofillField; }, + fillScript: AutofillScript): AutofillScript { + let lastField: AutofillField = null; + let lastPasswordField: AutofillField = null; + + for (const opid in filledFields) { + if (filledFields.hasOwnProperty(opid) && filledFields[opid].viewable) { + lastField = filledFields[opid]; + + if (filledFields[opid].type === 'password') { + lastPasswordField = filledFields[opid]; + } + } + } + + // Prioritize password field over others. + if (lastPasswordField) { + fillScript.script.push(['focus_by_opid', lastPasswordField.opid]); + } else if (lastField) { + fillScript.script.push(['focus_by_opid', lastField.opid]); + } + + return fillScript; + } +} diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts new file mode 100644 index 0000000000..7b0d5d08b2 --- /dev/null +++ b/src/services/cipher.service.ts @@ -0,0 +1,514 @@ +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; + cipher.collectionIds = model.collectionIds; + + 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: any[] = []; + const ciphers = await this.getAll(); + ciphers.forEach((cipher) => { + promises.push(cipher.decrypt().then((c: any) => { + decCiphers.push(c); + })); + }); + + await Promise.all(promises); + this.decryptedCipherCache = decCiphers; + return this.decryptedCipherCache; + } + + async getAllDecryptedForGrouping(groupingId: string, folder: boolean = true): Promise { + const ciphers = await this.getAllDecrypted(); + const ciphersToReturn: any[] = []; + + ciphers.forEach((cipher) => { + if (folder && cipher.folderId === groupingId) { + ciphersToReturn.push(cipher); + } else if (!folder && cipher.collectionIds != null && cipher.collectionIds.indexOf(groupingId) > -1) { + 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[] = []; + eqDomains.forEach((eqDomain) => { + 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: any[] = []; + + ciphers.forEach((cipher) => { + 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) { + return null; + } + + 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 (let i = 0; i < this.decryptedCipherCache.length; i++) { + const cached = this.decryptedCipherCache[i]; + 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, cipher.collectionIds); + 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); + + let response: CipherResponse; + try { + response = await self.apiService.postCipherAttachment(cipher.id, fd); + } catch (e) { + reject((e as ErrorResponse).getSingleMessage()); + return; + } + + const userId = await self.userService.getUserId(); + const data = new CipherData(response, userId, cipher.collectionIds); + 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 { + (cipher as CipherData[]).forEach((c) => { + ciphers[c.id] = c; + }); + } + + await UtilsService.saveObjToStorage(Keys.ciphersPrefix + userId, ciphers); + this.decryptedCipherCache = null; + } + + async replace(ciphers: { [id: string]: 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 { + (id as string[]).forEach((i) => { + 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 { + try { + await this.apiService.deleteCipherAttachment(id, attachmentId); + } catch (e) { + return Promise.reject((e as ErrorResponse).getSingleMessage()); + } + await this.deleteAttachment(id, attachmentId); + } + + 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.'); + } + } +} diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts new file mode 100644 index 0000000000..14e23118be --- /dev/null +++ b/src/services/collection.service.ts @@ -0,0 +1,124 @@ +import { CipherString } from '../models/domain/cipherString'; +import { Collection } from '../models/domain/collection'; + +import { CollectionData } from '../models/data/collectionData'; + +import CryptoService from './crypto.service'; +import UserService from './user.service'; +import UtilsService from './utils.service'; + +const Keys = { + collectionsPrefix: 'collections_', +}; + +export default class CollectionService { + decryptedCollectionCache: any[]; + + constructor(private cryptoService: CryptoService, private userService: UserService) { + } + + clearCache(): void { + this.decryptedCollectionCache = null; + } + + async get(id: string): Promise { + const userId = await this.userService.getUserId(); + const collections = await UtilsService.getObjFromStorage<{ [id: string]: CollectionData; }>( + Keys.collectionsPrefix + userId); + if (collections == null || !collections.hasOwnProperty(id)) { + return null; + } + + return new Collection(collections[id]); + } + + async getAll(): Promise { + const userId = await this.userService.getUserId(); + const collections = await UtilsService.getObjFromStorage<{ [id: string]: CollectionData; }>( + Keys.collectionsPrefix + userId); + const response: Collection[] = []; + for (const id in collections) { + if (collections.hasOwnProperty(id)) { + response.push(new Collection(collections[id])); + } + } + return response; + } + + async getAllDecrypted(): Promise { + if (this.decryptedCollectionCache != null) { + return this.decryptedCollectionCache; + } + + const key = await this.cryptoService.getKey(); + if (key == null) { + throw new Error('No key.'); + } + + const decFolders: any[] = []; + const promises: Array> = []; + const folders = await this.getAll(); + folders.forEach((folder) => { + promises.push(folder.decrypt().then((f: any) => { + decFolders.push(f); + })); + }); + + await Promise.all(promises); + this.decryptedCollectionCache = decFolders; + return this.decryptedCollectionCache; + } + + async upsert(collection: CollectionData | CollectionData[]): Promise { + const userId = await this.userService.getUserId(); + let collections = await UtilsService.getObjFromStorage<{ [id: string]: CollectionData; }>( + Keys.collectionsPrefix + userId); + if (collections == null) { + collections = {}; + } + + if (collection instanceof CollectionData) { + const c = collection as CollectionData; + collections[c.id] = c; + } else { + (collection as CollectionData[]).forEach((c) => { + collections[c.id] = c; + }); + } + + await UtilsService.saveObjToStorage(Keys.collectionsPrefix + userId, collections); + this.decryptedCollectionCache = null; + } + + async replace(collections: { [id: string]: CollectionData; }): Promise { + const userId = await this.userService.getUserId(); + await UtilsService.saveObjToStorage(Keys.collectionsPrefix + userId, collections); + this.decryptedCollectionCache = null; + } + + async clear(userId: string): Promise { + await UtilsService.removeFromStorage(Keys.collectionsPrefix + userId); + this.decryptedCollectionCache = null; + } + + async delete(id: string | string[]): Promise { + const userId = await this.userService.getUserId(); + const collections = await UtilsService.getObjFromStorage<{ [id: string]: CollectionData; }>( + Keys.collectionsPrefix + userId); + if (collections == null) { + return; + } + + if (typeof id === 'string') { + const i = id as string; + delete collections[id]; + } else { + (id as string[]).forEach((i) => { + delete collections[i]; + }); + } + + await UtilsService.saveObjToStorage(Keys.collectionsPrefix + userId, collections); + this.decryptedCollectionCache = null; + } +} diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts new file mode 100644 index 0000000000..337c6174a4 --- /dev/null +++ b/src/services/constants.service.ts @@ -0,0 +1,116 @@ +import UtilsService from './utils.service'; + +export default 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, utilsService: UtilsService) { + if (utilsService.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 0000000000..16c3c7497b --- /dev/null +++ b/src/services/crypto.service.ts @@ -0,0 +1,597 @@ +import * as forge from 'node-forge'; + +import { EncryptionType } from '../enums/encryptionType.enum'; + +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 ConstantsService from './constants.service'; +import UtilsService from './utils.service'; + +import { CryptoService as CryptoServiceInterface } from './abstractions/crypto.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 default class CryptoService implements CryptoServiceInterface { + private key: SymmetricCryptoKey; + private encKey: SymmetricCryptoKey; + private legacyEtmKey: SymmetricCryptoKey; + private keyHash: string; + private privateKey: ArrayBuffer; + private orgKeys: Map; + + async setKey(key: SymmetricCryptoKey): Promise { + this.key = key; + + const option = await UtilsService.getObjFromStorage(ConstantsService.lockOptionKey); + if (option != null) { + // if we have a lock option set, we do not store the key + return; + } + + return UtilsService.saveObjToStorage(Keys.key, key.keyB64); + } + + setKeyHash(keyHash: string): Promise<{}> { + this.keyHash = keyHash; + return UtilsService.saveObjToStorage(Keys.keyHash, keyHash); + } + + async setEncKey(encKey: string): Promise<{}> { + if (encKey == null) { + return; + } + await UtilsService.saveObjToStorage(Keys.encKey, encKey); + this.encKey = null; + } + + async setEncPrivateKey(encPrivateKey: string): Promise<{}> { + if (encPrivateKey == null) { + return; + } + + await UtilsService.saveObjToStorage(Keys.encPrivateKey, encPrivateKey); + this.privateKey = null; + } + + setOrgKeys(orgs: ProfileOrganizationResponse[]): Promise<{}> { + const orgKeys: any = {}; + orgs.forEach((org) => { + orgKeys[org.id] = org.key; + }); + + return UtilsService.saveObjToStorage(Keys.encOrgKeys, orgKeys); + } + + async getKey(): Promise { + if (this.key != null) { + return this.key; + } + + const option = await UtilsService.getObjFromStorage(ConstantsService.lockOptionKey); + if (option != null) { + return null; + } + + const key = await UtilsService.getObjFromStorage(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 UtilsService.getObjFromStorage(Keys.keyHash); + } + + async getEncKey(): Promise { + if (this.encKey != null) { + return this.encKey; + } + + const encKey = await UtilsService.getObjFromStorage(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 UtilsService.getObjFromStorage(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 self = this; + const encOrgKeys = await UtilsService.getObjFromStorage(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 UtilsService.removeFromStorage(Keys.key); + } + + clearKeyHash(): Promise { + this.keyHash = null; + return UtilsService.removeFromStorage(Keys.keyHash); + } + + clearEncKey(memoryOnly?: boolean): Promise { + this.encKey = null; + if (memoryOnly) { + return Promise.resolve(); + } + return UtilsService.removeFromStorage(Keys.encKey); + } + + clearPrivateKey(memoryOnly?: boolean): Promise { + this.privateKey = null; + if (memoryOnly) { + return Promise.resolve(); + } + return UtilsService.removeFromStorage(Keys.encPrivateKey); + } + + clearOrgKeys(memoryOnly?: boolean): Promise { + this.orgKeys = null; + if (memoryOnly) { + return Promise.resolve(); + } + return UtilsService.removeFromStorage(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 UtilsService.getObjFromStorage(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/environment.service.ts b/src/services/environment.service.ts new file mode 100644 index 0000000000..098a67f749 --- /dev/null +++ b/src/services/environment.service.ts @@ -0,0 +1,87 @@ +import ApiService from './api.service'; +import ConstantsService from './constants.service'; +import UtilsService from './utils.service'; + +import EnvironmentUrls from '../models/domain/environmentUrls'; + +export default class EnvironmentService { + baseUrl: string; + webVaultUrl: string; + apiUrl: string; + identityUrl: string; + iconsUrl: string; + + constructor(private apiService: ApiService) { + } + + async setUrlsFromStorage(): Promise { + const urlsObj: any = await UtilsService.getObjFromStorage(ConstantsService.environmentUrlsKey); + const urls = urlsObj || { + base: null, + api: null, + identity: null, + icons: null, + webVault: null, + }; + + const envUrls = new EnvironmentUrls(); + + if (urls.base) { + this.baseUrl = envUrls.base = urls.base; + await this.apiService.setUrls(envUrls); + return; + } + + this.webVaultUrl = urls.webVault; + this.apiUrl = envUrls.api = urls.api; + this.identityUrl = envUrls.identity = urls.identity; + this.iconsUrl = urls.icons; + await this.apiService.setUrls(envUrls); + } + + async setUrls(urls: any): Promise { + urls.base = this.formatUrl(urls.base); + urls.webVault = this.formatUrl(urls.webVault); + urls.api = this.formatUrl(urls.api); + urls.identity = this.formatUrl(urls.identity); + urls.icons = this.formatUrl(urls.icons); + + await UtilsService.saveObjToStorage(ConstantsService.environmentUrlsKey, { + base: urls.base, + api: urls.api, + identity: urls.identity, + webVault: urls.webVault, + icons: urls.icons, + }); + + this.baseUrl = urls.base; + this.webVaultUrl = urls.webVault; + this.apiUrl = urls.api; + this.identityUrl = urls.identity; + this.iconsUrl = urls.icons; + + const envUrls = new EnvironmentUrls(); + if (this.baseUrl) { + envUrls.base = this.baseUrl; + } else { + envUrls.api = this.apiUrl; + envUrls.identity = this.identityUrl; + } + + await this.apiService.setUrls(envUrls); + return urls; + } + + private formatUrl(url: string): string { + if (url == null || url === '') { + return null; + } + + url = url.replace(/\/+$/g, ''); + if (!url.startsWith('http://') && !url.startsWith('https://')) { + url = 'https://' + url; + } + + return url; + } +} diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts new file mode 100644 index 0000000000..3337c1b0bc --- /dev/null +++ b/src/services/folder.service.ts @@ -0,0 +1,161 @@ +import { CipherString } from '../models/domain/cipherString'; +import { Folder } from '../models/domain/folder'; + +import { FolderData } from '../models/data/folderData'; + +import { FolderRequest } from '../models/request/folderRequest'; +import { FolderResponse } from '../models/response/folderResponse'; + +import ApiService from './api.service'; +import CryptoService from './crypto.service'; +import UserService from './user.service'; +import UtilsService from './utils.service'; + +const Keys = { + foldersPrefix: 'folders_', +}; + +export default class FolderService { + decryptedFolderCache: any[]; + + constructor(private cryptoService: CryptoService, private userService: UserService, + private i18nService: any, private apiService: ApiService) { + } + + clearCache(): void { + this.decryptedFolderCache = null; + } + + async encrypt(model: any): Promise { + const folder = new Folder(); + folder.id = model.id; + folder.name = await this.cryptoService.encrypt(model.name); + return folder; + } + + async get(id: string): Promise { + const userId = await this.userService.getUserId(); + const folders = await UtilsService.getObjFromStorage<{ [id: string]: FolderData; }>( + Keys.foldersPrefix + userId); + if (folders == null || !folders.hasOwnProperty(id)) { + return null; + } + + return new Folder(folders[id]); + } + + async getAll(): Promise { + const userId = await this.userService.getUserId(); + const folders = await UtilsService.getObjFromStorage<{ [id: string]: FolderData; }>( + Keys.foldersPrefix + userId); + const response: Folder[] = []; + for (const id in folders) { + if (folders.hasOwnProperty(id)) { + response.push(new Folder(folders[id])); + } + } + return response; + } + + async getAllDecrypted(): Promise { + if (this.decryptedFolderCache != null) { + return this.decryptedFolderCache; + } + + const decFolders: any[] = [{ + id: null, + name: this.i18nService.noneFolder, + }]; + + const key = await this.cryptoService.getKey(); + if (key == null) { + throw new Error('No key.'); + } + + const promises: Array> = []; + const folders = await this.getAll(); + folders.forEach((folder) => { + promises.push(folder.decrypt().then((f: any) => { + decFolders.push(f); + })); + }); + + await Promise.all(promises); + this.decryptedFolderCache = decFolders; + return this.decryptedFolderCache; + } + + async saveWithServer(folder: Folder): Promise { + const request = new FolderRequest(folder); + + let response: FolderResponse; + if (folder.id == null) { + response = await this.apiService.postFolder(request); + folder.id = response.id; + } else { + response = await this.apiService.putFolder(folder.id, request); + } + + const userId = await this.userService.getUserId(); + const data = new FolderData(response, userId); + await this.upsert(data); + } + + async upsert(folder: FolderData | FolderData[]): Promise { + const userId = await this.userService.getUserId(); + let folders = await UtilsService.getObjFromStorage<{ [id: string]: FolderData; }>( + Keys.foldersPrefix + userId); + if (folders == null) { + folders = {}; + } + + if (folder instanceof FolderData) { + const f = folder as FolderData; + folders[f.id] = f; + } else { + (folder as FolderData[]).forEach((f) => { + folders[f.id] = f; + }); + } + + await UtilsService.saveObjToStorage(Keys.foldersPrefix + userId, folders); + this.decryptedFolderCache = null; + } + + async replace(folders: { [id: string]: FolderData; }): Promise { + const userId = await this.userService.getUserId(); + await UtilsService.saveObjToStorage(Keys.foldersPrefix + userId, folders); + this.decryptedFolderCache = null; + } + + async clear(userId: string): Promise { + await UtilsService.removeFromStorage(Keys.foldersPrefix + userId); + this.decryptedFolderCache = null; + } + + async delete(id: string | string[]): Promise { + const userId = await this.userService.getUserId(); + const folders = await UtilsService.getObjFromStorage<{ [id: string]: FolderData; }>( + Keys.foldersPrefix + userId); + if (folders == null) { + return; + } + + if (typeof id === 'string') { + const i = id as string; + delete folders[id]; + } else { + (id as string[]).forEach((i) => { + delete folders[i]; + }); + } + + await UtilsService.saveObjToStorage(Keys.foldersPrefix + userId, folders); + this.decryptedFolderCache = null; + } + + async deleteWithServer(id: string): Promise { + await this.apiService.deleteFolder(id); + await this.delete(id); + } +} diff --git a/src/services/i18n.service.ts b/src/services/i18n.service.ts new file mode 100644 index 0000000000..a294015f3c --- /dev/null +++ b/src/services/i18n.service.ts @@ -0,0 +1,28 @@ +import UtilsService from '../services/utils.service'; + +export default function i18nService(utilsService: UtilsService) { + const edgeMessages: any = {}; + + if (utilsService.isEdge()) { + fetch('../_locales/en/messages.json').then((file) => { + return file.json(); + }).then((locales) => { + for (const prop in locales) { + if (locales.hasOwnProperty(prop)) { + edgeMessages[prop] = chrome.i18n.getMessage(prop); + } + } + }); + + return edgeMessages; + } + + return new Proxy({}, { + get: (target, name) => { + return chrome.i18n.getMessage(name); + }, + set: (target, name, value) => { + return false; + }, + }); +} diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts new file mode 100644 index 0000000000..bc58aacbab --- /dev/null +++ b/src/services/lock.service.ts @@ -0,0 +1,89 @@ +import CipherService from './cipher.service'; +import CollectionService from './collection.service'; +import ConstantsService from './constants.service'; +import CryptoService from './crypto.service'; +import FolderService from './folder.service'; +import UtilsService from './utils.service'; + +export default class LockService { + constructor(private cipherService: CipherService, private folderService: FolderService, + private collectionService: CollectionService, private cryptoService: CryptoService, + private utilsService: UtilsService, private setIcon: Function, private refreshBadgeAndMenu: Function) { + this.checkLock(); + setInterval(() => this.checkLock(), 10 * 1000); // check every 10 seconds + + const self = this; + if ((window as any).chrome.idle && (window as any).chrome.idle.onStateChanged) { + (window as any).chrome.idle.onStateChanged.addListener(async (newState: string) => { + if (newState === 'locked') { + const lockOption = await UtilsService.getObjFromStorage(ConstantsService.lockOptionKey); + if (lockOption === -2) { + self.lock(); + } + } + }); + } + } + + async checkLock(): Promise { + const popupOpen = chrome.extension.getViews({ type: 'popup' }).length > 0; + const tabOpen = chrome.extension.getViews({ type: 'tab' }).length > 0; + const sidebarView = this.sidebarViewName(); + const sidebarOpen = sidebarView != null && chrome.extension.getViews({ type: sidebarView }).length > 0; + + if (popupOpen || tabOpen || sidebarOpen) { + // Do not lock + return; + } + + const key = await this.cryptoService.getKey(); + if (key == null) { + // no key so no need to lock + return; + } + + const lockOption = await UtilsService.getObjFromStorage(ConstantsService.lockOptionKey); + if (lockOption == null || lockOption < 0) { + return; + } + + const lastActive = await UtilsService.getObjFromStorage(ConstantsService.lastActiveKey); + if (lastActive == null) { + return; + } + + const lockOptionSeconds = lockOption * 60; + const diffSeconds = ((new Date()).getTime() - lastActive) / 1000; + if (diffSeconds >= lockOptionSeconds) { + // need to lock now + await this.lock(); + } + } + + async lock(): Promise { + await Promise.all([ + this.cryptoService.clearKey(), + this.cryptoService.clearOrgKeys(true), + this.cryptoService.clearPrivateKey(true), + this.cryptoService.clearEncKey(true), + this.setIcon(), + this.refreshBadgeAndMenu(), + ]); + + this.folderService.clearCache(); + this.cipherService.clearCache(); + this.collectionService.clearCache(); + } + + // Helpers + + private sidebarViewName(): string { + if ((window as any).chrome.sidebarAction && this.utilsService.isFirefox()) { + return 'sidebar'; + } else if (this.utilsService.isOpera() && (typeof opr !== 'undefined') && opr.sidebarAction) { + return 'sidebar_panel'; + } + + return null; + } +} diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts new file mode 100644 index 0000000000..4e52438d46 --- /dev/null +++ b/src/services/passwordGeneration.service.ts @@ -0,0 +1,237 @@ +import { CipherString } from '../models/domain/cipherString'; +import PasswordHistory from '../models/domain/passwordHistory'; + +import CryptoService from './crypto.service'; +import UtilsService from './utils.service'; + +const DefaultOptions = { + length: 14, + ambiguous: false, + number: true, + minNumber: 1, + uppercase: true, + minUppercase: 1, + lowercase: true, + minLowercase: 1, + special: false, + minSpecial: 1, +}; + +const Keys = { + options: 'passwordGenerationOptions', + history: 'generatedPasswordHistory', +}; + +const MaxPasswordsInHistory = 100; + +export default class PasswordGenerationService { + static generatePassword(options: any): string { + // overload defaults with given options + const o = Object.assign({}, DefaultOptions, options); + + // sanitize + if (o.uppercase && o.minUppercase < 0) { + o.minUppercase = 1; + } + if (o.lowercase && o.minLowercase < 0) { + o.minLowercase = 1; + } + if (o.number && o.minNumber < 0) { + o.minNumber = 1; + } + if (o.special && o.minSpecial < 0) { + o.minSpecial = 1; + } + + if (!o.length || o.length < 1) { + o.length = 10; + } + + const minLength: number = o.minUppercase + o.minLowercase + o.minNumber + o.minSpecial; + if (o.length < minLength) { + o.length = minLength; + } + + const positions: string[] = []; + if (o.lowercase && o.minLowercase > 0) { + for (let i = 0; i < o.minLowercase; i++) { + positions.push('l'); + } + } + if (o.uppercase && o.minUppercase > 0) { + for (let i = 0; i < o.minUppercase; i++) { + positions.push('u'); + } + } + if (o.number && o.minNumber > 0) { + for (let i = 0; i < o.minNumber; i++) { + positions.push('n'); + } + } + if (o.special && o.minSpecial > 0) { + for (let i = 0; i < o.minSpecial; i++) { + positions.push('s'); + } + } + while (positions.length < o.length) { + positions.push('a'); + } + + // shuffle + positions.sort(() => { + return UtilsService.secureRandomNumber(0, 1) * 2 - 1; + }); + + // build out the char sets + let allCharSet = ''; + + let lowercaseCharSet = 'abcdefghijkmnopqrstuvwxyz'; + if (o.ambiguous) { + lowercaseCharSet += 'l'; + } + if (o.lowercase) { + allCharSet += lowercaseCharSet; + } + + let uppercaseCharSet = 'ABCDEFGHIJKLMNPQRSTUVWXYZ'; + if (o.ambiguous) { + uppercaseCharSet += 'O'; + } + if (o.uppercase) { + allCharSet += uppercaseCharSet; + } + + let numberCharSet = '23456789'; + if (o.ambiguous) { + numberCharSet += '01'; + } + if (o.number) { + allCharSet += numberCharSet; + } + + const specialCharSet = '!@#$%^&*'; + if (o.special) { + allCharSet += specialCharSet; + } + + let password = ''; + for (let i = 0; i < o.length; i++) { + let positionChars: string; + switch (positions[i]) { + case 'l': + positionChars = lowercaseCharSet; + break; + case 'u': + positionChars = uppercaseCharSet; + break; + case 'n': + positionChars = numberCharSet; + break; + case 's': + positionChars = specialCharSet; + break; + case 'a': + positionChars = allCharSet; + break; + } + + const randomCharIndex = UtilsService.secureRandomNumber(0, positionChars.length - 1); + password += positionChars.charAt(randomCharIndex); + } + + return password; + } + + optionsCache: any; + history: PasswordHistory[] = []; + + constructor(private cryptoService: CryptoService) { + UtilsService.getObjFromStorage(Keys.history).then((encrypted) => { + return this.decryptHistory(encrypted); + }).then((history) => { + this.history = history; + }); + } + + generatePassword(options: any) { + return PasswordGenerationService.generatePassword(options); + } + + async getOptions() { + if (this.optionsCache == null) { + const options = await UtilsService.getObjFromStorage(Keys.options); + if (options == null) { + this.optionsCache = DefaultOptions; + } else { + this.optionsCache = options; + } + } + + return this.optionsCache; + } + + async saveOptions(options: any) { + await UtilsService.saveObjToStorage(Keys.options, options); + this.optionsCache = options; + } + + getHistory() { + return this.history || new Array(); + } + + async addHistory(password: string): Promise { + // Prevent duplicates + if (this.matchesPrevious(password)) { + return; + } + + this.history.push(new PasswordHistory(password, Date.now())); + + // Remove old items. + if (this.history.length > MaxPasswordsInHistory) { + this.history.shift(); + } + + const newHistory = await this.encryptHistory(); + return await UtilsService.saveObjToStorage(Keys.history, newHistory); + } + + async clear(): Promise { + this.history = []; + return await UtilsService.removeFromStorage(Keys.history); + } + + private async encryptHistory(): Promise { + if (this.history == null || this.history.length === 0) { + return Promise.resolve([]); + } + + const promises = this.history.map(async (item) => { + const encrypted = await this.cryptoService.encrypt(item.password); + return new PasswordHistory(encrypted.encryptedString, item.date); + }); + + return await Promise.all(promises); + } + + private async decryptHistory(history: PasswordHistory[]): Promise { + if (history == null || history.length === 0) { + return Promise.resolve([]); + } + + const promises = history.map(async (item) => { + const decrypted = await this.cryptoService.decrypt(new CipherString(item.password)); + return new PasswordHistory(decrypted, item.date); + }); + + return await Promise.all(promises); + } + + private matchesPrevious(password: string): boolean { + if (this.history == null || this.history.length === 0) { + return false; + } + + return this.history[this.history.length - 1].password === password; + } +} diff --git a/src/services/settings.service.ts b/src/services/settings.service.ts new file mode 100644 index 0000000000..25d598f83c --- /dev/null +++ b/src/services/settings.service.ts @@ -0,0 +1,61 @@ +import UserService from './user.service'; +import UtilsService from './utils.service'; + +const Keys = { + settingsPrefix: 'settings_', + equivalentDomains: 'equivalentDomains', +}; + +export default class SettingsService { + private settingsCache: any; + + constructor(private userService: UserService) { + } + + clearCache(): void { + this.settingsCache = null; + } + + getEquivalentDomains(): Promise { + return this.getSettingsKey(Keys.equivalentDomains); + } + + async setEquivalentDomains(equivalentDomains: string[][]) { + await this.setSettingsKey(Keys.equivalentDomains, equivalentDomains); + } + + async clear(userId: string): Promise { + await UtilsService.removeFromStorage(Keys.settingsPrefix + userId); + this.settingsCache = null; + } + + // Helpers + + private async getSettings(): Promise { + if (this.settingsCache == null) { + const userId = await this.userService.getUserId(); + this.settingsCache = UtilsService.getObjFromStorage(Keys.settingsPrefix + userId); + } + return this.settingsCache; + } + + private async getSettingsKey(key: string): Promise { + const settings = await this.getSettings(); + if (settings != null && settings[key]) { + return settings[key]; + } + return null; + } + + private async setSettingsKey(key: string, value: any): Promise { + const userId = await this.userService.getUserId(); + let settings = await this.getSettings(); + if (!settings) { + settings = {}; + } + + settings[key] = value; + await UtilsService.saveObjToStorage(Keys.settingsPrefix + userId, settings); + this.settingsCache = settings; + } +} diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts new file mode 100644 index 0000000000..a28c401f19 --- /dev/null +++ b/src/services/sync.service.ts @@ -0,0 +1,180 @@ +import { CipherData } from '../models/data/cipherData'; +import { CollectionData } from '../models/data/collectionData'; +import { FolderData } from '../models/data/folderData'; + +import { CipherResponse } from '../models/response/cipherResponse'; +import { CollectionResponse } from '../models/response/collectionResponse'; +import { DomainsResponse } from '../models/response/domainsResponse'; +import { FolderResponse } from '../models/response/folderResponse'; +import { ProfileResponse } from '../models/response/profileResponse'; +import { SyncResponse } from '../models/response/syncResponse'; + +import ApiService from './api.service'; +import CipherService from './cipher.service'; +import CollectionService from './collection.service'; +import CryptoService from './crypto.service'; +import FolderService from './folder.service'; +import SettingsService from './settings.service'; +import UserService from './user.service'; +import UtilsService from './utils.service'; + +const Keys = { + lastSyncPrefix: 'lastSync_', +}; + +export default class SyncService { + syncInProgress: boolean = false; + + constructor(private userService: UserService, private apiService: ApiService, + private settingsService: SettingsService, private folderService: FolderService, + private cipherService: CipherService, private cryptoService: CryptoService, + private collectionService: CollectionService, private logoutCallback: Function) { + } + + async getLastSync() { + const userId = await this.userService.getUserId(); + const lastSync = await UtilsService.getObjFromStorage(Keys.lastSyncPrefix + userId); + if (lastSync) { + return new Date(lastSync); + } + + return null; + } + + async setLastSync(date: Date) { + const userId = await this.userService.getUserId(); + await UtilsService.saveObjToStorage(Keys.lastSyncPrefix + userId, date.toJSON()); + } + + syncStarted() { + this.syncInProgress = true; + chrome.runtime.sendMessage({ command: 'syncStarted' }); + } + + syncCompleted(successfully: boolean) { + this.syncInProgress = false; + // tslint:disable-next-line + chrome.runtime.sendMessage({ command: 'syncCompleted', successfully: successfully }); + } + + async fullSync(forceSync: boolean) { + this.syncStarted(); + const isAuthenticated = await this.userService.isAuthenticated(); + if (!isAuthenticated) { + this.syncCompleted(false); + return false; + } + + const now = new Date(); + const needsSyncResult = await this.needsSyncing(forceSync); + const needsSync = needsSyncResult[0]; + const skipped = needsSyncResult[1]; + + if (skipped) { + this.syncCompleted(false); + return false; + } + + if (!needsSync) { + await this.setLastSync(now); + this.syncCompleted(false); + return false; + } + + const userId = await this.userService.getUserId(); + try { + const response = await this.apiService.getSync(); + + await this.syncProfile(response.profile); + await this.syncFolders(userId, response.folders); + await this.syncCollections(response.collections); + await this.syncCiphers(userId, response.ciphers); + await this.syncSettings(userId, response.domains); + + await this.setLastSync(now); + this.syncCompleted(true); + return true; + } catch (e) { + this.syncCompleted(false); + return false; + } + } + + // Helpers + + private async needsSyncing(forceSync: boolean) { + if (forceSync) { + return [true, false]; + } + + try { + const response = await this.apiService.getAccountRevisionDate(); + const accountRevisionDate = new Date(response); + const lastSync = await this.getLastSync(); + if (lastSync != null && accountRevisionDate <= lastSync) { + return [false, false]; + } + + return [true, false]; + } catch (e) { + return [false, true]; + } + } + + private async syncProfile(response: ProfileResponse) { + const stamp = await this.userService.getSecurityStamp(); + if (stamp != null && stamp !== response.securityStamp) { + if (this.logoutCallback != null) { + this.logoutCallback(true); + } + + throw new Error('Stamp has changed'); + } + + await this.cryptoService.setEncKey(response.key); + await this.cryptoService.setEncPrivateKey(response.privateKey); + await this.cryptoService.setOrgKeys(response.organizations); + await this.userService.setSecurityStamp(response.securityStamp); + } + + private async syncFolders(userId: string, response: FolderResponse[]) { + const folders: { [id: string]: FolderData; } = {}; + response.forEach((f) => { + folders[f.id] = new FolderData(f, userId); + }); + return await this.folderService.replace(folders); + } + + private async syncCollections(response: CollectionResponse[]) { + const collections: { [id: string]: CollectionData; } = {}; + response.forEach((c) => { + collections[c.id] = new CollectionData(c); + }); + return await this.collectionService.replace(collections); + } + + private async syncCiphers(userId: string, response: CipherResponse[]) { + const ciphers: { [id: string]: CipherData; } = {}; + response.forEach((c) => { + ciphers[c.id] = new CipherData(c, userId); + }); + return await this.cipherService.replace(ciphers); + } + + private async syncSettings(userId: string, response: DomainsResponse) { + let eqDomains: string[][] = []; + if (response != null && response.equivalentDomains != null) { + eqDomains = eqDomains.concat(response.equivalentDomains); + } + + if (response != null && response.globalEquivalentDomains != null) { + response.globalEquivalentDomains.forEach((global) => { + if (global.domains.length > 0) { + eqDomains.push(global.domains); + } + }); + } + + return this.settingsService.setEquivalentDomains(eqDomains); + } +} diff --git a/src/services/token.service.ts b/src/services/token.service.ts new file mode 100644 index 0000000000..3ebaa239be --- /dev/null +++ b/src/services/token.service.ts @@ -0,0 +1,170 @@ +import ConstantsService from './constants.service'; +import UtilsService from './utils.service'; + +const Keys = { + accessToken: 'accessToken', + refreshToken: 'refreshToken', + twoFactorTokenPrefix: 'twoFactorToken_', +}; + +export default class TokenService { + token: string; + decodedToken: any; + refreshToken: string; + + setTokens(accessToken: string, refreshToken: string): Promise { + return Promise.all([ + this.setToken(accessToken), + this.setRefreshToken(refreshToken), + ]); + } + + setToken(token: string): Promise { + this.token = token; + this.decodedToken = null; + return UtilsService.saveObjToStorage(Keys.accessToken, token); + } + + async getToken(): Promise { + if (this.token != null) { + return this.token; + } + + this.token = await UtilsService.getObjFromStorage(Keys.accessToken); + return this.token; + } + + setRefreshToken(refreshToken: string): Promise { + this.refreshToken = refreshToken; + return UtilsService.saveObjToStorage(Keys.refreshToken, refreshToken); + } + + async getRefreshToken(): Promise { + if (this.refreshToken != null) { + return this.refreshToken; + } + + this.refreshToken = await UtilsService.getObjFromStorage(Keys.refreshToken); + return this.refreshToken; + } + + setTwoFactorToken(token: string, email: string): Promise { + return UtilsService.saveObjToStorage(Keys.twoFactorTokenPrefix + email, token); + } + + getTwoFactorToken(email: string): Promise { + return UtilsService.getObjFromStorage(Keys.twoFactorTokenPrefix + email); + } + + clearTwoFactorToken(email: string): Promise { + return UtilsService.removeFromStorage(Keys.twoFactorTokenPrefix + email); + } + + clearToken(): Promise { + this.token = null; + this.decodedToken = null; + this.refreshToken = null; + + return Promise.all([ + UtilsService.removeFromStorage(Keys.accessToken), + UtilsService.removeFromStorage(Keys.refreshToken), + ]); + } + + // jwthelper methods + // ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js + + decodeToken(): any { + if (this.decodedToken) { + return this.decodedToken; + } + + if (this.token == null) { + throw new Error('Token not found.'); + } + + const parts = this.token.split('.'); + if (parts.length !== 3) { + throw new Error('JWT must have 3 parts'); + } + + const decoded = UtilsService.urlBase64Decode(parts[1]); + if (decoded == null) { + throw new Error('Cannot decode the token'); + } + + this.decodedToken = JSON.parse(decoded); + return this.decodedToken; + } + + getTokenExpirationDate(): Date { + const decoded = this.decodeToken(); + if (typeof decoded.exp === 'undefined') { + return null; + } + + const d = new Date(0); // The 0 here is the key, which sets the date to the epoch + d.setUTCSeconds(decoded.exp); + return d; + } + + tokenSecondsRemaining(offsetSeconds: number = 0): number { + const d = this.getTokenExpirationDate(); + if (d == null) { + return 0; + } + + const msRemaining = d.valueOf() - (new Date().valueOf() + (offsetSeconds * 1000)); + return Math.round(msRemaining / 1000); + } + + tokenNeedsRefresh(minutes: number = 5): boolean { + const sRemaining = this.tokenSecondsRemaining(); + return sRemaining < (60 * minutes); + } + + getUserId(): string { + const decoded = this.decodeToken(); + if (typeof decoded.sub === 'undefined') { + throw new Error('No user id found'); + } + + return decoded.sub as string; + } + + getEmail(): string { + const decoded = this.decodeToken(); + if (typeof decoded.email === 'undefined') { + throw new Error('No email found'); + } + + return decoded.email as string; + } + + getName(): string { + const decoded = this.decodeToken(); + if (typeof decoded.name === 'undefined') { + throw new Error('No name found'); + } + + return decoded.name as string; + } + + getPremium(): boolean { + const decoded = this.decodeToken(); + if (typeof decoded.premium === 'undefined') { + return false; + } + + return decoded.premium as boolean; + } + + getIssuer(): string { + const decoded = this.decodeToken(); + if (typeof decoded.iss === 'undefined') { + throw new Error('No issuer found'); + } + + return decoded.iss as string; + } +} diff --git a/src/services/totp.service.ts b/src/services/totp.service.ts new file mode 100644 index 0000000000..ae4d4ec57a --- /dev/null +++ b/src/services/totp.service.ts @@ -0,0 +1,113 @@ +import ConstantsService from './constants.service'; +import UtilsService from './utils.service'; + +const b32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + +const TotpAlgorithm = { + name: 'HMAC', + hash: { name: 'SHA-1' }, +}; + +export default class TotpService { + async getCode(keyb32: string): Promise { + const epoch = Math.round(new Date().getTime() / 1000.0); + const timeHex = this.leftpad(this.dec2hex(Math.floor(epoch / 30)), 16, '0'); + const timeBytes = this.hex2bytes(timeHex); + const keyBytes = this.b32tobytes(keyb32); + + if (!keyBytes.length || !timeBytes.length) { + return null; + } + + const hashHex = await this.sign(keyBytes, timeBytes); + if (!hashHex) { + return null; + } + + const offset = this.hex2dec(hashHex.substring(hashHex.length - 1)); + // tslint:disable-next-line + let otp = (this.hex2dec(hashHex.substr(offset * 2, 8)) & this.hex2dec('7fffffff')) + ''; + otp = (otp).substr(otp.length - 6, 6); + return otp; + } + + async isAutoCopyEnabled(): Promise { + return !(await UtilsService.getObjFromStorage(ConstantsService.disableAutoTotpCopyKey)); + } + + // Helpers + + private leftpad(s: string, l: number, p: string): string { + if (l + 1 >= s.length) { + s = Array(l + 1 - s.length).join(p) + s; + } + return s; + } + + private dec2hex(d: number): string { + return (d < 15.5 ? '0' : '') + Math.round(d).toString(16); + } + + private hex2dec(s: string): number { + return parseInt(s, 16); + } + + private hex2bytes(s: string): Uint8Array { + const bytes = new Uint8Array(s.length / 2); + for (let i = 0; i < s.length; i += 2) { + bytes[i / 2] = parseInt(s.substr(i, 2), 16); + } + return bytes; + } + + private buff2hex(buff: ArrayBuffer): string { + const bytes = new Uint8Array(buff); + const hex: string[] = []; + bytes.forEach((b) => { + // tslint:disable-next-line + hex.push((b >>> 4).toString(16)); + // tslint:disable-next-line + hex.push((b & 0xF).toString(16)); + }); + return hex.join(''); + } + + private b32tohex(s: string): string { + s = s.toUpperCase(); + let cleanedInput = ''; + + for (let i = 0; i < s.length; i++) { + if (b32Chars.indexOf(s[i]) < 0) { + continue; + } + + cleanedInput += s[i]; + } + s = cleanedInput; + + let bits = ''; + let hex = ''; + for (let i = 0; i < s.length; i++) { + const byteIndex = b32Chars.indexOf(s.charAt(i)); + if (byteIndex < 0) { + continue; + } + bits += this.leftpad(byteIndex.toString(2), 5, '0'); + } + for (let i = 0; i + 4 <= bits.length; i += 4) { + const chunk = bits.substr(i, 4); + hex = hex + parseInt(chunk, 2).toString(16); + } + return hex; + } + + private b32tobytes(s: string): Uint8Array { + return this.hex2bytes(this.b32tohex(s)); + } + + private async sign(keyBytes: Uint8Array, timeBytes: Uint8Array) { + const key = await window.crypto.subtle.importKey('raw', keyBytes, TotpAlgorithm, false, ['sign']); + const signature = await window.crypto.subtle.sign(TotpAlgorithm, key, timeBytes); + return this.buff2hex(signature); + } +} diff --git a/src/services/user.service.ts b/src/services/user.service.ts new file mode 100644 index 0000000000..caa56c0d6f --- /dev/null +++ b/src/services/user.service.ts @@ -0,0 +1,79 @@ +import TokenService from './token.service'; +import UtilsService from './utils.service'; + +const Keys = { + userId: 'userId', + userEmail: 'userEmail', + stamp: 'securityStamp', +}; + +export default class UserService { + userId: string; + email: string; + stamp: string; + + constructor(private tokenService: TokenService) { + } + + setUserIdAndEmail(userId: string, email: string): Promise { + this.email = email; + this.userId = userId; + + return Promise.all([ + UtilsService.saveObjToStorage(Keys.userEmail, email), + UtilsService.saveObjToStorage(Keys.userId, userId), + ]); + } + + setSecurityStamp(stamp: string): Promise { + this.stamp = stamp; + return UtilsService.saveObjToStorage(Keys.stamp, stamp); + } + + async getUserId(): Promise { + if (this.userId != null) { + return this.userId; + } + + this.userId = await UtilsService.getObjFromStorage(Keys.userId); + return this.userId; + } + + async getEmail(): Promise { + if (this.email != null) { + return this.email; + } + + this.email = await UtilsService.getObjFromStorage(Keys.userEmail); + return this.email; + } + + async getSecurityStamp(): Promise { + if (this.stamp != null) { + return this.stamp; + } + + this.stamp = await UtilsService.getObjFromStorage(Keys.stamp); + return this.stamp; + } + + async clear(): Promise { + await Promise.all([ + UtilsService.removeFromStorage(Keys.userId), + UtilsService.removeFromStorage(Keys.userEmail), + UtilsService.removeFromStorage(Keys.stamp), + ]); + + this.userId = this.email = this.stamp = null; + } + + async isAuthenticated(): Promise { + const token = await this.tokenService.getToken(); + if (token == null) { + return false; + } + + const userId = await this.getUserId(); + return userId != null; + } +} diff --git a/src/services/utils.service.spec.ts b/src/services/utils.service.spec.ts new file mode 100644 index 0000000000..c976888de8 --- /dev/null +++ b/src/services/utils.service.spec.ts @@ -0,0 +1,113 @@ +import UtilsService from './utils.service'; +import { BrowserType } from '../enums/browserType.enum'; + +describe('Utils Service', () => { + describe('getDomain', () => { + it('should fail for invalid urls', () => { + expect(UtilsService.getDomain(null)).toBeNull(); + expect(UtilsService.getDomain(undefined)).toBeNull(); + expect(UtilsService.getDomain(' ')).toBeNull(); + expect(UtilsService.getDomain('https://bit!:"_&ward.com')).toBeNull(); + expect(UtilsService.getDomain('bitwarden')).toBeNull(); + }); + + it('should handle urls without protocol', () => { + expect(UtilsService.getDomain('bitwarden.com')).toBe('bitwarden.com'); + expect(UtilsService.getDomain('wrong://bitwarden.com')).toBe('bitwarden.com'); + }); + + it('should handle valid urls', () => { + expect(UtilsService.getDomain('https://bitwarden')).toBe('bitwarden'); + expect(UtilsService.getDomain('https://bitwarden.com')).toBe('bitwarden.com'); + expect(UtilsService.getDomain('http://bitwarden.com')).toBe('bitwarden.com'); + expect(UtilsService.getDomain('http://vault.bitwarden.com')).toBe('bitwarden.com'); + expect(UtilsService.getDomain('https://user:password@bitwarden.com:8080/password/sites?and&query#hash')).toBe('bitwarden.com'); + expect(UtilsService.getDomain('https://bitwarden.unknown')).toBe('bitwarden.unknown'); + }); + + it('should support localhost and IP', () => { + expect(UtilsService.getDomain('https://localhost')).toBe('localhost'); + expect(UtilsService.getDomain('https://192.168.1.1')).toBe('192.168.1.1'); + }); + }); + + describe('getHostname', () => { + it('should fail for invalid urls', () => { + expect(UtilsService.getHostname(null)).toBeNull(); + expect(UtilsService.getHostname(undefined)).toBeNull(); + expect(UtilsService.getHostname(' ')).toBeNull(); + expect(UtilsService.getHostname('https://bit!:"_&ward.com')).toBeNull(); + expect(UtilsService.getHostname('bitwarden')).toBeNull(); + }); + + it('should handle valid urls', () => { + expect(UtilsService.getHostname('https://bitwarden.com')).toBe('bitwarden.com'); + expect(UtilsService.getHostname('http://bitwarden.com')).toBe('bitwarden.com'); + expect(UtilsService.getHostname('http://vault.bitwarden.com')).toBe('vault.bitwarden.com'); + expect(UtilsService.getHostname('https://user:password@bitwarden.com:8080/password/sites?and&query#hash')).toBe('bitwarden.com'); + }); + + it('should support localhost and IP', () => { + expect(UtilsService.getHostname('https://localhost')).toBe('localhost'); + expect(UtilsService.getHostname('https://192.168.1.1')).toBe('192.168.1.1'); + }); + }); + + describe('newGuid', () => { + it('should create a valid guid', () => { + const validGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + expect(UtilsService.newGuid()).toMatch(validGuid); + }); + }); + + describe('getBrowser', () => { + const original = navigator.userAgent; + + // Reset the userAgent. + afterAll(() => { + Object.defineProperty(navigator, 'userAgent', { + value: original + }); + }); + + it('should detect chrome', () => { + Object.defineProperty(navigator, 'userAgent', { + configurable: true, + value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36' + }); + + const utilsService = new UtilsService(); + expect(utilsService.getBrowser()).toBe(BrowserType.Chrome); + }); + + it('should detect firefox', () => { + Object.defineProperty(navigator, 'userAgent', { + configurable: true, + value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0' + }); + + const utilsService = new UtilsService(); + expect(utilsService.getBrowser()).toBe(BrowserType.Firefox); + }); + + it('should detect opera', () => { + Object.defineProperty(navigator, 'userAgent', { + configurable: true, + value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3175.3 Safari/537.36 OPR/49.0.2695.0 (Edition developer)' + }); + + const utilsService = new UtilsService(); + expect(utilsService.getBrowser()).toBe(BrowserType.Opera); + }); + + it('should detect edge', () => { + Object.defineProperty(navigator, 'userAgent', { + configurable: true, + value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; ServiceUI 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.15063' + }); + + const utilsService = new UtilsService(); + expect(utilsService.getBrowser()).toBe(BrowserType.Edge); + }); + }); +}); diff --git a/src/services/utils.service.ts b/src/services/utils.service.ts new file mode 100644 index 0000000000..b2230de2b9 --- /dev/null +++ b/src/services/utils.service.ts @@ -0,0 +1,407 @@ +import * as tldjs from 'tldjs'; +import { BrowserType } from '../enums/browserType.enum'; +import { UtilsService as UtilsServiceInterface } from './abstractions/utils.service'; + +const AnalyticsIds = { + [BrowserType.Chrome]: 'UA-81915606-6', + [BrowserType.Firefox]: 'UA-81915606-7', + [BrowserType.Opera]: 'UA-81915606-8', + [BrowserType.Edge]: 'UA-81915606-9', + [BrowserType.Vivaldi]: 'UA-81915606-15', + [BrowserType.Safari]: 'UA-81915606-16', +}; + +export default class UtilsService implements UtilsServiceInterface { + static copyToClipboard(text: string, doc?: Document): void { + doc = doc || document; + if ((window as any).clipboardData && (window as any).clipboardData.setData) { + // IE specific code path to prevent textarea being shown while dialog is visible. + (window as any).clipboardData.setData('Text', text); + } else if (doc.queryCommandSupported && doc.queryCommandSupported('copy')) { + const textarea = doc.createElement('textarea'); + textarea.textContent = text; + // Prevent scrolling to bottom of page in MS Edge. + textarea.style.position = 'fixed'; + doc.body.appendChild(textarea); + textarea.select(); + + try { + // Security exception may be thrown by some browsers. + doc.execCommand('copy'); + } catch (e) { + // tslint:disable-next-line + console.warn('Copy to clipboard failed.', e); + } finally { + doc.body.removeChild(textarea); + } + } + } + + static urlBase64Decode(str: string): string { + let output = str.replace(/-/g, '+').replace(/_/g, '/'); + switch (output.length % 4) { + case 0: + break; + case 2: + output += '=='; + break; + case 3: + output += '='; + break; + default: + throw new Error('Illegal base64url string!'); + } + + return decodeURIComponent(escape(window.atob(output))); + } + + // ref: http://stackoverflow.com/a/2117523/1090359 + static newGuid(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + // tslint:disable-next-line + const r = Math.random() * 16 | 0; + // tslint:disable-next-line + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } + + // EFForg/OpenWireless + // ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js + static secureRandomNumber(min: number, max: number): number { + let rval = 0; + const range = max - min + 1; + const bitsNeeded = Math.ceil(Math.log2(range)); + if (bitsNeeded > 53) { + throw new Error('We cannot generate numbers larger than 53 bits.'); + } + + const bytesNeeded = Math.ceil(bitsNeeded / 8); + const mask = Math.pow(2, bitsNeeded) - 1; + // 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111 + + // Create byte array and fill with N random numbers + const byteArray = new Uint8Array(bytesNeeded); + window.crypto.getRandomValues(byteArray); + + let p = (bytesNeeded - 1) * 8; + for (let i = 0; i < bytesNeeded; i++) { + rval += byteArray[i] * Math.pow(2, p); + p -= 8; + } + + // Use & to apply the mask and reduce the number of recursive lookups + // tslint:disable-next-line + rval = rval & mask; + + if (rval >= range) { + // Integer out of acceptable range + return UtilsService.secureRandomNumber(min, max); + } + + // Return an integer that falls within the range + return min + rval; + } + + static fromB64ToArray(str: string): Uint8Array { + const binaryString = window.atob(str); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; + } + + static fromUtf8ToArray(str: string): Uint8Array { + const strUtf8 = unescape(encodeURIComponent(str)); + const arr = new Uint8Array(strUtf8.length); + for (let i = 0; i < strUtf8.length; i++) { + arr[i] = strUtf8.charCodeAt(i); + } + return arr; + } + + static fromBufferToB64(buffer: ArrayBuffer): string { + let binary = ''; + const bytes = new Uint8Array(buffer); + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + return window.btoa(binary); + } + + static fromBufferToUtf8(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer); + const encodedString = String.fromCharCode.apply(null, bytes); + return decodeURIComponent(escape(encodedString)); + } + + static saveObjToStorage(key: string, obj: any) { + return new Promise((resolve) => { + chrome.storage.local.set({ [key]: obj }, () => { + resolve(); + }); + }); + } + + static removeFromStorage(key: string) { + return new Promise((resolve) => { + chrome.storage.local.remove(key, () => { + resolve(); + }); + }); + } + + static getObjFromStorage(key: string): Promise { + return new Promise((resolve) => { + chrome.storage.local.get(key, (obj: any) => { + if (obj && (typeof obj[key] !== 'undefined') && obj[key] !== null) { + resolve(obj[key] as T); + } else { + resolve(null); + } + }); + }); + } + + static getDomain(uriString: string): string { + if (uriString == null) { + return null; + } + + uriString = uriString.trim(); + if (uriString === '') { + return null; + } + + if (uriString.startsWith('http://') || uriString.startsWith('https://')) { + try { + const url = new URL(uriString); + + if (url.hostname === 'localhost' || UtilsService.validIpAddress(url.hostname)) { + return url.hostname; + } + + const urlDomain = tldjs.getDomain(url.hostname); + return urlDomain != null ? urlDomain : url.hostname; + } catch (e) { } + } + + const domain = tldjs.getDomain(uriString); + if (domain != null) { + return domain; + } + + return null; + } + + static getHostname(uriString: string): string { + if (uriString == null) { + return null; + } + + uriString = uriString.trim(); + if (uriString === '') { + return null; + } + + if (uriString.startsWith('http://') || uriString.startsWith('https://')) { + try { + const url = new URL(uriString); + return url.hostname; + } catch (e) { } + } + + return null; + } + + private static validIpAddress(ipString: string): boolean { + // tslint:disable-next-line + const ipRegex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + return ipRegex.test(ipString); + } + + private browserCache: BrowserType = null; + private analyticsIdCache: string = null; + + getBrowser(): BrowserType { + if (this.browserCache) { + return this.browserCache; + } + + if (navigator.userAgent.indexOf('Firefox') !== -1 || navigator.userAgent.indexOf('Gecko/') !== -1) { + this.browserCache = BrowserType.Firefox; + } else if ((!!(window as any).opr && !!opr.addons) || !!(window as any).opera || + navigator.userAgent.indexOf(' OPR/') >= 0) { + this.browserCache = BrowserType.Opera; + } else if (navigator.userAgent.indexOf(' Edge/') !== -1) { + this.browserCache = BrowserType.Edge; + } else if (navigator.userAgent.indexOf(' Vivaldi/') !== -1) { + this.browserCache = BrowserType.Vivaldi; + } else if ((window as any).chrome) { + this.browserCache = BrowserType.Chrome; + } + + return this.browserCache; + } + + getBrowserString(): string { + return BrowserType[this.getBrowser()].toLowerCase(); + } + + isFirefox(): boolean { + return this.getBrowser() === BrowserType.Firefox; + } + + isChrome(): boolean { + return this.getBrowser() === BrowserType.Chrome; + } + + isEdge(): boolean { + return this.getBrowser() === BrowserType.Edge; + } + + isOpera(): boolean { + return this.getBrowser() === BrowserType.Opera; + } + + isVivaldi(): boolean { + return this.getBrowser() === BrowserType.Vivaldi; + } + + isSafari(): boolean { + return this.getBrowser() === BrowserType.Safari; + } + + analyticsId(): string { + if (this.analyticsIdCache) { + return this.analyticsIdCache; + } + + this.analyticsIdCache = AnalyticsIds[this.getBrowser()]; + return this.analyticsIdCache; + } + + initListSectionItemListeners(doc: Document, angular: any): void { + if (!doc) { + throw new Error('doc parameter required'); + } + + const sectionItems = doc.querySelectorAll( + '.list-section-item:not([data-bw-events="1"])'); + const sectionFormItems = doc.querySelectorAll( + '.list-section-item:not([data-bw-events="1"]) input, ' + + '.list-section-item:not([data-bw-events="1"]) select, ' + + '.list-section-item:not([data-bw-events="1"]) textarea'); + + sectionItems.forEach((item) => { + (item as HTMLElement).dataset.bwEvents = '1'; + + item.addEventListener('click', (e) => { + if (e.defaultPrevented) { + return; + } + + const el = e.target as HTMLElement; + + // Some elements will already focus properly + if (el.tagName != null) { + switch (el.tagName.toLowerCase()) { + case 'label': case 'input': case 'textarea': case 'select': + return; + default: + break; + } + } + + const cell = el.closest('.list-section-item'); + if (!cell) { + return; + } + + const textFilter = 'input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"])'; + const text = cell.querySelectorAll(textFilter + ', textarea'); + const checkbox = cell.querySelectorAll('input[type="checkbox"]'); + const select = cell.querySelectorAll('select'); + + if (text.length > 0) { + (text[0] as HTMLElement).focus(); + } else if (select.length > 0) { + (select[0] as HTMLElement).focus(); + } else if (checkbox.length > 0) { + const cb = checkbox[0] as HTMLInputElement; + cb.checked = !cb.checked; + if (angular) { + angular.element(checkbox[0]).triggerHandler('click'); + } + } + }, false); + }); + + sectionFormItems.forEach((item) => { + const itemCell = item.closest('.list-section-item'); + (itemCell as HTMLElement).dataset.bwEvents = '1'; + + item.addEventListener('focus', (e: Event) => { + const el = e.target as HTMLElement; + const cell = el.closest('.list-section-item'); + if (!cell) { + return; + } + + cell.classList.add('active'); + }, false); + + item.addEventListener('blur', (e: Event) => { + const el = e.target as HTMLElement; + const cell = el.closest('.list-section-item'); + if (!cell) { + return; + } + + cell.classList.remove('active'); + }, false); + }); + } + + getDomain(uriString: string): string { + return UtilsService.getDomain(uriString); + } + + getHostname(uriString: string): string { + return UtilsService.getHostname(uriString); + } + + copyToClipboard(text: string, doc?: Document) { + UtilsService.copyToClipboard(text, doc); + } + + inSidebar(theWindow: Window): boolean { + return theWindow.location.search !== '' && theWindow.location.search.indexOf('uilocation=sidebar') > -1; + } + + inTab(theWindow: Window): boolean { + return theWindow.location.search !== '' && theWindow.location.search.indexOf('uilocation=tab') > -1; + } + + inPopout(theWindow: Window): boolean { + return theWindow.location.search !== '' && theWindow.location.search.indexOf('uilocation=popout') > -1; + } + + inPopup(theWindow: Window): boolean { + return theWindow.location.search === '' || theWindow.location.search.indexOf('uilocation=') === -1 || + theWindow.location.search.indexOf('uilocation=popup') > -1; + } + + saveObjToStorage(key: string, obj: any): Promise { + return UtilsService.saveObjToStorage(key, obj); + } + + removeFromStorage(key: string): Promise { + return UtilsService.removeFromStorage(key); + } + + getObjFromStorage(key: string): Promise { + return UtilsService.getObjFromStorage(key); + } +} From 9be966017d0bd41fc57da3db97c9d2b33a2548f5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 3 Jan 2018 21:20:58 -0500 Subject: [PATCH 0003/1626] project/config files --- .editorconfig | 16 ++++++++++++++ .gitignore | 7 ++++++ SECURITY.md | 45 ++++++++++++++++++++++++++++++++++++++ bitwarden-jslib.sln | 38 ++++++++++++++++++++++++++++++++ package.json | 30 +++++++++++++++++++++++++ tsconfig.json | 14 ++++++++++++ tslint.json | 53 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 203 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 SECURITY.md create mode 100644 bitwarden-jslib.sln create mode 100644 package.json create mode 100644 tsconfig.json create mode 100644 tslint.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..65932d2edc --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Matches multiple files with brace expansion notation +# Set default charset +[*.{js,ts,less}] +charset = utf-8 +indent_style = space +indent_size = 4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..c7d7fd6a34 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.vs +.idea +node_modules +npm-debug.log +*.crx +*.pem +package-lock.json diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..3564ea7b08 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,45 @@ +bitwarden believes that working with security researchers across the globe is crucial to keeping our +users safe. If you believe you've found a security issue in our product or service, we encourage you to +notify us. We welcome working with you to resolve the issue promptly. Thanks in advance! + +# Disclosure Policy + +- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every + effort to quickly resolve the issue. +- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a + third-party. We may publicly disclose the issue before resolving it, if appropriate. +- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or + degradation of our service. Only interact with accounts you own or with explicit permission of the + account holder. +- If you would like to encrypt your report, please use the PGP key with long ID + `0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool). + +# In-scope + +- Security issues in any current release of bitwarden. This includes the web vault, browser extension, + and mobile apps (iOS and Android). Product downloads are available at https://bitwarden.com. Source + code is available at https://github.com/bitwarden. + +# Exclusions + +The following bug classes are out-of scope: + +- Bugs that are already reported on any of bitwarden's issue trackers (https://github.com/bitwarden), + or that we already know of. Note that some of our issue tracking is private. +- Issues in an upstream software dependency (ex: Xamarin, ASP.NET) which are already reported to the + upstream maintainer. +- Attacks requiring physical access to a user's device. +- Self-XSS +- Issues related to software or protocols not under bitwarden's control +- Vulnerabilities in outdated versions of bitwarden +- Missing security best practices that do not directly lead to a vulnerability +- Issues that do not have any impact on the general public + +While researching, we'd like to ask you to refrain from: + +- Denial of service +- Spamming +- Social engineering (including phishing) of bitwarden staff or contractors +- Any physical attempts against bitwarden property or data centers + +Thank you for helping keep bitwarden and our users safe! diff --git a/bitwarden-jslib.sln b/bitwarden-jslib.sln new file mode 100644 index 0000000000..3e98b7dc54 --- /dev/null +++ b/bitwarden-jslib.sln @@ -0,0 +1,38 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "bitwarden-jslib", ".", "{A4DE5293-DB47-41D1-8890-7C67B83F663C}" + ProjectSection(WebsiteProperties) = preProject + TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.0" + Debug.AspNetCompiler.VirtualPath = "/localhost_4405" + Debug.AspNetCompiler.PhysicalPath = "." + Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_4405\" + Debug.AspNetCompiler.Updateable = "true" + Debug.AspNetCompiler.ForceOverwrite = "true" + Debug.AspNetCompiler.FixedNames = "false" + Debug.AspNetCompiler.Debug = "True" + Release.AspNetCompiler.VirtualPath = "/localhost_4405" + Release.AspNetCompiler.PhysicalPath = "." + Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_4405\" + Release.AspNetCompiler.Updateable = "true" + Release.AspNetCompiler.ForceOverwrite = "true" + Release.AspNetCompiler.FixedNames = "false" + Release.AspNetCompiler.Debug = "False" + VWDPort = "4405" + SlnRelativePath = "." + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A4DE5293-DB47-41D1-8890-7C67B83F663C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4DE5293-DB47-41D1-8890-7C67B83F663C}.Debug|Any CPU.Build.0 = Debug|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/package.json b/package.json new file mode 100644 index 0000000000..4fd9fdd4a1 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "bitwarden-jslib", + "version": "0.0.1", + "scripts": { + + }, + "devDependencies": { + "clean-webpack-plugin": "^0.1.17", + "copy-webpack-plugin": "^4.2.0", + "css-loader": "^0.28.7", + "extract-text-webpack-plugin": "^3.0.1", + "file-loader": "^1.1.5", + "html-loader": "^0.5.1", + "html-webpack-plugin": "^2.30.1", + "style-loader": "^0.19.0", + "ts-loader": "^3.0.5", + "tslint": "^5.8.0", + "tslint-loader": "^3.5.3", + "typescript": "^2.5.3", + "webpack": "^3.8.1", + "webpack-merge": "^4.1.0" + }, + "dependencies": { + "@types/node-forge": "0.6.10", + "@types/tldjs": "1.7.1", + "@types/webcrypto": "0.0.28", + "node-forge": "0.7.1", + "tldjs": "2.0.0" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..4087a2c91b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "noImplicitAny": true, + "module": "es6", + "target": "ES2016", + "allowJs": true, + "sourceMap": true, + "types": [ + ] + }, + "exclude": [ + "node_modules" + ] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000000..b6d5571669 --- /dev/null +++ b/tslint.json @@ -0,0 +1,53 @@ +{ + "extends": "tslint:recommended", + "rules": { + "align": [ true, "statements", "members" ], + "ban-types": { + "options": [ + [ "Object", "Avoid using the `Object` type. Did you mean `object`?" ], + [ "Boolean", "Avoid using the `Boolean` type. Did you mean `boolean`?" ], + [ "Number", "Avoid using the `Number` type. Did you mean `number`?" ], + [ "String", "Avoid using the `String` type. Did you mean `string`?" ], + [ "Symbol", "Avoid using the `Symbol` type. Did you mean `symbol`?" ] + ] + }, + "member-access": [ true, "no-public" ], + "member-ordering": [ + true, + { + "order": [ + "public-static-field", + "public-static-method", + "protected-static-field", + "protected-static-method", + "private-static-field", + "private-static-method", + "public-instance-field", + "protected-instance-field", + "private-instance-field", + "public-constructor", + "protected-constructor", + "private-constructor", + "public-instance-method", + "protected-instance-method", + "private-instance-method" + ] + } + ], + "no-empty": [ true, "allow-empty-catch" ], + "object-literal-sort-keys": false, + "object-literal-shorthand": [ true, "never" ], + "prefer-for-of": false, + "quotemark": [ true, "single" ], + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-module", + "check-operator", + "check-preblock", + "check-separator", + "check-type" + ] + } +} From bbfe69eea0e228f3b41f0fbd3c27056dcd8ae59d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 4 Jan 2018 23:51:02 -0500 Subject: [PATCH 0004/1626] clean out old models/services --- package.json | 23 +- src/index.ts | 1 + src/models/data/attachmentData.ts | 20 - src/models/data/cardData.ts | 20 - src/models/data/cipherData.ts | 87 -- src/models/data/collectionData.ts | 16 - src/models/data/fieldData.ts | 16 - src/models/data/folderData.ts | 18 - src/models/data/identityData.ts | 44 - src/models/data/loginData.ts | 16 - src/models/data/secureNoteData.ts | 12 - src/models/domain/attachment.ts | 43 - src/models/domain/autofillField.ts | 22 - src/models/domain/autofillForm.ts | 7 - src/models/domain/autofillPageDetails.ts | 13 - src/models/domain/autofillScript.ts | 12 - src/models/domain/card.ts | 43 - src/models/domain/cipher.ts | 192 ---- src/models/domain/cipherString.ts | 115 --- src/models/domain/collection.ts | 37 - src/models/domain/domain.ts | 46 - src/models/domain/encryptedObject.ts | 8 - src/models/domain/environmentUrls.ts | 5 - src/models/domain/field.ts | 39 - src/models/domain/folder.ts | 34 - src/models/domain/identity.ts | 79 -- src/models/domain/login.ts | 37 - src/models/domain/passwordHistory.ts | 9 - src/models/domain/secureNote.ts | 27 - src/models/domain/symmetricCryptoKey.ts | 80 -- .../domain/symmetricCryptoKeyBuffers.ts | 9 - src/models/request/cipherRequest.ts | 89 -- src/models/request/deviceRequest.ts | 19 - src/models/request/deviceTokenRequest.ts | 10 - src/models/request/folderRequest.ts | 12 - src/models/request/passwordHintRequest.ts | 10 - src/models/request/registerRequest.ts | 18 - src/models/request/tokenRequest.ts | 49 - src/models/request/twoFactorEmailRequest.ts | 12 - src/models/response/attachmentResponse.ts | 18 - src/models/response/cipherResponse.ts | 44 - src/models/response/collectionResponse.ts | 14 - src/models/response/deviceResponse.ts | 20 - src/models/response/domainsResponse.ts | 20 - src/models/response/errorResponse.ts | 37 - src/models/response/folderResponse.ts | 14 - src/models/response/globalDomainResponse.ts | 14 - src/models/response/identityTokenResponse.ts | 24 - src/models/response/keysResponse.ts | 12 - src/models/response/listResponse.ts | 10 - .../response/profileOrganizationResponse.ts | 30 - src/models/response/profileResponse.ts | 39 - src/models/response/syncResponse.ts | 44 - src/services/abstractions/crypto.service.ts | 28 - src/services/abstractions/utils.service.ts | 22 - src/services/api.service.ts | 454 --------- src/services/appId.service.ts | 31 - src/services/autofill.service.ts | 872 ------------------ src/services/cipher.service.ts | 514 ----------- src/services/collection.service.ts | 124 --- src/services/constants.service.ts | 116 --- src/services/crypto.service.ts | 597 ------------ src/services/environment.service.ts | 87 -- src/services/folder.service.ts | 161 ---- src/services/i18n.service.ts | 28 - src/services/lock.service.ts | 89 -- src/services/passwordGeneration.service.ts | 237 ----- src/services/settings.service.ts | 61 -- src/services/sync.service.ts | 180 ---- src/services/token.service.ts | 170 ---- src/services/totp.service.ts | 113 --- src/services/user.service.ts | 79 -- src/services/utils.service.spec.ts | 113 --- src/services/utils.service.ts | 407 -------- 74 files changed, 6 insertions(+), 6166 deletions(-) create mode 100644 src/index.ts delete mode 100644 src/models/data/attachmentData.ts delete mode 100644 src/models/data/cardData.ts delete mode 100644 src/models/data/cipherData.ts delete mode 100644 src/models/data/collectionData.ts delete mode 100644 src/models/data/fieldData.ts delete mode 100644 src/models/data/folderData.ts delete mode 100644 src/models/data/identityData.ts delete mode 100644 src/models/data/loginData.ts delete mode 100644 src/models/data/secureNoteData.ts delete mode 100644 src/models/domain/attachment.ts delete mode 100644 src/models/domain/autofillField.ts delete mode 100644 src/models/domain/autofillForm.ts delete mode 100644 src/models/domain/autofillPageDetails.ts delete mode 100644 src/models/domain/autofillScript.ts delete mode 100644 src/models/domain/card.ts delete mode 100644 src/models/domain/cipher.ts delete mode 100644 src/models/domain/cipherString.ts delete mode 100644 src/models/domain/collection.ts delete mode 100644 src/models/domain/domain.ts delete mode 100644 src/models/domain/encryptedObject.ts delete mode 100644 src/models/domain/environmentUrls.ts delete mode 100644 src/models/domain/field.ts delete mode 100644 src/models/domain/folder.ts delete mode 100644 src/models/domain/identity.ts delete mode 100644 src/models/domain/login.ts delete mode 100644 src/models/domain/passwordHistory.ts delete mode 100644 src/models/domain/secureNote.ts delete mode 100644 src/models/domain/symmetricCryptoKey.ts delete mode 100644 src/models/domain/symmetricCryptoKeyBuffers.ts delete mode 100644 src/models/request/cipherRequest.ts delete mode 100644 src/models/request/deviceRequest.ts delete mode 100644 src/models/request/deviceTokenRequest.ts delete mode 100644 src/models/request/folderRequest.ts delete mode 100644 src/models/request/passwordHintRequest.ts delete mode 100644 src/models/request/registerRequest.ts delete mode 100644 src/models/request/tokenRequest.ts delete mode 100644 src/models/request/twoFactorEmailRequest.ts delete mode 100644 src/models/response/attachmentResponse.ts delete mode 100644 src/models/response/cipherResponse.ts delete mode 100644 src/models/response/collectionResponse.ts delete mode 100644 src/models/response/deviceResponse.ts delete mode 100644 src/models/response/domainsResponse.ts delete mode 100644 src/models/response/errorResponse.ts delete mode 100644 src/models/response/folderResponse.ts delete mode 100644 src/models/response/globalDomainResponse.ts delete mode 100644 src/models/response/identityTokenResponse.ts delete mode 100644 src/models/response/keysResponse.ts delete mode 100644 src/models/response/listResponse.ts delete mode 100644 src/models/response/profileOrganizationResponse.ts delete mode 100644 src/models/response/profileResponse.ts delete mode 100644 src/models/response/syncResponse.ts delete mode 100644 src/services/abstractions/crypto.service.ts delete mode 100644 src/services/abstractions/utils.service.ts delete mode 100644 src/services/api.service.ts delete mode 100644 src/services/appId.service.ts delete mode 100644 src/services/autofill.service.ts delete mode 100644 src/services/cipher.service.ts delete mode 100644 src/services/collection.service.ts delete mode 100644 src/services/constants.service.ts delete mode 100644 src/services/crypto.service.ts delete mode 100644 src/services/environment.service.ts delete mode 100644 src/services/folder.service.ts delete mode 100644 src/services/i18n.service.ts delete mode 100644 src/services/lock.service.ts delete mode 100644 src/services/passwordGeneration.service.ts delete mode 100644 src/services/settings.service.ts delete mode 100644 src/services/sync.service.ts delete mode 100644 src/services/token.service.ts delete mode 100644 src/services/totp.service.ts delete mode 100644 src/services/user.service.ts delete mode 100644 src/services/utils.service.spec.ts delete mode 100644 src/services/utils.service.ts diff --git a/package.json b/package.json index 4fd9fdd4a1..8a192f1729 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,17 @@ { - "name": "bitwarden-jslib", + "name": "@bitwarden/jslib", "version": "0.0.1", "scripts": { }, + "main": "src/index.ts", "devDependencies": { - "clean-webpack-plugin": "^0.1.17", - "copy-webpack-plugin": "^4.2.0", - "css-loader": "^0.28.7", - "extract-text-webpack-plugin": "^3.0.1", - "file-loader": "^1.1.5", - "html-loader": "^0.5.1", - "html-webpack-plugin": "^2.30.1", - "style-loader": "^0.19.0", - "ts-loader": "^3.0.5", "tslint": "^5.8.0", - "tslint-loader": "^3.5.3", - "typescript": "^2.5.3", - "webpack": "^3.8.1", - "webpack-merge": "^4.1.0" + "typescript": "^2.6.2" }, "dependencies": { - "@types/node-forge": "0.6.10", - "@types/tldjs": "1.7.1", + "@types/node-forge": "0.7.1", "@types/webcrypto": "0.0.28", - "node-forge": "0.7.1", - "tldjs": "2.0.0" + "node-forge": "0.7.1" } } diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ + diff --git a/src/models/data/attachmentData.ts b/src/models/data/attachmentData.ts deleted file mode 100644 index 2ff3ab423c..0000000000 --- a/src/models/data/attachmentData.ts +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index f0b9f63f2e..0000000000 --- a/src/models/data/cardData.ts +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 413612122f..0000000000 --- a/src/models/data/cipherData.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { CipherType } from '../../enums/cipherType.enum'; - -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 deleted file mode 100644 index f2d5fc9f03..0000000000 --- a/src/models/data/collectionData.ts +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 4914bb6ef9..0000000000 --- a/src/models/data/fieldData.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { FieldType } from '../../enums/fieldType.enum'; - -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 deleted file mode 100644 index 6f03781cc7..0000000000 --- a/src/models/data/folderData.ts +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index ee849166ee..0000000000 --- a/src/models/data/identityData.ts +++ /dev/null @@ -1,44 +0,0 @@ -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/loginData.ts b/src/models/data/loginData.ts deleted file mode 100644 index de0aecc133..0000000000 --- a/src/models/data/loginData.ts +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index ccfc9bd614..0000000000 --- a/src/models/data/secureNoteData.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { SecureNoteType } from '../../enums/secureNoteType.enum'; - -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 deleted file mode 100644 index d77152b007..0000000000 --- a/src/models/domain/attachment.ts +++ /dev/null @@ -1,43 +0,0 @@ -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/autofillField.ts b/src/models/domain/autofillField.ts deleted file mode 100644 index dfa6cdc770..0000000000 --- a/src/models/domain/autofillField.ts +++ /dev/null @@ -1,22 +0,0 @@ -export default class AutofillField { - opid: string; - elementNumber: number; - visible: boolean; - viewable: boolean; - htmlID: string; - htmlName: string; - htmlClass: string; - 'label-left': string; - 'label-right': string; - 'label-top': string; - 'label-tag': string; - placeholder: string; - type: string; - value: string; - disabled: boolean; - readonly: boolean; - onePasswordFieldType: string; - form: string; - autoCompleteType: string; - selectInfo: any; -} diff --git a/src/models/domain/autofillForm.ts b/src/models/domain/autofillForm.ts deleted file mode 100644 index 2d7fc4800b..0000000000 --- a/src/models/domain/autofillForm.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default class AutofillForm { - opid: string; - htmlName: string; - htmlID: string; - htmlAction: string; - htmlMethod: string; -} diff --git a/src/models/domain/autofillPageDetails.ts b/src/models/domain/autofillPageDetails.ts deleted file mode 100644 index 70f922bbe6..0000000000 --- a/src/models/domain/autofillPageDetails.ts +++ /dev/null @@ -1,13 +0,0 @@ -import AutofillField from './autofillField'; -import AutofillForm from './autofillForm'; - -export default class AutofillPageDetails { - documentUUID: string; - title: string; - url: string; - documentUrl: string; - tabUrl: string; - forms: { [id: string]: AutofillForm; }; - fields: AutofillField[]; - collectedTimestamp: number; -} diff --git a/src/models/domain/autofillScript.ts b/src/models/domain/autofillScript.ts deleted file mode 100644 index 875e620ea8..0000000000 --- a/src/models/domain/autofillScript.ts +++ /dev/null @@ -1,12 +0,0 @@ -export default class AutofillScript { - script: string[][] = []; - documentUUID: any = {}; - properties: any = {}; - options: any = {}; - metadata: any = {}; - autosubmit: any = null; - - constructor(documentUUID: string) { - this.documentUUID = documentUUID; - } -} diff --git a/src/models/domain/card.ts b/src/models/domain/card.ts deleted file mode 100644 index 3e42f7affb..0000000000 --- a/src/models/domain/card.ts +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index 5d51c82828..0000000000 --- a/src/models/domain/cipher.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { CipherType } from '../../enums/cipherType.enum'; - -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'; - -import { UtilsService } from '../../services/abstractions/utils.service'; - -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[]; - - private utilsService: UtilsService; - - 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) { - if (this.utilsService == null) { - this.utilsService = chrome.extension.getBackgroundPage() - .bitwardenMain.utilsService as UtilsService; - } - - model.login.domain = this.utilsService.getDomain(model.login.uri); - } - 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 deleted file mode 100644 index 61aec8ca6f..0000000000 --- a/src/models/domain/cipherString.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { EncryptionType } from '../../enums/encryptionType.enum'; -import { CryptoService } from '../../services/abstractions/crypto.service'; - -class CipherString { - encryptedString?: string; - encryptionType?: EncryptionType; - decryptedValue?: string; - cipherText?: string; - initializationVector?: string; - mac?: string; - - private cryptoService: CryptoService; - - 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) { - if (this.decryptedValue) { - return Promise.resolve(this.decryptedValue); - } - - const self = this; - if (this.cryptoService == null) { - this.cryptoService = chrome.extension.getBackgroundPage() - .bitwardenMain.cryptoService as CryptoService; - } - - return this.cryptoService.getOrgKey(orgId).then((orgKey: any) => { - return self.cryptoService.decrypt(self, orgKey); - }).then((decValue: any) => { - self.decryptedValue = decValue; - return self.decryptedValue; - }).catch(() => { - self.decryptedValue = '[error: cannot decrypt]'; - return self.decryptedValue; - }); - } -} - -export { CipherString }; -(window as any).CipherString = CipherString; diff --git a/src/models/domain/collection.ts b/src/models/domain/collection.ts deleted file mode 100644 index 0a5079e53a..0000000000 --- a/src/models/domain/collection.ts +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index cdb220776b..0000000000 --- a/src/models/domain/domain.ts +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index 668c30e262..0000000000 --- a/src/models/domain/encryptedObject.ts +++ /dev/null @@ -1,8 +0,0 @@ -import SymmetricCryptoKey from './symmetricCryptoKey'; - -export default class EncryptedObject { - iv: Uint8Array; - ct: Uint8Array; - mac: Uint8Array; - key: SymmetricCryptoKey; -} diff --git a/src/models/domain/environmentUrls.ts b/src/models/domain/environmentUrls.ts deleted file mode 100644 index 0eec60a114..0000000000 --- a/src/models/domain/environmentUrls.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default class EnvironmentUrls { - base: string; - api: string; - identity: string; -} diff --git a/src/models/domain/field.ts b/src/models/domain/field.ts deleted file mode 100644 index decc78664f..0000000000 --- a/src/models/domain/field.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { FieldType } from '../../enums/fieldType.enum'; - -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 deleted file mode 100644 index 180cd44e4f..0000000000 --- a/src/models/domain/folder.ts +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index ede933ac30..0000000000 --- a/src/models/domain/identity.ts +++ /dev/null @@ -1,79 +0,0 @@ -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/login.ts b/src/models/domain/login.ts deleted file mode 100644 index 8ed1e1f7c7..0000000000 --- a/src/models/domain/login.ts +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index fc4eb56688..0000000000 --- a/src/models/domain/passwordHistory.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default 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 deleted file mode 100644 index 2c742f8ea8..0000000000 --- a/src/models/domain/secureNote.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { SecureNoteType } from '../../enums/secureNoteType.enum'; - -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 deleted file mode 100644 index bf50795a02..0000000000 --- a/src/models/domain/symmetricCryptoKey.ts +++ /dev/null @@ -1,80 +0,0 @@ -import * as forge from 'node-forge'; - -import { EncryptionType } from '../../enums/encryptionType.enum'; - -import SymmetricCryptoKeyBuffers from './symmetricCryptoKeyBuffers'; - -import UtilsService from '../../services/utils.service'; - -export default 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 deleted file mode 100644 index 5a378ad246..0000000000 --- a/src/models/domain/symmetricCryptoKeyBuffers.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default 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 deleted file mode 100644 index 659ee5be2d..0000000000 --- a/src/models/request/cipherRequest.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { CipherType } from '../../enums/cipherType.enum'; - -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 deleted file mode 100644 index 928a46557f..0000000000 --- a/src/models/request/deviceRequest.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { BrowserType } from '../../enums/browserType.enum'; -import { UtilsService } from '../../services/abstractions/utils.service'; - -class DeviceRequest { - type: BrowserType; - name: string; - identifier: string; - pushToken?: string; - - constructor(appId: string, utilsService: UtilsService) { - this.type = utilsService.getBrowser(); - this.name = utilsService.getBrowserString(); - 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 deleted file mode 100644 index 69ef20bb9a..0000000000 --- a/src/models/request/deviceTokenRequest.ts +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 7ff9794b18..0000000000 --- a/src/models/request/folderRequest.ts +++ /dev/null @@ -1,12 +0,0 @@ -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/passwordHintRequest.ts b/src/models/request/passwordHintRequest.ts deleted file mode 100644 index 4feb92944b..0000000000 --- a/src/models/request/passwordHintRequest.ts +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 4b80fb7884..0000000000 --- a/src/models/request/registerRequest.ts +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 4cf7564615..0000000000 --- a/src/models/request/tokenRequest.ts +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index d540b08ecf..0000000000 --- a/src/models/request/twoFactorEmailRequest.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index df5138650c..0000000000 --- a/src/models/response/attachmentResponse.ts +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index b2e597e23a..0000000000 --- a/src/models/response/cipherResponse.ts +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index 8b0247720d..0000000000 --- a/src/models/response/collectionResponse.ts +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 63f69fe3b3..0000000000 --- a/src/models/response/deviceResponse.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { BrowserType } from '../../enums/browserType.enum'; - -class DeviceResponse { - id: string; - name: number; - identifier: string; - type: BrowserType; - 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 deleted file mode 100644 index d65077147b..0000000000 --- a/src/models/response/domainsResponse.ts +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 4d976932cd..0000000000 --- a/src/models/response/errorResponse.ts +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index c5ff0ada70..0000000000 --- a/src/models/response/folderResponse.ts +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 8e9a45df11..0000000000 --- a/src/models/response/globalDomainResponse.ts +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 2d188707c0..0000000000 --- a/src/models/response/identityTokenResponse.ts +++ /dev/null @@ -1,24 +0,0 @@ -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/keysResponse.ts b/src/models/response/keysResponse.ts deleted file mode 100644 index cb96dad51e..0000000000 --- a/src/models/response/keysResponse.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 9cf5455ada..0000000000 --- a/src/models/response/listResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 484857745a..0000000000 --- a/src/models/response/profileOrganizationResponse.ts +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 69e4f3e9cb..0000000000 --- a/src/models/response/profileResponse.ts +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index 8acdc5202d..0000000000 --- a/src/models/response/syncResponse.ts +++ /dev/null @@ -1,44 +0,0 @@ -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/abstractions/crypto.service.ts b/src/services/abstractions/crypto.service.ts deleted file mode 100644 index 1fd413d34f..0000000000 --- a/src/services/abstractions/crypto.service.ts +++ /dev/null @@ -1,28 +0,0 @@ -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/services/abstractions/utils.service.ts b/src/services/abstractions/utils.service.ts deleted file mode 100644 index e52eb44fec..0000000000 --- a/src/services/abstractions/utils.service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { BrowserType } from '../../enums/browserType.enum'; - -export interface UtilsService { - getBrowser(): BrowserType; - getBrowserString(): string; - isFirefox(): boolean; - isChrome(): boolean; - isEdge(): boolean; - isOpera(): boolean; - analyticsId(): string; - initListSectionItemListeners(doc: Document, angular: any): void; - copyToClipboard(text: string, doc?: Document): void; - getDomain(uriString: string): string; - getHostname(uriString: string): string; - inSidebar(theWindow: Window): boolean; - inTab(theWindow: Window): boolean; - inPopout(theWindow: Window): boolean; - inPopup(theWindow: Window): boolean; - saveObjToStorage(key: string, obj: any): Promise; - removeFromStorage(key: string): Promise; - getObjFromStorage(key: string): Promise; -} diff --git a/src/services/api.service.ts b/src/services/api.service.ts deleted file mode 100644 index d97ec6c47a..0000000000 --- a/src/services/api.service.ts +++ /dev/null @@ -1,454 +0,0 @@ -import AppIdService from './appId.service'; -import ConstantsService from './constants.service'; -import TokenService from './token.service'; -import UtilsService from './utils.service'; - -import EnvironmentUrls from '../models/domain/environmentUrls'; - -import { CipherRequest } from '../models/request/cipherRequest'; -import { DeviceRequest } from '../models/request/deviceRequest'; -import { DeviceTokenRequest } from '../models/request/deviceTokenRequest'; -import { FolderRequest } from '../models/request/folderRequest'; -import { PasswordHintRequest } from '../models/request/passwordHintRequest'; -import { RegisterRequest } from '../models/request/registerRequest'; -import { TokenRequest } from '../models/request/tokenRequest'; -import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; - -import { AttachmentResponse } from '../models/response/attachmentResponse'; -import { CipherResponse } from '../models/response/cipherResponse'; -import { DeviceResponse } from '../models/response/deviceResponse'; -import { DomainsResponse } from '../models/response/domainsResponse'; -import { ErrorResponse } from '../models/response/errorResponse'; -import { FolderResponse } from '../models/response/folderResponse'; -import { GlobalDomainResponse } from '../models/response/globalDomainResponse'; -import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; -import { KeysResponse } from '../models/response/keysResponse'; -import { ListResponse } from '../models/response/listResponse'; -import { ProfileOrganizationResponse } from '../models/response/profileOrganizationResponse'; -import { ProfileResponse } from '../models/response/profileResponse'; -import { SyncResponse } from '../models/response/syncResponse'; - -export default class ApiService { - urlsSet: boolean = false; - baseUrl: string; - identityBaseUrl: string; - logoutCallback: Function; - - constructor(private tokenService: TokenService, private utilsService: UtilsService, - logoutCallback: Function) { - this.logoutCallback = logoutCallback; - } - - setUrls(urls: EnvironmentUrls) { - this.urlsSet = true; - - if (urls.base != null) { - this.baseUrl = urls.base + '/api'; - this.identityBaseUrl = urls.base + '/identity'; - return; - } - - if (urls.api != null && urls.identity != null) { - this.baseUrl = urls.api; - this.identityBaseUrl = urls.identity; - return; - } - - /* tslint:disable */ - // Desktop - //this.baseUrl = 'http://localhost:4000'; - //this.identityBaseUrl = 'http://localhost:33656'; - - // Desktop HTTPS - //this.baseUrl = 'https://localhost:44377'; - //this.identityBaseUrl = 'https://localhost:44392'; - - // Desktop external - //this.baseUrl = 'http://192.168.1.3:4000'; - //this.identityBaseUrl = 'http://192.168.1.3:33656'; - - // Preview - //this.baseUrl = 'https://preview-api.bitwarden.com'; - //this.identityBaseUrl = 'https://preview-identity.bitwarden.com'; - - // Production - this.baseUrl = 'https://api.bitwarden.com'; - this.identityBaseUrl = 'https://identity.bitwarden.com'; - /* tslint:enable */ - } - - // Auth APIs - - async postIdentityToken(request: TokenRequest): Promise { - const response = await fetch(new Request(this.identityBaseUrl + '/connect/token', { - body: this.qsStringify(request.toIdentityToken()), - cache: 'no-cache', - headers: new Headers({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', - 'Accept': 'application/json', - 'Device-Type': this.utilsService.getBrowser().toString(), - }), - method: 'POST', - })); - - let responseJson: any = null; - const typeHeader = response.headers.get('content-type'); - if (typeHeader != null && typeHeader.indexOf('application/json') > -1) { - responseJson = await response.json(); - } - - if (responseJson != null) { - if (response.status === 200) { - return new IdentityTokenResponse(responseJson); - } else if (response.status === 400 && responseJson.TwoFactorProviders2 && - Object.keys(responseJson.TwoFactorProviders2).length) { - await this.tokenService.clearTwoFactorToken(request.email); - return responseJson.TwoFactorProviders2; - } - } - - return Promise.reject(new ErrorResponse(responseJson, response.status, true)); - } - - async refreshIdentityToken(): Promise { - try { - await this.doRefreshToken(); - } catch (e) { - return Promise.reject(null); - } - } - - // Two Factor APIs - - async postTwoFactorEmail(request: TwoFactorEmailRequest): Promise { - const response = await fetch(new Request(this.baseUrl + '/two-factor/send-email-login', { - body: JSON.stringify(request), - cache: 'no-cache', - headers: new Headers({ - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.utilsService.getBrowser().toString(), - }), - method: 'POST', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - // Account APIs - - async getAccountRevisionDate(): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/accounts/revision-date', { - cache: 'no-cache', - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Device-Type': this.utilsService.getBrowser().toString(), - }), - })); - - if (response.status === 200) { - return (await response.json() as number); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - async postPasswordHint(request: PasswordHintRequest): Promise { - const response = await fetch(new Request(this.baseUrl + '/accounts/password-hint', { - body: JSON.stringify(request), - cache: 'no-cache', - headers: new Headers({ - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.utilsService.getBrowser().toString(), - }), - method: 'POST', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - async postRegister(request: RegisterRequest): Promise { - const response = await fetch(new Request(this.baseUrl + '/accounts/register', { - body: JSON.stringify(request), - cache: 'no-cache', - headers: new Headers({ - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.utilsService.getBrowser().toString(), - }), - method: 'POST', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - // Folder APIs - - async postFolder(request: FolderRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/folders', { - body: JSON.stringify(request), - cache: 'no-cache', - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.utilsService.getBrowser().toString(), - }), - method: 'POST', - })); - - if (response.status === 200) { - const responseJson = await response.json(); - return new FolderResponse(responseJson); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - async putFolder(id: string, request: FolderRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/folders/' + id, { - body: JSON.stringify(request), - cache: 'no-cache', - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.utilsService.getBrowser().toString(), - }), - method: 'PUT', - })); - - if (response.status === 200) { - const responseJson = await response.json(); - return new FolderResponse(responseJson); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - async deleteFolder(id: string): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/folders/' + id, { - cache: 'no-cache', - headers: new Headers({ - 'Authorization': authHeader, - 'Device-Type': this.utilsService.getBrowser().toString(), - }), - method: 'DELETE', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - // Cipher APIs - - async postCipher(request: CipherRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers', { - body: JSON.stringify(request), - cache: 'no-cache', - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.utilsService.getBrowser().toString(), - }), - method: 'POST', - })); - - if (response.status === 200) { - const responseJson = await response.json(); - return new CipherResponse(responseJson); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - async putCipher(id: string, request: CipherRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { - body: JSON.stringify(request), - cache: 'no-cache', - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.utilsService.getBrowser().toString(), - }), - method: 'PUT', - })); - - if (response.status === 200) { - const responseJson = await response.json(); - return new CipherResponse(responseJson); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - async deleteCipher(id: string): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { - cache: 'no-cache', - headers: new Headers({ - 'Authorization': authHeader, - 'Device-Type': this.utilsService.getBrowser().toString(), - }), - method: 'DELETE', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - // Attachments APIs - - async postCipherAttachment(id: string, data: FormData): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment', { - body: data, - cache: 'no-cache', - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Device-Type': this.utilsService.getBrowser().toString(), - }), - method: 'POST', - })); - - if (response.status === 200) { - const responseJson = await response.json(); - return new CipherResponse(responseJson); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - async deleteCipherAttachment(id: string, attachmentId: string): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment/' + attachmentId, { - cache: 'no-cache', - headers: new Headers({ - 'Authorization': authHeader, - 'Device-Type': this.utilsService.getBrowser().toString(), - }), - method: 'DELETE', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - // Sync APIs - - async getSync(): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/sync', { - cache: 'no-cache', - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Device-Type': this.utilsService.getBrowser().toString(), - }), - })); - - if (response.status === 200) { - const responseJson = await response.json(); - return new SyncResponse(responseJson); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - // Helpers - - private async handleError(response: Response, tokenError: boolean): Promise { - if ((tokenError && response.status === 400) || response.status === 401 || response.status === 403) { - this.logoutCallback(true); - return null; - } - - let responseJson: any = null; - const typeHeader = response.headers.get('content-type'); - if (typeHeader != null && typeHeader.indexOf('application/json') > -1) { - responseJson = await response.json(); - } - - return new ErrorResponse(responseJson, response.status, tokenError); - } - - private async handleTokenState(): Promise { - let accessToken: string; - if (this.tokenService.tokenNeedsRefresh()) { - const tokenResponse = await this.doRefreshToken(); - accessToken = tokenResponse.accessToken; - } else { - accessToken = await this.tokenService.getToken(); - } - - return 'Bearer ' + accessToken; - } - - private async doRefreshToken(): Promise { - const refreshToken = await this.tokenService.getRefreshToken(); - if (refreshToken == null || refreshToken === '') { - throw new Error(); - } - - const response = await fetch(new Request(this.identityBaseUrl + '/connect/token', { - body: this.qsStringify({ - grant_type: 'refresh_token', - client_id: 'browser', - refresh_token: refreshToken, - }), - cache: 'no-cache', - headers: new Headers({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', - 'Accept': 'application/json', - 'Device-Type': this.utilsService.getBrowser().toString(), - }), - method: 'POST', - })); - - if (response.status === 200) { - const responseJson = await response.json(); - const tokenResponse = new IdentityTokenResponse(responseJson); - await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken); - return tokenResponse; - } else { - const error = await this.handleError(response, true); - return Promise.reject(error); - } - } - - private qsStringify(params: any): string { - return Object.keys(params).map((key) => { - return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); - }).join('&'); - } -} diff --git a/src/services/appId.service.ts b/src/services/appId.service.ts deleted file mode 100644 index d4e0dce5cb..0000000000 --- a/src/services/appId.service.ts +++ /dev/null @@ -1,31 +0,0 @@ -import UtilsService from './utils.service'; - -export default class AppIdService { - static getAppId(): Promise { - return AppIdService.makeAndGetAppId('appId'); - } - - static getAnonymousAppId(): Promise { - return AppIdService.makeAndGetAppId('anonymousAppId'); - } - - private static async makeAndGetAppId(key: string) { - const existingId = await UtilsService.getObjFromStorage(key); - if (existingId != null) { - return existingId; - } - - const guid = UtilsService.newGuid(); - await UtilsService.saveObjToStorage(key, guid); - return guid; - } - - // TODO: remove these in favor of static methods - getAppId(): Promise { - return AppIdService.getAppId(); - } - - getAnonymousAppId(): Promise { - return AppIdService.getAnonymousAppId(); - } -} diff --git a/src/services/autofill.service.ts b/src/services/autofill.service.ts deleted file mode 100644 index 2b2593bffe..0000000000 --- a/src/services/autofill.service.ts +++ /dev/null @@ -1,872 +0,0 @@ -import { CipherType } from '../enums/cipherType.enum'; -import { FieldType } from '../enums/fieldType.enum'; - -import AutofillField from '../models/domain/autofillField'; -import AutofillPageDetails from '../models/domain/autofillPageDetails'; -import AutofillScript from '../models/domain/autofillScript'; - -import CipherService from './cipher.service'; -import TokenService from './token.service'; -import TotpService from './totp.service'; -import UtilsService from './utils.service'; - -const CardAttributes: string[] = ['autoCompleteType', 'data-stripe', 'htmlName', 'htmlID', 'label-tag', - 'placeholder', 'label-left', 'label-top']; - -const IdentityAttributes: string[] = ['autoCompleteType', 'data-stripe', 'htmlName', 'htmlID', 'label-tag', - 'placeholder', 'label-left', 'label-top']; - -const UsernameFieldNames: string[] = [ - // English - 'username', 'user name', 'email', 'email address', 'e-mail', 'e-mail address', 'userid', 'user id', - // German - 'benutzername', 'benutzer name', 'email adresse', 'e-mail adresse', 'benutzerid', 'benutzer id']; - -/* tslint:disable */ -const IsoCountries: { [id: string]: string; } = { - afghanistan: "AF", "aland islands": "AX", albania: "AL", algeria: "DZ", "american samoa": "AS", andorra: "AD", - angola: "AO", anguilla: "AI", antarctica: "AQ", "antigua and barbuda": "AG", argentina: "AR", armenia: "AM", - aruba: "AW", australia: "AU", austria: "AT", azerbaijan: "AZ", bahamas: "BS", bahrain: "BH", bangladesh: "BD", - barbados: "BB", belarus: "BY", belgium: "BE", belize: "BZ", benin: "BJ", bermuda: "BM", bhutan: "BT", bolivia: "BO", - "bosnia and herzegovina": "BA", botswana: "BW", "bouvet island": "BV", brazil: "BR", - "british indian ocean territory": "IO", "brunei darussalam": "BN", bulgaria: "BG", "burkina faso": "BF", burundi: "BI", - cambodia: "KH", cameroon: "CM", canada: "CA", "cape verde": "CV", "cayman islands": "KY", - "central african republic": "CF", chad: "TD", chile: "CL", china: "CN", "christmas island": "CX", - "cocos (keeling) islands": "CC", colombia: "CO", comoros: "KM", congo: "CG", "congo, democratic republic": "CD", - "cook islands": "CK", "costa rica": "CR", "cote d'ivoire": "CI", croatia: "HR", cuba: "CU", cyprus: "CY", - "czech republic": "CZ", denmark: "DK", djibouti: "DJ", dominica: "DM", "dominican republic": "DO", ecuador: "EC", - egypt: "EG", "el salvador": "SV", "equatorial guinea": "GQ", eritrea: "ER", estonia: "EE", ethiopia: "ET", - "falkland islands": "FK", "faroe islands": "FO", fiji: "FJ", finland: "FI", france: "FR", "french guiana": "GF", - "french polynesia": "PF", "french southern territories": "TF", gabon: "GA", gambia: "GM", georgia: "GE", germany: "DE", - ghana: "GH", gibraltar: "GI", greece: "GR", greenland: "GL", grenada: "GD", guadeloupe: "GP", guam: "GU", - guatemala: "GT", guernsey: "GG", guinea: "GN", "guinea-bissau": "GW", guyana: "GY", haiti: "HT", - "heard island & mcdonald islands": "HM", "holy see (vatican city state)": "VA", honduras: "HN", "hong kong": "HK", - hungary: "HU", iceland: "IS", india: "IN", indonesia: "ID", "iran, islamic republic of": "IR", iraq: "IQ", - ireland: "IE", "isle of man": "IM", israel: "IL", italy: "IT", jamaica: "JM", japan: "JP", jersey: "JE", - jordan: "JO", kazakhstan: "KZ", kenya: "KE", kiribati: "KI", "republic of korea": "KR", "south korea": "KR", - "democratic people's republic of korea": "KP", "north korea": "KP", kuwait: "KW", kyrgyzstan: "KG", - "lao people's democratic republic": "LA", latvia: "LV", lebanon: "LB", lesotho: "LS", liberia: "LR", - "libyan arab jamahiriya": "LY", liechtenstein: "LI", lithuania: "LT", luxembourg: "LU", macao: "MO", macedonia: "MK", - madagascar: "MG", malawi: "MW", malaysia: "MY", maldives: "MV", mali: "ML", malta: "MT", "marshall islands": "MH", - martinique: "MQ", mauritania: "MR", mauritius: "MU", mayotte: "YT", mexico: "MX", - "micronesia, federated states of": "FM", moldova: "MD", monaco: "MC", mongolia: "MN", montenegro: "ME", montserrat: "MS", - morocco: "MA", mozambique: "MZ", myanmar: "MM", namibia: "NA", nauru: "NR", nepal: "NP", netherlands: "NL", - "netherlands antilles": "AN", "new caledonia": "NC", "new zealand": "NZ", nicaragua: "NI", niger: "NE", nigeria: "NG", - niue: "NU", "norfolk island": "NF", "northern mariana islands": "MP", norway: "NO", oman: "OM", pakistan: "PK", - palau: "PW", "palestinian territory, occupied": "PS", panama: "PA", "papua new guinea": "PG", paraguay: "PY", peru: "PE", - philippines: "PH", pitcairn: "PN", poland: "PL", portugal: "PT", "puerto rico": "PR", qatar: "QA", reunion: "RE", - romania: "RO", "russian federation": "RU", rwanda: "RW", "saint barthelemy": "BL", "saint helena": "SH", - "saint kitts and nevis": "KN", "saint lucia": "LC", "saint martin": "MF", "saint pierre and miquelon": "PM", - "saint vincent and grenadines": "VC", samoa: "WS", "san marino": "SM", "sao tome and principe": "ST", - "saudi arabia": "SA", senegal: "SN", serbia: "RS", seychelles: "SC", "sierra leone": "SL", singapore: "SG", - slovakia: "SK", slovenia: "SI", "solomon islands": "SB", somalia: "SO", "south africa": "ZA", - "south georgia and sandwich isl.": "GS", spain: "ES", "sri lanka": "LK", sudan: "SD", suriname: "SR", - "svalbard and jan mayen": "SJ", swaziland: "SZ", sweden: "SE", switzerland: "CH", "syrian arab republic": "SY", - taiwan: "TW", tajikistan: "TJ", tanzania: "TZ", thailand: "TH", "timor-leste": "TL", togo: "TG", tokelau: "TK", - tonga: "TO", "trinidad and tobago": "TT", tunisia: "TN", turkey: "TR", turkmenistan: "TM", - "turks and caicos islands": "TC", tuvalu: "TV", uganda: "UG", ukraine: "UA", "united arab emirates": "AE", - "united kingdom": "GB", "united states": "US", "united states outlying islands": "UM", uruguay: "UY", - uzbekistan: "UZ", vanuatu: "VU", venezuela: "VE", vietnam: "VN", "virgin islands, british": "VG", - "virgin islands, u.s.": "VI", "wallis and futuna": "WF", "western sahara": "EH", yemen: "YE", zambia: "ZM", - zimbabwe: "ZW", -}; - -const IsoStates: { [id: string]: string; } = { - alabama: 'AL', alaska: 'AK', 'american samoa': 'AS', arizona: 'AZ', arkansas: 'AR', california: 'CA', colorado: 'CO', - connecticut: 'CT', delaware: 'DE', 'district of columbia': 'DC', 'federated states of micronesia': 'FM', florida: 'FL', - georgia: 'GA', guam: 'GU', hawaii: 'HI', idaho: 'ID', illinois: 'IL', indiana: 'IN', iowa: 'IA', kansas: 'KS', - kentucky: 'KY', louisiana: 'LA', maine: 'ME', 'marshall islands': 'MH', maryland: 'MD', massachusetts: 'MA', - michigan: 'MI', minnesota: 'MN', mississippi: 'MS', missouri: 'MO', montana: 'MT', nebraska: 'NE', nevada: 'NV', - 'new hampshire': 'NH', 'new jersey': 'NJ', 'new mexico': 'NM', 'new york': 'NY', 'north carolina': 'NC', - 'north dakota': 'ND', 'northern mariana islands': 'MP', ohio: 'OH', oklahoma: 'OK', oregon: 'OR', palau: 'PW', - pennsylvania: 'PA', 'puerto rico': 'PR', 'rhode island': 'RI', 'south carolina': 'SC', 'south dakota': 'SD', - tennessee: 'TN', texas: 'TX', utah: 'UT', vermont: 'VT', 'virgin islands': 'VI', virginia: 'VA', washington: 'WA', - 'west virginia': 'WV', wisconsin: 'WI', wyoming: 'WY', -}; - -var IsoProvinces: { [id: string]: string; } = { - alberta: 'AB', 'british columbia': 'BC', manitoba: 'MB', 'new brunswick': 'NB', 'newfoundland and labrador': 'NL', - 'nova scotia': 'NS', ontario: 'ON', 'prince edward island': 'PE', quebec: 'QC', saskatchewan: 'SK', -}; -/* tslint:enable */ - -export default class AutofillService { - constructor(public cipherService: CipherService, public tokenService: TokenService, - public totpService: TotpService, public utilsService: UtilsService) { - } - - getFormsWithPasswordFields(pageDetails: AutofillPageDetails): any[] { - const formData: any[] = []; - - const passwordFields = this.loadPasswordFields(pageDetails, true); - if (passwordFields.length === 0) { - return formData; - } - - for (const formKey in pageDetails.forms) { - if (!pageDetails.forms.hasOwnProperty(formKey)) { - continue; - } - - for (let i = 0; i < passwordFields.length; i++) { - const pf = passwordFields[i]; - if (formKey !== pf.form) { - continue; - } - - let uf = this.findUsernameField(pageDetails, pf, false, false); - if (uf == null) { - // not able to find any viewable username fields. maybe there are some "hidden" ones? - uf = this.findUsernameField(pageDetails, pf, true, false); - } - - formData.push({ - form: pageDetails.forms[formKey], - password: pf, - username: uf, - }); - break; - } - } - - return formData; - } - - async doAutoFill(options: any) { - let totpPromise: Promise = null; - const tab = await this.getActiveTab(); - if (!tab || !options.cipher || !options.pageDetails || !options.pageDetails.length) { - throw new Error('Nothing to auto-fill.'); - } - - let didAutofill = false; - options.pageDetails.forEach((pd: any) => { - // make sure we're still on correct tab - if (pd.tab.id !== tab.id || pd.tab.url !== tab.url) { - return; - } - - const fillScript = this.generateFillScript(pd.details, { - skipUsernameOnlyFill: options.skipUsernameOnlyFill || false, - cipher: options.cipher, - }); - - if (!fillScript || !fillScript.script || !fillScript.script.length) { - return; - } - - didAutofill = true; - if (!options.skipLastUsed) { - this.cipherService.updateLastUsedDate(options.cipher.id); - } - - chrome.tabs.sendMessage(tab.id, { - command: 'fillForm', - // tslint:disable-next-line - fillScript: fillScript, - }, { frameId: pd.frameId }); - - if (options.cipher.type !== CipherType.Login || totpPromise || - (options.fromBackground && this.utilsService.isFirefox()) || options.skipTotp || - !options.cipher.login.totp || !this.tokenService.getPremium()) { - return; - } - - totpPromise = this.totpService.isAutoCopyEnabled().then((enabled) => { - if (enabled) { - return this.totpService.getCode(options.cipher.login.totp); - } - - return null; - }).then((code: string) => { - if (code) { - UtilsService.copyToClipboard(code); - } - - return code; - }); - }); - - if (didAutofill) { - if (totpPromise != null) { - const totpCode = await totpPromise; - return totpCode; - } else { - return null; - } - } else { - throw new Error('Did not auto-fill.'); - } - } - - async doAutoFillForLastUsedLogin(pageDetails: any, fromCommand: boolean) { - const tab = await this.getActiveTab(); - if (!tab || !tab.url) { - return; - } - - const tabDomain = UtilsService.getDomain(tab.url); - if (tabDomain == null) { - return; - } - - const lastUsedCipher = await this.cipherService.getLastUsedForDomain(tabDomain); - if (!lastUsedCipher) { - return; - } - - await this.doAutoFill({ - cipher: lastUsedCipher, - // tslint:disable-next-line - pageDetails: pageDetails, - fromBackground: true, - skipTotp: !fromCommand, - skipLastUsed: true, - skipUsernameOnlyFill: !fromCommand, - }); - } - - // Helpers - - private getActiveTab(): Promise { - return new Promise((resolve, reject) => { - chrome.tabs.query({ active: true, currentWindow: true }, (tabs: any[]) => { - if (tabs.length === 0) { - reject('No tab found.'); - } else { - resolve(tabs[0]); - } - }); - }); - } - - private generateFillScript(pageDetails: AutofillPageDetails, options: any): AutofillScript { - if (!pageDetails || !options.cipher) { - return null; - } - - let fillScript = new AutofillScript(pageDetails.documentUUID); - const filledFields: { [id: string]: AutofillField; } = {}; - const fields = options.cipher.fields; - - if (fields && fields.length) { - const fieldNames: string[] = []; - - fields.forEach((f: any) => { - if (this.hasValue(f.name)) { - fieldNames.push(f.name.toLowerCase()); - } else { - fieldNames.push(null); - } - }); - - pageDetails.fields.forEach((field: any) => { - if (filledFields.hasOwnProperty(field.opid) || !field.viewable) { - return; - } - - const matchingIndex = this.findMatchingFieldIndex(field, fieldNames); - if (matchingIndex > -1) { - let val = fields[matchingIndex].value; - if (val == null && fields[matchingIndex].type === FieldType.Boolean) { - val = 'false'; - } - - filledFields[field.opid] = field; - fillScript.script.push(['click_on_opid', field.opid]); - fillScript.script.push(['fill_by_opid', field.opid, val]); - } - }); - } - - switch (options.cipher.type) { - case CipherType.Login: - fillScript = this.generateLoginFillScript(fillScript, pageDetails, filledFields, options); - break; - case CipherType.Card: - fillScript = this.generateCardFillScript(fillScript, pageDetails, filledFields, options); - break; - case CipherType.Identity: - fillScript = this.generateIdentityFillScript(fillScript, pageDetails, filledFields, options); - break; - default: - return null; - } - - return fillScript; - } - - private generateLoginFillScript(fillScript: AutofillScript, pageDetails: any, - filledFields: { [id: string]: AutofillField; }, options: any): AutofillScript { - if (!options.cipher.login) { - return null; - } - - const passwords: AutofillField[] = []; - const usernames: AutofillField[] = []; - let pf: AutofillField = null; - let username: AutofillField = null; - const login = options.cipher.login; - - if (!login.password || login.password === '') { - // No password for this login. Maybe they just wanted to auto-fill some custom fields? - fillScript = this.setFillScriptForFocus(filledFields, fillScript); - return fillScript; - } - - let passwordFields = this.loadPasswordFields(pageDetails, false); - if (!passwordFields.length) { - // not able to find any viewable password fields. maybe there are some "hidden" ones? - passwordFields = this.loadPasswordFields(pageDetails, true); - } - - for (const formKey in pageDetails.forms) { - if (!pageDetails.forms.hasOwnProperty(formKey)) { - continue; - } - - const passwordFieldsForForm: AutofillField[] = []; - passwordFields.forEach((passField) => { - if (formKey === passField.form) { - passwordFieldsForForm.push(passField); - } - }); - - passwordFields.forEach((passField) => { - pf = passField; - passwords.push(pf); - - if (login.username) { - username = this.findUsernameField(pageDetails, pf, false, false); - - if (!username) { - // not able to find any viewable username fields. maybe there are some "hidden" ones? - username = this.findUsernameField(pageDetails, pf, true, false); - } - - if (username) { - usernames.push(username); - } - } - }); - } - - if (passwordFields.length && !passwords.length) { - // The page does not have any forms with password fields. Use the first password field on the page and the - // input field just before it as the username. - - pf = passwordFields[0]; - passwords.push(pf); - - if (login.username && pf.elementNumber > 0) { - username = this.findUsernameField(pageDetails, pf, false, true); - - if (!username) { - // not able to find any viewable username fields. maybe there are some "hidden" ones? - username = this.findUsernameField(pageDetails, pf, true, true); - } - - if (username) { - usernames.push(username); - } - } - } - - if (!passwordFields.length && !options.skipUsernameOnlyFill) { - // No password fields on this page. Let's try to just fuzzy fill the username. - pageDetails.fields.forEach((f: any) => { - if (f.viewable && (f.type === 'text' || f.type === 'email' || f.type === 'tel') && - this.fieldIsFuzzyMatch(f, UsernameFieldNames)) { - usernames.push(f); - } - }); - } - - usernames.forEach((u) => { - if (filledFields.hasOwnProperty(u.opid)) { - return; - } - - filledFields[u.opid] = u; - fillScript.script.push(['click_on_opid', u.opid]); - fillScript.script.push(['fill_by_opid', u.opid, login.username]); - }); - - passwords.forEach((p) => { - if (filledFields.hasOwnProperty(p.opid)) { - return; - } - - filledFields[p.opid] = p; - fillScript.script.push(['click_on_opid', p.opid]); - fillScript.script.push(['fill_by_opid', p.opid, login.password]); - }); - - fillScript = this.setFillScriptForFocus(filledFields, fillScript); - return fillScript; - } - - private generateCardFillScript(fillScript: AutofillScript, pageDetails: any, - filledFields: { [id: string]: AutofillField; }, options: any): AutofillScript { - if (!options.cipher.card) { - return null; - } - - const fillFields: { [id: string]: AutofillField; } = {}; - - pageDetails.fields.forEach((f: any) => { - CardAttributes.forEach((attr) => { - if (!f.hasOwnProperty(attr) || !f[attr] || !f.viewable) { - return; - } - - // ref https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill - // ref https://developers.google.com/web/fundamentals/design-and-ux/input/forms/ - if (!fillFields.cardholderName && this.isFieldMatch(f[attr], - ['cc-name', 'card-name', 'cardholder-name', 'cardholder', 'name'], - ['cc-name', 'card-name', 'cardholder-name', 'cardholder'])) { - fillFields.cardholderName = f; - } else if (!fillFields.number && this.isFieldMatch(f[attr], - ['cc-number', 'cc-num', 'card-number', 'card-num', 'number'], - ['cc-number', 'cc-num', 'card-number', 'card-num'])) { - fillFields.number = f; - } else if (!fillFields.exp && this.isFieldMatch(f[attr], - ['cc-exp', 'card-exp', 'cc-expiration', 'card-expiration', 'cc-ex', 'card-ex'], - [])) { - fillFields.exp = f; - } else if (!fillFields.expMonth && this.isFieldMatch(f[attr], - ['exp-month', 'cc-exp-month', 'cc-month', 'card-month', 'cc-mo', 'card-mo', 'exp-mo', - 'card-exp-mo', 'cc-exp-mo', 'card-expiration-month', 'expiration-month', - 'cc-mm', 'card-mm', 'card-exp-mm', 'cc-exp-mm', 'exp-mm'])) { - fillFields.expMonth = f; - } else if (!fillFields.expYear && this.isFieldMatch(f[attr], - ['exp-year', 'cc-exp-year', 'cc-year', 'card-year', 'cc-yr', 'card-yr', 'exp-yr', - 'card-exp-yr', 'cc-exp-yr', 'card-expiration-year', 'expiration-year', - 'cc-yy', 'card-yy', 'card-exp-yy', 'cc-exp-yy', 'exp-yy', - 'cc-yyyy', 'card-yyyy', 'card-exp-yyyy', 'cc-exp-yyyy'])) { - fillFields.expYear = f; - } else if (!fillFields.code && this.isFieldMatch(f[attr], - ['cvv', 'cvc', 'cvv2', 'cc-csc', 'cc-cvv', 'card-csc', 'card-cvv', 'cvd', - 'cid', 'cvc2', 'cnv', 'cvn2', 'cc-code', 'card-code'])) { - fillFields.code = f; - } else if (!fillFields.brand && this.isFieldMatch(f[attr], - ['cc-type', 'card-type', 'card-brand', 'cc-brand'])) { - fillFields.brand = f; - } - }); - }); - - const card = options.cipher.card; - this.makeScriptAction(fillScript, card, fillFields, filledFields, 'cardholderName'); - this.makeScriptAction(fillScript, card, fillFields, filledFields, 'number'); - this.makeScriptAction(fillScript, card, fillFields, filledFields, 'expYear'); - this.makeScriptAction(fillScript, card, fillFields, filledFields, 'code'); - this.makeScriptAction(fillScript, card, fillFields, filledFields, 'brand'); - - if (fillFields.expMonth && this.hasValue(card.expMonth)) { - let expMonth = card.expMonth; - - if (fillFields.expMonth.selectInfo && fillFields.expMonth.selectInfo.options) { - let index: number = null; - if (fillFields.expMonth.selectInfo.options.length === 12) { - index = parseInt(card.expMonth, null) - 1; - } else if (fillFields.expMonth.selectInfo.options.length === 13) { - index = parseInt(card.expMonth, null); - } - - if (index != null) { - const option = fillFields.expMonth.selectInfo.options[index]; - if (option.length > 1) { - expMonth = option[1]; - } - } - } - - filledFields[fillFields.expMonth.opid] = fillFields.expMonth; - fillScript.script.push(['click_on_opid', fillFields.expMonth.opid]); - fillScript.script.push(['fill_by_opid', fillFields.expMonth.opid, expMonth]); - } - - if (fillFields.exp && this.hasValue(card.expMonth) && this.hasValue(card.expYear)) { - let year = card.expYear; - if (year.length === 2) { - year = '20' + year; - } - - const exp = year + '-' + ('0' + card.expMonth).slice(-2); - this.makeScriptActionWithValue(fillScript, exp, fillFields.exp, filledFields); - } - - return fillScript; - } - - private generateIdentityFillScript(fillScript: AutofillScript, pageDetails: any, - filledFields: { [id: string]: AutofillField; }, options: any): AutofillScript { - if (!options.cipher.identity) { - return null; - } - - const fillFields: { [id: string]: AutofillField; } = {}; - - pageDetails.fields.forEach((f: any) => { - IdentityAttributes.forEach((attr) => { - if (!f.hasOwnProperty(attr) || !f[attr] || !f.viewable) { - return; - } - - // ref https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill - // ref https://developers.google.com/web/fundamentals/design-and-ux/input/forms/ - if (!fillFields.name && this.isFieldMatch(f[attr], - ['name', 'full-name', 'your-name'], ['full-name', 'your-name'])) { - fillFields.name = f; - } else if (!fillFields.firstName && this.isFieldMatch(f[attr], - ['f-name', 'first-name', 'given-name', 'first-n'])) { - fillFields.firstName = f; - } else if (!fillFields.middleName && this.isFieldMatch(f[attr], - ['m-name', 'middle-name', 'additional-name', 'middle-initial', 'middle-n', 'middle-i'])) { - fillFields.middleName = f; - } else if (!fillFields.lastName && this.isFieldMatch(f[attr], - ['l-name', 'last-name', 's-name', 'surname', 'family-name', 'family-n', 'last-n'])) { - fillFields.lastName = f; - } else if (!fillFields.title && this.isFieldMatch(f[attr], - ['honorific-prefix', 'prefix', 'title'])) { - fillFields.title = f; - } else if (!fillFields.email && this.isFieldMatch(f[attr], - ['e-mail', 'email-address'])) { - fillFields.email = f; - } else if (!fillFields.address && this.isFieldMatch(f[attr], - ['address', 'street-address', 'addr'], [])) { - fillFields.address = f; - } else if (!fillFields.address1 && this.isFieldMatch(f[attr], - ['address-1', 'address-line-1', 'addr-1'])) { - fillFields.address1 = f; - } else if (!fillFields.address2 && this.isFieldMatch(f[attr], - ['address-2', 'address-line-2', 'addr-2'])) { - fillFields.address2 = f; - } else if (!fillFields.address3 && this.isFieldMatch(f[attr], - ['address-3', 'address-line-3', 'addr-3'])) { - fillFields.address3 = f; - } else if (!fillFields.postalCode && this.isFieldMatch(f[attr], - ['postal', 'zip', 'zip2', 'zip-code', 'postal-code', 'post-code', 'address-zip', - 'address-postal', 'address-code', 'address-postal-code', 'address-zip-code'])) { - fillFields.postalCode = f; - } else if (!fillFields.city && this.isFieldMatch(f[attr], - ['city', 'town', 'address-level-2', 'address-city', 'address-town'])) { - fillFields.city = f; - } else if (!fillFields.state && this.isFieldMatch(f[attr], - ['state', 'province', 'provence', 'address-level-1', 'address-state', - 'address-province'])) { - fillFields.state = f; - } else if (!fillFields.country && this.isFieldMatch(f[attr], - ['country', 'country-code', 'country-name', 'address-country', 'address-country-name', - 'address-country-code'])) { - fillFields.country = f; - } else if (!fillFields.phone && this.isFieldMatch(f[attr], - ['phone', 'mobile', 'mobile-phone', 'tel', 'telephone', 'phone-number'])) { - fillFields.phone = f; - } else if (!fillFields.username && this.isFieldMatch(f[attr], - ['user-name', 'user-id', 'screen-name'])) { - fillFields.username = f; - } else if (!fillFields.company && this.isFieldMatch(f[attr], - ['company', 'company-name', 'organization', 'organization-name'])) { - fillFields.company = f; - } - }); - }); - - const identity = options.cipher.identity; - this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'title'); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'firstName'); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'middleName'); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'lastName'); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'address1'); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'address2'); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'address3'); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'city'); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'postalCode'); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'company'); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'email'); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'phone'); - this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'username'); - - let filledState = false; - if (fillFields.state && identity.state && identity.state.length > 2) { - const stateLower = identity.state.toLowerCase(); - const isoState = IsoStates[stateLower] || IsoProvinces[stateLower]; - if (isoState) { - filledState = true; - this.makeScriptActionWithValue(fillScript, isoState, fillFields.state, filledFields); - } - } - - if (!filledState) { - this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'state'); - } - - let filledCountry = false; - if (fillFields.country && identity.country && identity.country.length > 2) { - const countryLower = identity.country.toLowerCase(); - const isoCountry = IsoCountries[countryLower]; - if (isoCountry) { - filledCountry = true; - this.makeScriptActionWithValue(fillScript, isoCountry, fillFields.country, filledFields); - } - } - - if (!filledCountry) { - this.makeScriptAction(fillScript, identity, fillFields, filledFields, 'country'); - } - - if (fillFields.name && (identity.firstName || identity.lastName)) { - let fullName = ''; - if (this.hasValue(identity.firstName)) { - fullName = identity.firstName; - } - if (this.hasValue(identity.middleName)) { - if (fullName !== '') { - fullName += ' '; - } - fullName += identity.middleName; - } - if (this.hasValue(identity.lastName)) { - if (fullName !== '') { - fullName += ' '; - } - fullName += identity.lastName; - } - - this.makeScriptActionWithValue(fillScript, fullName, fillFields.name, filledFields); - } - - if (fillFields.address && this.hasValue(identity.address1)) { - let address = ''; - if (this.hasValue(identity.address1)) { - address = identity.address1; - } - if (this.hasValue(identity.address2)) { - if (address !== '') { - address += ', '; - } - address += identity.address2; - } - if (this.hasValue(identity.address3)) { - if (address !== '') { - address += ', '; - } - address += identity.address3; - } - - this.makeScriptActionWithValue(fillScript, address, fillFields.address, filledFields); - } - - return fillScript; - } - - private isFieldMatch(value: string, options: string[], containsOptions?: string[]): boolean { - value = value.trim().toLowerCase().replace(/[^a-zA-Z]+/g, ''); - for (let i = 0; i < options.length; i++) { - let option = options[i]; - const checkValueContains = containsOptions == null || containsOptions.indexOf(option) > -1; - option = option.replace(/-/g, ''); - if (value === option || (checkValueContains && value.indexOf(option) > -1)) { - return true; - } - } - - return false; - } - - private makeScriptAction(fillScript: AutofillScript, cipherData: any, fillFields: { [id: string]: AutofillField; }, - filledFields: { [id: string]: AutofillField; }, dataProp: string, fieldProp?: string) { - fieldProp = fieldProp || dataProp; - this.makeScriptActionWithValue(fillScript, cipherData[dataProp], fillFields[fieldProp], filledFields); - } - - private makeScriptActionWithValue(fillScript: AutofillScript, dataValue: any, field: AutofillField, - filledFields: { [id: string]: AutofillField; }) { - - let doFill = false; - if (this.hasValue(dataValue) && field) { - if (field.type === 'select-one' && field.selectInfo && field.selectInfo.options) { - for (let i = 0; i < field.selectInfo.options.length; i++) { - const option = field.selectInfo.options[i]; - for (let j = 0; j < option.length; j++) { - if (option[j].toLowerCase() === dataValue.toLowerCase()) { - doFill = true; - if (option.length > 1) { - dataValue = option[1]; - } - break; - } - } - - if (doFill) { - break; - } - } - } else { - doFill = true; - } - } - - if (doFill) { - filledFields[field.opid] = field; - fillScript.script.push(['click_on_opid', field.opid]); - fillScript.script.push(['fill_by_opid', field.opid, dataValue]); - } - } - - private loadPasswordFields(pageDetails: AutofillPageDetails, canBeHidden: boolean) { - const arr: AutofillField[] = []; - pageDetails.fields.forEach((f) => { - if (!f.disabled && !f.readonly && f.type === 'password' && (canBeHidden || f.viewable)) { - arr.push(f); - } - }); - - return arr; - } - - private findUsernameField(pageDetails: AutofillPageDetails, passwordField: AutofillField, canBeHidden: boolean, - withoutForm: boolean) { - let usernameField: AutofillField = null; - for (let i = 0; i < pageDetails.fields.length; i++) { - const f = pageDetails.fields[i]; - if (f.elementNumber >= passwordField.elementNumber) { - break; - } - - if (!f.disabled && !f.readonly && - (withoutForm || f.form === passwordField.form) && (canBeHidden || f.viewable) && - (f.type === 'text' || f.type === 'email' || f.type === 'tel')) { - usernameField = f; - - if (this.findMatchingFieldIndex(f, UsernameFieldNames) > -1) { - // We found an exact match. No need to keep looking. - break; - } - } - } - - return usernameField; - } - - private findMatchingFieldIndex(field: AutofillField, names: string[]): number { - for (let i = 0; i < names.length; i++) { - if (this.fieldPropertyIsMatch(field, 'htmlID', names[i])) { - return i; - } - if (this.fieldPropertyIsMatch(field, 'htmlName', names[i])) { - return i; - } - if (this.fieldPropertyIsMatch(field, 'label-tag', names[i])) { - return i; - } - if (this.fieldPropertyIsMatch(field, 'placeholder', names[i])) { - return i; - } - } - - return -1; - } - - private fieldPropertyIsMatch(field: any, property: string, name: string): boolean { - let fieldVal = field[property] as string; - if (!this.hasValue(fieldVal)) { - return false; - } - - fieldVal = fieldVal.trim().replace(/(?:\r\n|\r|\n)/g, ''); - if (name.startsWith('regex=')) { - try { - const regexParts = name.split('=', 2); - if (regexParts.length === 2) { - const regex = new RegExp(regexParts[1], 'i'); - return regex.test(fieldVal); - } - } catch (e) { } - } else if (name.startsWith('csv=')) { - const csvParts = name.split('=', 2); - if (csvParts.length === 2) { - const csvVals = csvParts[1].split(','); - for (let i = 0; i < csvVals.length; i++) { - const val = csvVals[i]; - if (val != null && val.trim().toLowerCase() === fieldVal.toLowerCase()) { - return true; - } - } - return false; - } - } - - return fieldVal.toLowerCase() === name; - } - - private fieldIsFuzzyMatch(field: AutofillField, names: string[]): boolean { - if (this.hasValue(field.htmlID) && this.fuzzyMatch(names, field.htmlID)) { - return true; - } - if (this.hasValue(field.htmlName) && this.fuzzyMatch(names, field.htmlName)) { - return true; - } - if (this.hasValue(field['label-tag']) && this.fuzzyMatch(names, field['label-tag'])) { - return true; - } - if (this.hasValue(field.placeholder) && this.fuzzyMatch(names, field.placeholder)) { - return true; - } - if (this.hasValue(field['label-left']) && this.fuzzyMatch(names, field['label-left'])) { - return true; - } - if (this.hasValue(field['label-top']) && this.fuzzyMatch(names, field['label-top'])) { - return true; - } - - return false; - } - - private fuzzyMatch(options: string[], value: string): boolean { - if (options == null || options.length === 0 || value == null || value === '') { - return false; - } - - value = value.replace(/(?:\r\n|\r|\n)/g, '').trim().toLowerCase(); - - for (let i = 0; i < options.length; i++) { - if (value.indexOf(options[i]) > -1) { - return true; - } - } - - return false; - } - - private hasValue(str: string): boolean { - return str && str !== ''; - } - - private setFillScriptForFocus(filledFields: { [id: string]: AutofillField; }, - fillScript: AutofillScript): AutofillScript { - let lastField: AutofillField = null; - let lastPasswordField: AutofillField = null; - - for (const opid in filledFields) { - if (filledFields.hasOwnProperty(opid) && filledFields[opid].viewable) { - lastField = filledFields[opid]; - - if (filledFields[opid].type === 'password') { - lastPasswordField = filledFields[opid]; - } - } - } - - // Prioritize password field over others. - if (lastPasswordField) { - fillScript.script.push(['focus_by_opid', lastPasswordField.opid]); - } else if (lastField) { - fillScript.script.push(['focus_by_opid', lastField.opid]); - } - - return fillScript; - } -} diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts deleted file mode 100644 index 7b0d5d08b2..0000000000 --- a/src/services/cipher.service.ts +++ /dev/null @@ -1,514 +0,0 @@ -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; - cipher.collectionIds = model.collectionIds; - - 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: any[] = []; - const ciphers = await this.getAll(); - ciphers.forEach((cipher) => { - promises.push(cipher.decrypt().then((c: any) => { - decCiphers.push(c); - })); - }); - - await Promise.all(promises); - this.decryptedCipherCache = decCiphers; - return this.decryptedCipherCache; - } - - async getAllDecryptedForGrouping(groupingId: string, folder: boolean = true): Promise { - const ciphers = await this.getAllDecrypted(); - const ciphersToReturn: any[] = []; - - ciphers.forEach((cipher) => { - if (folder && cipher.folderId === groupingId) { - ciphersToReturn.push(cipher); - } else if (!folder && cipher.collectionIds != null && cipher.collectionIds.indexOf(groupingId) > -1) { - 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[] = []; - eqDomains.forEach((eqDomain) => { - 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: any[] = []; - - ciphers.forEach((cipher) => { - 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) { - return null; - } - - 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 (let i = 0; i < this.decryptedCipherCache.length; i++) { - const cached = this.decryptedCipherCache[i]; - 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, cipher.collectionIds); - 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); - - let response: CipherResponse; - try { - response = await self.apiService.postCipherAttachment(cipher.id, fd); - } catch (e) { - reject((e as ErrorResponse).getSingleMessage()); - return; - } - - const userId = await self.userService.getUserId(); - const data = new CipherData(response, userId, cipher.collectionIds); - 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 { - (cipher as CipherData[]).forEach((c) => { - ciphers[c.id] = c; - }); - } - - await UtilsService.saveObjToStorage(Keys.ciphersPrefix + userId, ciphers); - this.decryptedCipherCache = null; - } - - async replace(ciphers: { [id: string]: 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 { - (id as string[]).forEach((i) => { - 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 { - try { - await this.apiService.deleteCipherAttachment(id, attachmentId); - } catch (e) { - return Promise.reject((e as ErrorResponse).getSingleMessage()); - } - await this.deleteAttachment(id, attachmentId); - } - - 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.'); - } - } -} diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts deleted file mode 100644 index 14e23118be..0000000000 --- a/src/services/collection.service.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { CipherString } from '../models/domain/cipherString'; -import { Collection } from '../models/domain/collection'; - -import { CollectionData } from '../models/data/collectionData'; - -import CryptoService from './crypto.service'; -import UserService from './user.service'; -import UtilsService from './utils.service'; - -const Keys = { - collectionsPrefix: 'collections_', -}; - -export default class CollectionService { - decryptedCollectionCache: any[]; - - constructor(private cryptoService: CryptoService, private userService: UserService) { - } - - clearCache(): void { - this.decryptedCollectionCache = null; - } - - async get(id: string): Promise { - const userId = await this.userService.getUserId(); - const collections = await UtilsService.getObjFromStorage<{ [id: string]: CollectionData; }>( - Keys.collectionsPrefix + userId); - if (collections == null || !collections.hasOwnProperty(id)) { - return null; - } - - return new Collection(collections[id]); - } - - async getAll(): Promise { - const userId = await this.userService.getUserId(); - const collections = await UtilsService.getObjFromStorage<{ [id: string]: CollectionData; }>( - Keys.collectionsPrefix + userId); - const response: Collection[] = []; - for (const id in collections) { - if (collections.hasOwnProperty(id)) { - response.push(new Collection(collections[id])); - } - } - return response; - } - - async getAllDecrypted(): Promise { - if (this.decryptedCollectionCache != null) { - return this.decryptedCollectionCache; - } - - const key = await this.cryptoService.getKey(); - if (key == null) { - throw new Error('No key.'); - } - - const decFolders: any[] = []; - const promises: Array> = []; - const folders = await this.getAll(); - folders.forEach((folder) => { - promises.push(folder.decrypt().then((f: any) => { - decFolders.push(f); - })); - }); - - await Promise.all(promises); - this.decryptedCollectionCache = decFolders; - return this.decryptedCollectionCache; - } - - async upsert(collection: CollectionData | CollectionData[]): Promise { - const userId = await this.userService.getUserId(); - let collections = await UtilsService.getObjFromStorage<{ [id: string]: CollectionData; }>( - Keys.collectionsPrefix + userId); - if (collections == null) { - collections = {}; - } - - if (collection instanceof CollectionData) { - const c = collection as CollectionData; - collections[c.id] = c; - } else { - (collection as CollectionData[]).forEach((c) => { - collections[c.id] = c; - }); - } - - await UtilsService.saveObjToStorage(Keys.collectionsPrefix + userId, collections); - this.decryptedCollectionCache = null; - } - - async replace(collections: { [id: string]: CollectionData; }): Promise { - const userId = await this.userService.getUserId(); - await UtilsService.saveObjToStorage(Keys.collectionsPrefix + userId, collections); - this.decryptedCollectionCache = null; - } - - async clear(userId: string): Promise { - await UtilsService.removeFromStorage(Keys.collectionsPrefix + userId); - this.decryptedCollectionCache = null; - } - - async delete(id: string | string[]): Promise { - const userId = await this.userService.getUserId(); - const collections = await UtilsService.getObjFromStorage<{ [id: string]: CollectionData; }>( - Keys.collectionsPrefix + userId); - if (collections == null) { - return; - } - - if (typeof id === 'string') { - const i = id as string; - delete collections[id]; - } else { - (id as string[]).forEach((i) => { - delete collections[i]; - }); - } - - await UtilsService.saveObjToStorage(Keys.collectionsPrefix + userId, collections); - this.decryptedCollectionCache = null; - } -} diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts deleted file mode 100644 index 337c6174a4..0000000000 --- a/src/services/constants.service.ts +++ /dev/null @@ -1,116 +0,0 @@ -import UtilsService from './utils.service'; - -export default 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, utilsService: UtilsService) { - if (utilsService.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 deleted file mode 100644 index 16c3c7497b..0000000000 --- a/src/services/crypto.service.ts +++ /dev/null @@ -1,597 +0,0 @@ -import * as forge from 'node-forge'; - -import { EncryptionType } from '../enums/encryptionType.enum'; - -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 ConstantsService from './constants.service'; -import UtilsService from './utils.service'; - -import { CryptoService as CryptoServiceInterface } from './abstractions/crypto.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 default class CryptoService implements CryptoServiceInterface { - private key: SymmetricCryptoKey; - private encKey: SymmetricCryptoKey; - private legacyEtmKey: SymmetricCryptoKey; - private keyHash: string; - private privateKey: ArrayBuffer; - private orgKeys: Map; - - async setKey(key: SymmetricCryptoKey): Promise { - this.key = key; - - const option = await UtilsService.getObjFromStorage(ConstantsService.lockOptionKey); - if (option != null) { - // if we have a lock option set, we do not store the key - return; - } - - return UtilsService.saveObjToStorage(Keys.key, key.keyB64); - } - - setKeyHash(keyHash: string): Promise<{}> { - this.keyHash = keyHash; - return UtilsService.saveObjToStorage(Keys.keyHash, keyHash); - } - - async setEncKey(encKey: string): Promise<{}> { - if (encKey == null) { - return; - } - await UtilsService.saveObjToStorage(Keys.encKey, encKey); - this.encKey = null; - } - - async setEncPrivateKey(encPrivateKey: string): Promise<{}> { - if (encPrivateKey == null) { - return; - } - - await UtilsService.saveObjToStorage(Keys.encPrivateKey, encPrivateKey); - this.privateKey = null; - } - - setOrgKeys(orgs: ProfileOrganizationResponse[]): Promise<{}> { - const orgKeys: any = {}; - orgs.forEach((org) => { - orgKeys[org.id] = org.key; - }); - - return UtilsService.saveObjToStorage(Keys.encOrgKeys, orgKeys); - } - - async getKey(): Promise { - if (this.key != null) { - return this.key; - } - - const option = await UtilsService.getObjFromStorage(ConstantsService.lockOptionKey); - if (option != null) { - return null; - } - - const key = await UtilsService.getObjFromStorage(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 UtilsService.getObjFromStorage(Keys.keyHash); - } - - async getEncKey(): Promise { - if (this.encKey != null) { - return this.encKey; - } - - const encKey = await UtilsService.getObjFromStorage(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 UtilsService.getObjFromStorage(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 self = this; - const encOrgKeys = await UtilsService.getObjFromStorage(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 UtilsService.removeFromStorage(Keys.key); - } - - clearKeyHash(): Promise { - this.keyHash = null; - return UtilsService.removeFromStorage(Keys.keyHash); - } - - clearEncKey(memoryOnly?: boolean): Promise { - this.encKey = null; - if (memoryOnly) { - return Promise.resolve(); - } - return UtilsService.removeFromStorage(Keys.encKey); - } - - clearPrivateKey(memoryOnly?: boolean): Promise { - this.privateKey = null; - if (memoryOnly) { - return Promise.resolve(); - } - return UtilsService.removeFromStorage(Keys.encPrivateKey); - } - - clearOrgKeys(memoryOnly?: boolean): Promise { - this.orgKeys = null; - if (memoryOnly) { - return Promise.resolve(); - } - return UtilsService.removeFromStorage(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 UtilsService.getObjFromStorage(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/environment.service.ts b/src/services/environment.service.ts deleted file mode 100644 index 098a67f749..0000000000 --- a/src/services/environment.service.ts +++ /dev/null @@ -1,87 +0,0 @@ -import ApiService from './api.service'; -import ConstantsService from './constants.service'; -import UtilsService from './utils.service'; - -import EnvironmentUrls from '../models/domain/environmentUrls'; - -export default class EnvironmentService { - baseUrl: string; - webVaultUrl: string; - apiUrl: string; - identityUrl: string; - iconsUrl: string; - - constructor(private apiService: ApiService) { - } - - async setUrlsFromStorage(): Promise { - const urlsObj: any = await UtilsService.getObjFromStorage(ConstantsService.environmentUrlsKey); - const urls = urlsObj || { - base: null, - api: null, - identity: null, - icons: null, - webVault: null, - }; - - const envUrls = new EnvironmentUrls(); - - if (urls.base) { - this.baseUrl = envUrls.base = urls.base; - await this.apiService.setUrls(envUrls); - return; - } - - this.webVaultUrl = urls.webVault; - this.apiUrl = envUrls.api = urls.api; - this.identityUrl = envUrls.identity = urls.identity; - this.iconsUrl = urls.icons; - await this.apiService.setUrls(envUrls); - } - - async setUrls(urls: any): Promise { - urls.base = this.formatUrl(urls.base); - urls.webVault = this.formatUrl(urls.webVault); - urls.api = this.formatUrl(urls.api); - urls.identity = this.formatUrl(urls.identity); - urls.icons = this.formatUrl(urls.icons); - - await UtilsService.saveObjToStorage(ConstantsService.environmentUrlsKey, { - base: urls.base, - api: urls.api, - identity: urls.identity, - webVault: urls.webVault, - icons: urls.icons, - }); - - this.baseUrl = urls.base; - this.webVaultUrl = urls.webVault; - this.apiUrl = urls.api; - this.identityUrl = urls.identity; - this.iconsUrl = urls.icons; - - const envUrls = new EnvironmentUrls(); - if (this.baseUrl) { - envUrls.base = this.baseUrl; - } else { - envUrls.api = this.apiUrl; - envUrls.identity = this.identityUrl; - } - - await this.apiService.setUrls(envUrls); - return urls; - } - - private formatUrl(url: string): string { - if (url == null || url === '') { - return null; - } - - url = url.replace(/\/+$/g, ''); - if (!url.startsWith('http://') && !url.startsWith('https://')) { - url = 'https://' + url; - } - - return url; - } -} diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts deleted file mode 100644 index 3337c1b0bc..0000000000 --- a/src/services/folder.service.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { CipherString } from '../models/domain/cipherString'; -import { Folder } from '../models/domain/folder'; - -import { FolderData } from '../models/data/folderData'; - -import { FolderRequest } from '../models/request/folderRequest'; -import { FolderResponse } from '../models/response/folderResponse'; - -import ApiService from './api.service'; -import CryptoService from './crypto.service'; -import UserService from './user.service'; -import UtilsService from './utils.service'; - -const Keys = { - foldersPrefix: 'folders_', -}; - -export default class FolderService { - decryptedFolderCache: any[]; - - constructor(private cryptoService: CryptoService, private userService: UserService, - private i18nService: any, private apiService: ApiService) { - } - - clearCache(): void { - this.decryptedFolderCache = null; - } - - async encrypt(model: any): Promise { - const folder = new Folder(); - folder.id = model.id; - folder.name = await this.cryptoService.encrypt(model.name); - return folder; - } - - async get(id: string): Promise { - const userId = await this.userService.getUserId(); - const folders = await UtilsService.getObjFromStorage<{ [id: string]: FolderData; }>( - Keys.foldersPrefix + userId); - if (folders == null || !folders.hasOwnProperty(id)) { - return null; - } - - return new Folder(folders[id]); - } - - async getAll(): Promise { - const userId = await this.userService.getUserId(); - const folders = await UtilsService.getObjFromStorage<{ [id: string]: FolderData; }>( - Keys.foldersPrefix + userId); - const response: Folder[] = []; - for (const id in folders) { - if (folders.hasOwnProperty(id)) { - response.push(new Folder(folders[id])); - } - } - return response; - } - - async getAllDecrypted(): Promise { - if (this.decryptedFolderCache != null) { - return this.decryptedFolderCache; - } - - const decFolders: any[] = [{ - id: null, - name: this.i18nService.noneFolder, - }]; - - const key = await this.cryptoService.getKey(); - if (key == null) { - throw new Error('No key.'); - } - - const promises: Array> = []; - const folders = await this.getAll(); - folders.forEach((folder) => { - promises.push(folder.decrypt().then((f: any) => { - decFolders.push(f); - })); - }); - - await Promise.all(promises); - this.decryptedFolderCache = decFolders; - return this.decryptedFolderCache; - } - - async saveWithServer(folder: Folder): Promise { - const request = new FolderRequest(folder); - - let response: FolderResponse; - if (folder.id == null) { - response = await this.apiService.postFolder(request); - folder.id = response.id; - } else { - response = await this.apiService.putFolder(folder.id, request); - } - - const userId = await this.userService.getUserId(); - const data = new FolderData(response, userId); - await this.upsert(data); - } - - async upsert(folder: FolderData | FolderData[]): Promise { - const userId = await this.userService.getUserId(); - let folders = await UtilsService.getObjFromStorage<{ [id: string]: FolderData; }>( - Keys.foldersPrefix + userId); - if (folders == null) { - folders = {}; - } - - if (folder instanceof FolderData) { - const f = folder as FolderData; - folders[f.id] = f; - } else { - (folder as FolderData[]).forEach((f) => { - folders[f.id] = f; - }); - } - - await UtilsService.saveObjToStorage(Keys.foldersPrefix + userId, folders); - this.decryptedFolderCache = null; - } - - async replace(folders: { [id: string]: FolderData; }): Promise { - const userId = await this.userService.getUserId(); - await UtilsService.saveObjToStorage(Keys.foldersPrefix + userId, folders); - this.decryptedFolderCache = null; - } - - async clear(userId: string): Promise { - await UtilsService.removeFromStorage(Keys.foldersPrefix + userId); - this.decryptedFolderCache = null; - } - - async delete(id: string | string[]): Promise { - const userId = await this.userService.getUserId(); - const folders = await UtilsService.getObjFromStorage<{ [id: string]: FolderData; }>( - Keys.foldersPrefix + userId); - if (folders == null) { - return; - } - - if (typeof id === 'string') { - const i = id as string; - delete folders[id]; - } else { - (id as string[]).forEach((i) => { - delete folders[i]; - }); - } - - await UtilsService.saveObjToStorage(Keys.foldersPrefix + userId, folders); - this.decryptedFolderCache = null; - } - - async deleteWithServer(id: string): Promise { - await this.apiService.deleteFolder(id); - await this.delete(id); - } -} diff --git a/src/services/i18n.service.ts b/src/services/i18n.service.ts deleted file mode 100644 index a294015f3c..0000000000 --- a/src/services/i18n.service.ts +++ /dev/null @@ -1,28 +0,0 @@ -import UtilsService from '../services/utils.service'; - -export default function i18nService(utilsService: UtilsService) { - const edgeMessages: any = {}; - - if (utilsService.isEdge()) { - fetch('../_locales/en/messages.json').then((file) => { - return file.json(); - }).then((locales) => { - for (const prop in locales) { - if (locales.hasOwnProperty(prop)) { - edgeMessages[prop] = chrome.i18n.getMessage(prop); - } - } - }); - - return edgeMessages; - } - - return new Proxy({}, { - get: (target, name) => { - return chrome.i18n.getMessage(name); - }, - set: (target, name, value) => { - return false; - }, - }); -} diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts deleted file mode 100644 index bc58aacbab..0000000000 --- a/src/services/lock.service.ts +++ /dev/null @@ -1,89 +0,0 @@ -import CipherService from './cipher.service'; -import CollectionService from './collection.service'; -import ConstantsService from './constants.service'; -import CryptoService from './crypto.service'; -import FolderService from './folder.service'; -import UtilsService from './utils.service'; - -export default class LockService { - constructor(private cipherService: CipherService, private folderService: FolderService, - private collectionService: CollectionService, private cryptoService: CryptoService, - private utilsService: UtilsService, private setIcon: Function, private refreshBadgeAndMenu: Function) { - this.checkLock(); - setInterval(() => this.checkLock(), 10 * 1000); // check every 10 seconds - - const self = this; - if ((window as any).chrome.idle && (window as any).chrome.idle.onStateChanged) { - (window as any).chrome.idle.onStateChanged.addListener(async (newState: string) => { - if (newState === 'locked') { - const lockOption = await UtilsService.getObjFromStorage(ConstantsService.lockOptionKey); - if (lockOption === -2) { - self.lock(); - } - } - }); - } - } - - async checkLock(): Promise { - const popupOpen = chrome.extension.getViews({ type: 'popup' }).length > 0; - const tabOpen = chrome.extension.getViews({ type: 'tab' }).length > 0; - const sidebarView = this.sidebarViewName(); - const sidebarOpen = sidebarView != null && chrome.extension.getViews({ type: sidebarView }).length > 0; - - if (popupOpen || tabOpen || sidebarOpen) { - // Do not lock - return; - } - - const key = await this.cryptoService.getKey(); - if (key == null) { - // no key so no need to lock - return; - } - - const lockOption = await UtilsService.getObjFromStorage(ConstantsService.lockOptionKey); - if (lockOption == null || lockOption < 0) { - return; - } - - const lastActive = await UtilsService.getObjFromStorage(ConstantsService.lastActiveKey); - if (lastActive == null) { - return; - } - - const lockOptionSeconds = lockOption * 60; - const diffSeconds = ((new Date()).getTime() - lastActive) / 1000; - if (diffSeconds >= lockOptionSeconds) { - // need to lock now - await this.lock(); - } - } - - async lock(): Promise { - await Promise.all([ - this.cryptoService.clearKey(), - this.cryptoService.clearOrgKeys(true), - this.cryptoService.clearPrivateKey(true), - this.cryptoService.clearEncKey(true), - this.setIcon(), - this.refreshBadgeAndMenu(), - ]); - - this.folderService.clearCache(); - this.cipherService.clearCache(); - this.collectionService.clearCache(); - } - - // Helpers - - private sidebarViewName(): string { - if ((window as any).chrome.sidebarAction && this.utilsService.isFirefox()) { - return 'sidebar'; - } else if (this.utilsService.isOpera() && (typeof opr !== 'undefined') && opr.sidebarAction) { - return 'sidebar_panel'; - } - - return null; - } -} diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts deleted file mode 100644 index 4e52438d46..0000000000 --- a/src/services/passwordGeneration.service.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { CipherString } from '../models/domain/cipherString'; -import PasswordHistory from '../models/domain/passwordHistory'; - -import CryptoService from './crypto.service'; -import UtilsService from './utils.service'; - -const DefaultOptions = { - length: 14, - ambiguous: false, - number: true, - minNumber: 1, - uppercase: true, - minUppercase: 1, - lowercase: true, - minLowercase: 1, - special: false, - minSpecial: 1, -}; - -const Keys = { - options: 'passwordGenerationOptions', - history: 'generatedPasswordHistory', -}; - -const MaxPasswordsInHistory = 100; - -export default class PasswordGenerationService { - static generatePassword(options: any): string { - // overload defaults with given options - const o = Object.assign({}, DefaultOptions, options); - - // sanitize - if (o.uppercase && o.minUppercase < 0) { - o.minUppercase = 1; - } - if (o.lowercase && o.minLowercase < 0) { - o.minLowercase = 1; - } - if (o.number && o.minNumber < 0) { - o.minNumber = 1; - } - if (o.special && o.minSpecial < 0) { - o.minSpecial = 1; - } - - if (!o.length || o.length < 1) { - o.length = 10; - } - - const minLength: number = o.minUppercase + o.minLowercase + o.minNumber + o.minSpecial; - if (o.length < minLength) { - o.length = minLength; - } - - const positions: string[] = []; - if (o.lowercase && o.minLowercase > 0) { - for (let i = 0; i < o.minLowercase; i++) { - positions.push('l'); - } - } - if (o.uppercase && o.minUppercase > 0) { - for (let i = 0; i < o.minUppercase; i++) { - positions.push('u'); - } - } - if (o.number && o.minNumber > 0) { - for (let i = 0; i < o.minNumber; i++) { - positions.push('n'); - } - } - if (o.special && o.minSpecial > 0) { - for (let i = 0; i < o.minSpecial; i++) { - positions.push('s'); - } - } - while (positions.length < o.length) { - positions.push('a'); - } - - // shuffle - positions.sort(() => { - return UtilsService.secureRandomNumber(0, 1) * 2 - 1; - }); - - // build out the char sets - let allCharSet = ''; - - let lowercaseCharSet = 'abcdefghijkmnopqrstuvwxyz'; - if (o.ambiguous) { - lowercaseCharSet += 'l'; - } - if (o.lowercase) { - allCharSet += lowercaseCharSet; - } - - let uppercaseCharSet = 'ABCDEFGHIJKLMNPQRSTUVWXYZ'; - if (o.ambiguous) { - uppercaseCharSet += 'O'; - } - if (o.uppercase) { - allCharSet += uppercaseCharSet; - } - - let numberCharSet = '23456789'; - if (o.ambiguous) { - numberCharSet += '01'; - } - if (o.number) { - allCharSet += numberCharSet; - } - - const specialCharSet = '!@#$%^&*'; - if (o.special) { - allCharSet += specialCharSet; - } - - let password = ''; - for (let i = 0; i < o.length; i++) { - let positionChars: string; - switch (positions[i]) { - case 'l': - positionChars = lowercaseCharSet; - break; - case 'u': - positionChars = uppercaseCharSet; - break; - case 'n': - positionChars = numberCharSet; - break; - case 's': - positionChars = specialCharSet; - break; - case 'a': - positionChars = allCharSet; - break; - } - - const randomCharIndex = UtilsService.secureRandomNumber(0, positionChars.length - 1); - password += positionChars.charAt(randomCharIndex); - } - - return password; - } - - optionsCache: any; - history: PasswordHistory[] = []; - - constructor(private cryptoService: CryptoService) { - UtilsService.getObjFromStorage(Keys.history).then((encrypted) => { - return this.decryptHistory(encrypted); - }).then((history) => { - this.history = history; - }); - } - - generatePassword(options: any) { - return PasswordGenerationService.generatePassword(options); - } - - async getOptions() { - if (this.optionsCache == null) { - const options = await UtilsService.getObjFromStorage(Keys.options); - if (options == null) { - this.optionsCache = DefaultOptions; - } else { - this.optionsCache = options; - } - } - - return this.optionsCache; - } - - async saveOptions(options: any) { - await UtilsService.saveObjToStorage(Keys.options, options); - this.optionsCache = options; - } - - getHistory() { - return this.history || new Array(); - } - - async addHistory(password: string): Promise { - // Prevent duplicates - if (this.matchesPrevious(password)) { - return; - } - - this.history.push(new PasswordHistory(password, Date.now())); - - // Remove old items. - if (this.history.length > MaxPasswordsInHistory) { - this.history.shift(); - } - - const newHistory = await this.encryptHistory(); - return await UtilsService.saveObjToStorage(Keys.history, newHistory); - } - - async clear(): Promise { - this.history = []; - return await UtilsService.removeFromStorage(Keys.history); - } - - private async encryptHistory(): Promise { - if (this.history == null || this.history.length === 0) { - return Promise.resolve([]); - } - - const promises = this.history.map(async (item) => { - const encrypted = await this.cryptoService.encrypt(item.password); - return new PasswordHistory(encrypted.encryptedString, item.date); - }); - - return await Promise.all(promises); - } - - private async decryptHistory(history: PasswordHistory[]): Promise { - if (history == null || history.length === 0) { - return Promise.resolve([]); - } - - const promises = history.map(async (item) => { - const decrypted = await this.cryptoService.decrypt(new CipherString(item.password)); - return new PasswordHistory(decrypted, item.date); - }); - - return await Promise.all(promises); - } - - private matchesPrevious(password: string): boolean { - if (this.history == null || this.history.length === 0) { - return false; - } - - return this.history[this.history.length - 1].password === password; - } -} diff --git a/src/services/settings.service.ts b/src/services/settings.service.ts deleted file mode 100644 index 25d598f83c..0000000000 --- a/src/services/settings.service.ts +++ /dev/null @@ -1,61 +0,0 @@ -import UserService from './user.service'; -import UtilsService from './utils.service'; - -const Keys = { - settingsPrefix: 'settings_', - equivalentDomains: 'equivalentDomains', -}; - -export default class SettingsService { - private settingsCache: any; - - constructor(private userService: UserService) { - } - - clearCache(): void { - this.settingsCache = null; - } - - getEquivalentDomains(): Promise { - return this.getSettingsKey(Keys.equivalentDomains); - } - - async setEquivalentDomains(equivalentDomains: string[][]) { - await this.setSettingsKey(Keys.equivalentDomains, equivalentDomains); - } - - async clear(userId: string): Promise { - await UtilsService.removeFromStorage(Keys.settingsPrefix + userId); - this.settingsCache = null; - } - - // Helpers - - private async getSettings(): Promise { - if (this.settingsCache == null) { - const userId = await this.userService.getUserId(); - this.settingsCache = UtilsService.getObjFromStorage(Keys.settingsPrefix + userId); - } - return this.settingsCache; - } - - private async getSettingsKey(key: string): Promise { - const settings = await this.getSettings(); - if (settings != null && settings[key]) { - return settings[key]; - } - return null; - } - - private async setSettingsKey(key: string, value: any): Promise { - const userId = await this.userService.getUserId(); - let settings = await this.getSettings(); - if (!settings) { - settings = {}; - } - - settings[key] = value; - await UtilsService.saveObjToStorage(Keys.settingsPrefix + userId, settings); - this.settingsCache = settings; - } -} diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts deleted file mode 100644 index a28c401f19..0000000000 --- a/src/services/sync.service.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { CipherData } from '../models/data/cipherData'; -import { CollectionData } from '../models/data/collectionData'; -import { FolderData } from '../models/data/folderData'; - -import { CipherResponse } from '../models/response/cipherResponse'; -import { CollectionResponse } from '../models/response/collectionResponse'; -import { DomainsResponse } from '../models/response/domainsResponse'; -import { FolderResponse } from '../models/response/folderResponse'; -import { ProfileResponse } from '../models/response/profileResponse'; -import { SyncResponse } from '../models/response/syncResponse'; - -import ApiService from './api.service'; -import CipherService from './cipher.service'; -import CollectionService from './collection.service'; -import CryptoService from './crypto.service'; -import FolderService from './folder.service'; -import SettingsService from './settings.service'; -import UserService from './user.service'; -import UtilsService from './utils.service'; - -const Keys = { - lastSyncPrefix: 'lastSync_', -}; - -export default class SyncService { - syncInProgress: boolean = false; - - constructor(private userService: UserService, private apiService: ApiService, - private settingsService: SettingsService, private folderService: FolderService, - private cipherService: CipherService, private cryptoService: CryptoService, - private collectionService: CollectionService, private logoutCallback: Function) { - } - - async getLastSync() { - const userId = await this.userService.getUserId(); - const lastSync = await UtilsService.getObjFromStorage(Keys.lastSyncPrefix + userId); - if (lastSync) { - return new Date(lastSync); - } - - return null; - } - - async setLastSync(date: Date) { - const userId = await this.userService.getUserId(); - await UtilsService.saveObjToStorage(Keys.lastSyncPrefix + userId, date.toJSON()); - } - - syncStarted() { - this.syncInProgress = true; - chrome.runtime.sendMessage({ command: 'syncStarted' }); - } - - syncCompleted(successfully: boolean) { - this.syncInProgress = false; - // tslint:disable-next-line - chrome.runtime.sendMessage({ command: 'syncCompleted', successfully: successfully }); - } - - async fullSync(forceSync: boolean) { - this.syncStarted(); - const isAuthenticated = await this.userService.isAuthenticated(); - if (!isAuthenticated) { - this.syncCompleted(false); - return false; - } - - const now = new Date(); - const needsSyncResult = await this.needsSyncing(forceSync); - const needsSync = needsSyncResult[0]; - const skipped = needsSyncResult[1]; - - if (skipped) { - this.syncCompleted(false); - return false; - } - - if (!needsSync) { - await this.setLastSync(now); - this.syncCompleted(false); - return false; - } - - const userId = await this.userService.getUserId(); - try { - const response = await this.apiService.getSync(); - - await this.syncProfile(response.profile); - await this.syncFolders(userId, response.folders); - await this.syncCollections(response.collections); - await this.syncCiphers(userId, response.ciphers); - await this.syncSettings(userId, response.domains); - - await this.setLastSync(now); - this.syncCompleted(true); - return true; - } catch (e) { - this.syncCompleted(false); - return false; - } - } - - // Helpers - - private async needsSyncing(forceSync: boolean) { - if (forceSync) { - return [true, false]; - } - - try { - const response = await this.apiService.getAccountRevisionDate(); - const accountRevisionDate = new Date(response); - const lastSync = await this.getLastSync(); - if (lastSync != null && accountRevisionDate <= lastSync) { - return [false, false]; - } - - return [true, false]; - } catch (e) { - return [false, true]; - } - } - - private async syncProfile(response: ProfileResponse) { - const stamp = await this.userService.getSecurityStamp(); - if (stamp != null && stamp !== response.securityStamp) { - if (this.logoutCallback != null) { - this.logoutCallback(true); - } - - throw new Error('Stamp has changed'); - } - - await this.cryptoService.setEncKey(response.key); - await this.cryptoService.setEncPrivateKey(response.privateKey); - await this.cryptoService.setOrgKeys(response.organizations); - await this.userService.setSecurityStamp(response.securityStamp); - } - - private async syncFolders(userId: string, response: FolderResponse[]) { - const folders: { [id: string]: FolderData; } = {}; - response.forEach((f) => { - folders[f.id] = new FolderData(f, userId); - }); - return await this.folderService.replace(folders); - } - - private async syncCollections(response: CollectionResponse[]) { - const collections: { [id: string]: CollectionData; } = {}; - response.forEach((c) => { - collections[c.id] = new CollectionData(c); - }); - return await this.collectionService.replace(collections); - } - - private async syncCiphers(userId: string, response: CipherResponse[]) { - const ciphers: { [id: string]: CipherData; } = {}; - response.forEach((c) => { - ciphers[c.id] = new CipherData(c, userId); - }); - return await this.cipherService.replace(ciphers); - } - - private async syncSettings(userId: string, response: DomainsResponse) { - let eqDomains: string[][] = []; - if (response != null && response.equivalentDomains != null) { - eqDomains = eqDomains.concat(response.equivalentDomains); - } - - if (response != null && response.globalEquivalentDomains != null) { - response.globalEquivalentDomains.forEach((global) => { - if (global.domains.length > 0) { - eqDomains.push(global.domains); - } - }); - } - - return this.settingsService.setEquivalentDomains(eqDomains); - } -} diff --git a/src/services/token.service.ts b/src/services/token.service.ts deleted file mode 100644 index 3ebaa239be..0000000000 --- a/src/services/token.service.ts +++ /dev/null @@ -1,170 +0,0 @@ -import ConstantsService from './constants.service'; -import UtilsService from './utils.service'; - -const Keys = { - accessToken: 'accessToken', - refreshToken: 'refreshToken', - twoFactorTokenPrefix: 'twoFactorToken_', -}; - -export default class TokenService { - token: string; - decodedToken: any; - refreshToken: string; - - setTokens(accessToken: string, refreshToken: string): Promise { - return Promise.all([ - this.setToken(accessToken), - this.setRefreshToken(refreshToken), - ]); - } - - setToken(token: string): Promise { - this.token = token; - this.decodedToken = null; - return UtilsService.saveObjToStorage(Keys.accessToken, token); - } - - async getToken(): Promise { - if (this.token != null) { - return this.token; - } - - this.token = await UtilsService.getObjFromStorage(Keys.accessToken); - return this.token; - } - - setRefreshToken(refreshToken: string): Promise { - this.refreshToken = refreshToken; - return UtilsService.saveObjToStorage(Keys.refreshToken, refreshToken); - } - - async getRefreshToken(): Promise { - if (this.refreshToken != null) { - return this.refreshToken; - } - - this.refreshToken = await UtilsService.getObjFromStorage(Keys.refreshToken); - return this.refreshToken; - } - - setTwoFactorToken(token: string, email: string): Promise { - return UtilsService.saveObjToStorage(Keys.twoFactorTokenPrefix + email, token); - } - - getTwoFactorToken(email: string): Promise { - return UtilsService.getObjFromStorage(Keys.twoFactorTokenPrefix + email); - } - - clearTwoFactorToken(email: string): Promise { - return UtilsService.removeFromStorage(Keys.twoFactorTokenPrefix + email); - } - - clearToken(): Promise { - this.token = null; - this.decodedToken = null; - this.refreshToken = null; - - return Promise.all([ - UtilsService.removeFromStorage(Keys.accessToken), - UtilsService.removeFromStorage(Keys.refreshToken), - ]); - } - - // jwthelper methods - // ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js - - decodeToken(): any { - if (this.decodedToken) { - return this.decodedToken; - } - - if (this.token == null) { - throw new Error('Token not found.'); - } - - const parts = this.token.split('.'); - if (parts.length !== 3) { - throw new Error('JWT must have 3 parts'); - } - - const decoded = UtilsService.urlBase64Decode(parts[1]); - if (decoded == null) { - throw new Error('Cannot decode the token'); - } - - this.decodedToken = JSON.parse(decoded); - return this.decodedToken; - } - - getTokenExpirationDate(): Date { - const decoded = this.decodeToken(); - if (typeof decoded.exp === 'undefined') { - return null; - } - - const d = new Date(0); // The 0 here is the key, which sets the date to the epoch - d.setUTCSeconds(decoded.exp); - return d; - } - - tokenSecondsRemaining(offsetSeconds: number = 0): number { - const d = this.getTokenExpirationDate(); - if (d == null) { - return 0; - } - - const msRemaining = d.valueOf() - (new Date().valueOf() + (offsetSeconds * 1000)); - return Math.round(msRemaining / 1000); - } - - tokenNeedsRefresh(minutes: number = 5): boolean { - const sRemaining = this.tokenSecondsRemaining(); - return sRemaining < (60 * minutes); - } - - getUserId(): string { - const decoded = this.decodeToken(); - if (typeof decoded.sub === 'undefined') { - throw new Error('No user id found'); - } - - return decoded.sub as string; - } - - getEmail(): string { - const decoded = this.decodeToken(); - if (typeof decoded.email === 'undefined') { - throw new Error('No email found'); - } - - return decoded.email as string; - } - - getName(): string { - const decoded = this.decodeToken(); - if (typeof decoded.name === 'undefined') { - throw new Error('No name found'); - } - - return decoded.name as string; - } - - getPremium(): boolean { - const decoded = this.decodeToken(); - if (typeof decoded.premium === 'undefined') { - return false; - } - - return decoded.premium as boolean; - } - - getIssuer(): string { - const decoded = this.decodeToken(); - if (typeof decoded.iss === 'undefined') { - throw new Error('No issuer found'); - } - - return decoded.iss as string; - } -} diff --git a/src/services/totp.service.ts b/src/services/totp.service.ts deleted file mode 100644 index ae4d4ec57a..0000000000 --- a/src/services/totp.service.ts +++ /dev/null @@ -1,113 +0,0 @@ -import ConstantsService from './constants.service'; -import UtilsService from './utils.service'; - -const b32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; - -const TotpAlgorithm = { - name: 'HMAC', - hash: { name: 'SHA-1' }, -}; - -export default class TotpService { - async getCode(keyb32: string): Promise { - const epoch = Math.round(new Date().getTime() / 1000.0); - const timeHex = this.leftpad(this.dec2hex(Math.floor(epoch / 30)), 16, '0'); - const timeBytes = this.hex2bytes(timeHex); - const keyBytes = this.b32tobytes(keyb32); - - if (!keyBytes.length || !timeBytes.length) { - return null; - } - - const hashHex = await this.sign(keyBytes, timeBytes); - if (!hashHex) { - return null; - } - - const offset = this.hex2dec(hashHex.substring(hashHex.length - 1)); - // tslint:disable-next-line - let otp = (this.hex2dec(hashHex.substr(offset * 2, 8)) & this.hex2dec('7fffffff')) + ''; - otp = (otp).substr(otp.length - 6, 6); - return otp; - } - - async isAutoCopyEnabled(): Promise { - return !(await UtilsService.getObjFromStorage(ConstantsService.disableAutoTotpCopyKey)); - } - - // Helpers - - private leftpad(s: string, l: number, p: string): string { - if (l + 1 >= s.length) { - s = Array(l + 1 - s.length).join(p) + s; - } - return s; - } - - private dec2hex(d: number): string { - return (d < 15.5 ? '0' : '') + Math.round(d).toString(16); - } - - private hex2dec(s: string): number { - return parseInt(s, 16); - } - - private hex2bytes(s: string): Uint8Array { - const bytes = new Uint8Array(s.length / 2); - for (let i = 0; i < s.length; i += 2) { - bytes[i / 2] = parseInt(s.substr(i, 2), 16); - } - return bytes; - } - - private buff2hex(buff: ArrayBuffer): string { - const bytes = new Uint8Array(buff); - const hex: string[] = []; - bytes.forEach((b) => { - // tslint:disable-next-line - hex.push((b >>> 4).toString(16)); - // tslint:disable-next-line - hex.push((b & 0xF).toString(16)); - }); - return hex.join(''); - } - - private b32tohex(s: string): string { - s = s.toUpperCase(); - let cleanedInput = ''; - - for (let i = 0; i < s.length; i++) { - if (b32Chars.indexOf(s[i]) < 0) { - continue; - } - - cleanedInput += s[i]; - } - s = cleanedInput; - - let bits = ''; - let hex = ''; - for (let i = 0; i < s.length; i++) { - const byteIndex = b32Chars.indexOf(s.charAt(i)); - if (byteIndex < 0) { - continue; - } - bits += this.leftpad(byteIndex.toString(2), 5, '0'); - } - for (let i = 0; i + 4 <= bits.length; i += 4) { - const chunk = bits.substr(i, 4); - hex = hex + parseInt(chunk, 2).toString(16); - } - return hex; - } - - private b32tobytes(s: string): Uint8Array { - return this.hex2bytes(this.b32tohex(s)); - } - - private async sign(keyBytes: Uint8Array, timeBytes: Uint8Array) { - const key = await window.crypto.subtle.importKey('raw', keyBytes, TotpAlgorithm, false, ['sign']); - const signature = await window.crypto.subtle.sign(TotpAlgorithm, key, timeBytes); - return this.buff2hex(signature); - } -} diff --git a/src/services/user.service.ts b/src/services/user.service.ts deleted file mode 100644 index caa56c0d6f..0000000000 --- a/src/services/user.service.ts +++ /dev/null @@ -1,79 +0,0 @@ -import TokenService from './token.service'; -import UtilsService from './utils.service'; - -const Keys = { - userId: 'userId', - userEmail: 'userEmail', - stamp: 'securityStamp', -}; - -export default class UserService { - userId: string; - email: string; - stamp: string; - - constructor(private tokenService: TokenService) { - } - - setUserIdAndEmail(userId: string, email: string): Promise { - this.email = email; - this.userId = userId; - - return Promise.all([ - UtilsService.saveObjToStorage(Keys.userEmail, email), - UtilsService.saveObjToStorage(Keys.userId, userId), - ]); - } - - setSecurityStamp(stamp: string): Promise { - this.stamp = stamp; - return UtilsService.saveObjToStorage(Keys.stamp, stamp); - } - - async getUserId(): Promise { - if (this.userId != null) { - return this.userId; - } - - this.userId = await UtilsService.getObjFromStorage(Keys.userId); - return this.userId; - } - - async getEmail(): Promise { - if (this.email != null) { - return this.email; - } - - this.email = await UtilsService.getObjFromStorage(Keys.userEmail); - return this.email; - } - - async getSecurityStamp(): Promise { - if (this.stamp != null) { - return this.stamp; - } - - this.stamp = await UtilsService.getObjFromStorage(Keys.stamp); - return this.stamp; - } - - async clear(): Promise { - await Promise.all([ - UtilsService.removeFromStorage(Keys.userId), - UtilsService.removeFromStorage(Keys.userEmail), - UtilsService.removeFromStorage(Keys.stamp), - ]); - - this.userId = this.email = this.stamp = null; - } - - async isAuthenticated(): Promise { - const token = await this.tokenService.getToken(); - if (token == null) { - return false; - } - - const userId = await this.getUserId(); - return userId != null; - } -} diff --git a/src/services/utils.service.spec.ts b/src/services/utils.service.spec.ts deleted file mode 100644 index c976888de8..0000000000 --- a/src/services/utils.service.spec.ts +++ /dev/null @@ -1,113 +0,0 @@ -import UtilsService from './utils.service'; -import { BrowserType } from '../enums/browserType.enum'; - -describe('Utils Service', () => { - describe('getDomain', () => { - it('should fail for invalid urls', () => { - expect(UtilsService.getDomain(null)).toBeNull(); - expect(UtilsService.getDomain(undefined)).toBeNull(); - expect(UtilsService.getDomain(' ')).toBeNull(); - expect(UtilsService.getDomain('https://bit!:"_&ward.com')).toBeNull(); - expect(UtilsService.getDomain('bitwarden')).toBeNull(); - }); - - it('should handle urls without protocol', () => { - expect(UtilsService.getDomain('bitwarden.com')).toBe('bitwarden.com'); - expect(UtilsService.getDomain('wrong://bitwarden.com')).toBe('bitwarden.com'); - }); - - it('should handle valid urls', () => { - expect(UtilsService.getDomain('https://bitwarden')).toBe('bitwarden'); - expect(UtilsService.getDomain('https://bitwarden.com')).toBe('bitwarden.com'); - expect(UtilsService.getDomain('http://bitwarden.com')).toBe('bitwarden.com'); - expect(UtilsService.getDomain('http://vault.bitwarden.com')).toBe('bitwarden.com'); - expect(UtilsService.getDomain('https://user:password@bitwarden.com:8080/password/sites?and&query#hash')).toBe('bitwarden.com'); - expect(UtilsService.getDomain('https://bitwarden.unknown')).toBe('bitwarden.unknown'); - }); - - it('should support localhost and IP', () => { - expect(UtilsService.getDomain('https://localhost')).toBe('localhost'); - expect(UtilsService.getDomain('https://192.168.1.1')).toBe('192.168.1.1'); - }); - }); - - describe('getHostname', () => { - it('should fail for invalid urls', () => { - expect(UtilsService.getHostname(null)).toBeNull(); - expect(UtilsService.getHostname(undefined)).toBeNull(); - expect(UtilsService.getHostname(' ')).toBeNull(); - expect(UtilsService.getHostname('https://bit!:"_&ward.com')).toBeNull(); - expect(UtilsService.getHostname('bitwarden')).toBeNull(); - }); - - it('should handle valid urls', () => { - expect(UtilsService.getHostname('https://bitwarden.com')).toBe('bitwarden.com'); - expect(UtilsService.getHostname('http://bitwarden.com')).toBe('bitwarden.com'); - expect(UtilsService.getHostname('http://vault.bitwarden.com')).toBe('vault.bitwarden.com'); - expect(UtilsService.getHostname('https://user:password@bitwarden.com:8080/password/sites?and&query#hash')).toBe('bitwarden.com'); - }); - - it('should support localhost and IP', () => { - expect(UtilsService.getHostname('https://localhost')).toBe('localhost'); - expect(UtilsService.getHostname('https://192.168.1.1')).toBe('192.168.1.1'); - }); - }); - - describe('newGuid', () => { - it('should create a valid guid', () => { - const validGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - expect(UtilsService.newGuid()).toMatch(validGuid); - }); - }); - - describe('getBrowser', () => { - const original = navigator.userAgent; - - // Reset the userAgent. - afterAll(() => { - Object.defineProperty(navigator, 'userAgent', { - value: original - }); - }); - - it('should detect chrome', () => { - Object.defineProperty(navigator, 'userAgent', { - configurable: true, - value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36' - }); - - const utilsService = new UtilsService(); - expect(utilsService.getBrowser()).toBe(BrowserType.Chrome); - }); - - it('should detect firefox', () => { - Object.defineProperty(navigator, 'userAgent', { - configurable: true, - value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0' - }); - - const utilsService = new UtilsService(); - expect(utilsService.getBrowser()).toBe(BrowserType.Firefox); - }); - - it('should detect opera', () => { - Object.defineProperty(navigator, 'userAgent', { - configurable: true, - value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3175.3 Safari/537.36 OPR/49.0.2695.0 (Edition developer)' - }); - - const utilsService = new UtilsService(); - expect(utilsService.getBrowser()).toBe(BrowserType.Opera); - }); - - it('should detect edge', () => { - Object.defineProperty(navigator, 'userAgent', { - configurable: true, - value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; ServiceUI 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.15063' - }); - - const utilsService = new UtilsService(); - expect(utilsService.getBrowser()).toBe(BrowserType.Edge); - }); - }); -}); diff --git a/src/services/utils.service.ts b/src/services/utils.service.ts deleted file mode 100644 index b2230de2b9..0000000000 --- a/src/services/utils.service.ts +++ /dev/null @@ -1,407 +0,0 @@ -import * as tldjs from 'tldjs'; -import { BrowserType } from '../enums/browserType.enum'; -import { UtilsService as UtilsServiceInterface } from './abstractions/utils.service'; - -const AnalyticsIds = { - [BrowserType.Chrome]: 'UA-81915606-6', - [BrowserType.Firefox]: 'UA-81915606-7', - [BrowserType.Opera]: 'UA-81915606-8', - [BrowserType.Edge]: 'UA-81915606-9', - [BrowserType.Vivaldi]: 'UA-81915606-15', - [BrowserType.Safari]: 'UA-81915606-16', -}; - -export default class UtilsService implements UtilsServiceInterface { - static copyToClipboard(text: string, doc?: Document): void { - doc = doc || document; - if ((window as any).clipboardData && (window as any).clipboardData.setData) { - // IE specific code path to prevent textarea being shown while dialog is visible. - (window as any).clipboardData.setData('Text', text); - } else if (doc.queryCommandSupported && doc.queryCommandSupported('copy')) { - const textarea = doc.createElement('textarea'); - textarea.textContent = text; - // Prevent scrolling to bottom of page in MS Edge. - textarea.style.position = 'fixed'; - doc.body.appendChild(textarea); - textarea.select(); - - try { - // Security exception may be thrown by some browsers. - doc.execCommand('copy'); - } catch (e) { - // tslint:disable-next-line - console.warn('Copy to clipboard failed.', e); - } finally { - doc.body.removeChild(textarea); - } - } - } - - static urlBase64Decode(str: string): string { - let output = str.replace(/-/g, '+').replace(/_/g, '/'); - switch (output.length % 4) { - case 0: - break; - case 2: - output += '=='; - break; - case 3: - output += '='; - break; - default: - throw new Error('Illegal base64url string!'); - } - - return decodeURIComponent(escape(window.atob(output))); - } - - // ref: http://stackoverflow.com/a/2117523/1090359 - static newGuid(): string { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { - // tslint:disable-next-line - const r = Math.random() * 16 | 0; - // tslint:disable-next-line - const v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); - } - - // EFForg/OpenWireless - // ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js - static secureRandomNumber(min: number, max: number): number { - let rval = 0; - const range = max - min + 1; - const bitsNeeded = Math.ceil(Math.log2(range)); - if (bitsNeeded > 53) { - throw new Error('We cannot generate numbers larger than 53 bits.'); - } - - const bytesNeeded = Math.ceil(bitsNeeded / 8); - const mask = Math.pow(2, bitsNeeded) - 1; - // 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111 - - // Create byte array and fill with N random numbers - const byteArray = new Uint8Array(bytesNeeded); - window.crypto.getRandomValues(byteArray); - - let p = (bytesNeeded - 1) * 8; - for (let i = 0; i < bytesNeeded; i++) { - rval += byteArray[i] * Math.pow(2, p); - p -= 8; - } - - // Use & to apply the mask and reduce the number of recursive lookups - // tslint:disable-next-line - rval = rval & mask; - - if (rval >= range) { - // Integer out of acceptable range - return UtilsService.secureRandomNumber(min, max); - } - - // Return an integer that falls within the range - return min + rval; - } - - static fromB64ToArray(str: string): Uint8Array { - const binaryString = window.atob(str); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes; - } - - static fromUtf8ToArray(str: string): Uint8Array { - const strUtf8 = unescape(encodeURIComponent(str)); - const arr = new Uint8Array(strUtf8.length); - for (let i = 0; i < strUtf8.length; i++) { - arr[i] = strUtf8.charCodeAt(i); - } - return arr; - } - - static fromBufferToB64(buffer: ArrayBuffer): string { - let binary = ''; - const bytes = new Uint8Array(buffer); - for (let i = 0; i < bytes.byteLength; i++) { - binary += String.fromCharCode(bytes[i]); - } - return window.btoa(binary); - } - - static fromBufferToUtf8(buffer: ArrayBuffer): string { - const bytes = new Uint8Array(buffer); - const encodedString = String.fromCharCode.apply(null, bytes); - return decodeURIComponent(escape(encodedString)); - } - - static saveObjToStorage(key: string, obj: any) { - return new Promise((resolve) => { - chrome.storage.local.set({ [key]: obj }, () => { - resolve(); - }); - }); - } - - static removeFromStorage(key: string) { - return new Promise((resolve) => { - chrome.storage.local.remove(key, () => { - resolve(); - }); - }); - } - - static getObjFromStorage(key: string): Promise { - return new Promise((resolve) => { - chrome.storage.local.get(key, (obj: any) => { - if (obj && (typeof obj[key] !== 'undefined') && obj[key] !== null) { - resolve(obj[key] as T); - } else { - resolve(null); - } - }); - }); - } - - static getDomain(uriString: string): string { - if (uriString == null) { - return null; - } - - uriString = uriString.trim(); - if (uriString === '') { - return null; - } - - if (uriString.startsWith('http://') || uriString.startsWith('https://')) { - try { - const url = new URL(uriString); - - if (url.hostname === 'localhost' || UtilsService.validIpAddress(url.hostname)) { - return url.hostname; - } - - const urlDomain = tldjs.getDomain(url.hostname); - return urlDomain != null ? urlDomain : url.hostname; - } catch (e) { } - } - - const domain = tldjs.getDomain(uriString); - if (domain != null) { - return domain; - } - - return null; - } - - static getHostname(uriString: string): string { - if (uriString == null) { - return null; - } - - uriString = uriString.trim(); - if (uriString === '') { - return null; - } - - if (uriString.startsWith('http://') || uriString.startsWith('https://')) { - try { - const url = new URL(uriString); - return url.hostname; - } catch (e) { } - } - - return null; - } - - private static validIpAddress(ipString: string): boolean { - // tslint:disable-next-line - const ipRegex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; - return ipRegex.test(ipString); - } - - private browserCache: BrowserType = null; - private analyticsIdCache: string = null; - - getBrowser(): BrowserType { - if (this.browserCache) { - return this.browserCache; - } - - if (navigator.userAgent.indexOf('Firefox') !== -1 || navigator.userAgent.indexOf('Gecko/') !== -1) { - this.browserCache = BrowserType.Firefox; - } else if ((!!(window as any).opr && !!opr.addons) || !!(window as any).opera || - navigator.userAgent.indexOf(' OPR/') >= 0) { - this.browserCache = BrowserType.Opera; - } else if (navigator.userAgent.indexOf(' Edge/') !== -1) { - this.browserCache = BrowserType.Edge; - } else if (navigator.userAgent.indexOf(' Vivaldi/') !== -1) { - this.browserCache = BrowserType.Vivaldi; - } else if ((window as any).chrome) { - this.browserCache = BrowserType.Chrome; - } - - return this.browserCache; - } - - getBrowserString(): string { - return BrowserType[this.getBrowser()].toLowerCase(); - } - - isFirefox(): boolean { - return this.getBrowser() === BrowserType.Firefox; - } - - isChrome(): boolean { - return this.getBrowser() === BrowserType.Chrome; - } - - isEdge(): boolean { - return this.getBrowser() === BrowserType.Edge; - } - - isOpera(): boolean { - return this.getBrowser() === BrowserType.Opera; - } - - isVivaldi(): boolean { - return this.getBrowser() === BrowserType.Vivaldi; - } - - isSafari(): boolean { - return this.getBrowser() === BrowserType.Safari; - } - - analyticsId(): string { - if (this.analyticsIdCache) { - return this.analyticsIdCache; - } - - this.analyticsIdCache = AnalyticsIds[this.getBrowser()]; - return this.analyticsIdCache; - } - - initListSectionItemListeners(doc: Document, angular: any): void { - if (!doc) { - throw new Error('doc parameter required'); - } - - const sectionItems = doc.querySelectorAll( - '.list-section-item:not([data-bw-events="1"])'); - const sectionFormItems = doc.querySelectorAll( - '.list-section-item:not([data-bw-events="1"]) input, ' + - '.list-section-item:not([data-bw-events="1"]) select, ' + - '.list-section-item:not([data-bw-events="1"]) textarea'); - - sectionItems.forEach((item) => { - (item as HTMLElement).dataset.bwEvents = '1'; - - item.addEventListener('click', (e) => { - if (e.defaultPrevented) { - return; - } - - const el = e.target as HTMLElement; - - // Some elements will already focus properly - if (el.tagName != null) { - switch (el.tagName.toLowerCase()) { - case 'label': case 'input': case 'textarea': case 'select': - return; - default: - break; - } - } - - const cell = el.closest('.list-section-item'); - if (!cell) { - return; - } - - const textFilter = 'input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"])'; - const text = cell.querySelectorAll(textFilter + ', textarea'); - const checkbox = cell.querySelectorAll('input[type="checkbox"]'); - const select = cell.querySelectorAll('select'); - - if (text.length > 0) { - (text[0] as HTMLElement).focus(); - } else if (select.length > 0) { - (select[0] as HTMLElement).focus(); - } else if (checkbox.length > 0) { - const cb = checkbox[0] as HTMLInputElement; - cb.checked = !cb.checked; - if (angular) { - angular.element(checkbox[0]).triggerHandler('click'); - } - } - }, false); - }); - - sectionFormItems.forEach((item) => { - const itemCell = item.closest('.list-section-item'); - (itemCell as HTMLElement).dataset.bwEvents = '1'; - - item.addEventListener('focus', (e: Event) => { - const el = e.target as HTMLElement; - const cell = el.closest('.list-section-item'); - if (!cell) { - return; - } - - cell.classList.add('active'); - }, false); - - item.addEventListener('blur', (e: Event) => { - const el = e.target as HTMLElement; - const cell = el.closest('.list-section-item'); - if (!cell) { - return; - } - - cell.classList.remove('active'); - }, false); - }); - } - - getDomain(uriString: string): string { - return UtilsService.getDomain(uriString); - } - - getHostname(uriString: string): string { - return UtilsService.getHostname(uriString); - } - - copyToClipboard(text: string, doc?: Document) { - UtilsService.copyToClipboard(text, doc); - } - - inSidebar(theWindow: Window): boolean { - return theWindow.location.search !== '' && theWindow.location.search.indexOf('uilocation=sidebar') > -1; - } - - inTab(theWindow: Window): boolean { - return theWindow.location.search !== '' && theWindow.location.search.indexOf('uilocation=tab') > -1; - } - - inPopout(theWindow: Window): boolean { - return theWindow.location.search !== '' && theWindow.location.search.indexOf('uilocation=popout') > -1; - } - - inPopup(theWindow: Window): boolean { - return theWindow.location.search === '' || theWindow.location.search.indexOf('uilocation=') === -1 || - theWindow.location.search.indexOf('uilocation=popup') > -1; - } - - saveObjToStorage(key: string, obj: any): Promise { - return UtilsService.saveObjToStorage(key, obj); - } - - removeFromStorage(key: string): Promise { - return UtilsService.removeFromStorage(key); - } - - getObjFromStorage(key: string): Promise { - return UtilsService.getObjFromStorage(key); - } -} From 5bcfa67635cf73d1f5df5b81a12d4d19e6d1ff66 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 5 Jan 2018 00:31:04 -0500 Subject: [PATCH 0005/1626] npm package --- .npmignore | 10 ++++++++++ package.json | 2 +- src/index.ts | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 .npmignore diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000000..dab6b1ee03 --- /dev/null +++ b/.npmignore @@ -0,0 +1,10 @@ +.vs +.idea +./node_modules +npm-debug.log +*.crx +*.pem +package-lock.json +# more +*.sln +.editorconfig diff --git a/package.json b/package.json index 8a192f1729..07a72324e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/jslib", - "version": "0.0.1", + "version": "0.0.3", "scripts": { }, diff --git a/src/index.ts b/src/index.ts index 8b13789179..9870e4c37f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,2 @@ - +export { BrowserType } from './enums/browserType.enum'; +export { CipherType } from './enums/cipherType.enum'; From ad99560a92d6695d8d97fde92005742c8e2149f4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 5 Jan 2018 22:46:51 -0500 Subject: [PATCH 0006/1626] transpiled npm package --- .gitignore | 1 + .npmignore | 12 ++---------- package.json | 15 ++++----------- src/enums/browserType.enum.ts | 8 -------- src/enums/fieldType.enum.ts | 5 ----- src/enums/secureNoteType.enum.ts | 3 --- src/index.ts | 2 +- tsconfig.json | 10 ++++------ 8 files changed, 12 insertions(+), 44 deletions(-) delete mode 100644 src/enums/browserType.enum.ts delete mode 100644 src/enums/fieldType.enum.ts delete mode 100644 src/enums/secureNoteType.enum.ts diff --git a/.gitignore b/.gitignore index c7d7fd6a34..db50295cf8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ npm-debug.log *.crx *.pem package-lock.json +dist diff --git a/.npmignore b/.npmignore index dab6b1ee03..ba91551e84 100644 --- a/.npmignore +++ b/.npmignore @@ -1,10 +1,2 @@ -.vs -.idea -./node_modules -npm-debug.log -*.crx -*.pem -package-lock.json -# more -*.sln -.editorconfig +tsconfig.json +src diff --git a/package.json b/package.json index 07a72324e4..c0f1a8c0d9 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,10 @@ { "name": "@bitwarden/jslib", - "version": "0.0.3", - "scripts": { - - }, - "main": "src/index.ts", + "version": "0.0.5", + "main": "dist/index.js", + "types": "dist/index.d.ts", "devDependencies": { "tslint": "^5.8.0", - "typescript": "^2.6.2" - }, - "dependencies": { - "@types/node-forge": "0.7.1", - "@types/webcrypto": "0.0.28", - "node-forge": "0.7.1" + "typescript": "^2.5.3" } } diff --git a/src/enums/browserType.enum.ts b/src/enums/browserType.enum.ts deleted file mode 100644 index 64da6fb16b..0000000000 --- a/src/enums/browserType.enum.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum BrowserType { - Chrome = 2, - Firefox = 3, - Opera = 4, - Edge = 5, - Vivaldi = 19, - Safari = 20, -} diff --git a/src/enums/fieldType.enum.ts b/src/enums/fieldType.enum.ts deleted file mode 100644 index c28b26c1da..0000000000 --- a/src/enums/fieldType.enum.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum FieldType { - Text = 0, - Hidden = 1, - Boolean = 2, -} diff --git a/src/enums/secureNoteType.enum.ts b/src/enums/secureNoteType.enum.ts deleted file mode 100644 index c7f3e44a78..0000000000 --- a/src/enums/secureNoteType.enum.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum SecureNoteType { - Generic = 0, -} diff --git a/src/index.ts b/src/index.ts index 9870e4c37f..2fd4b680bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ -export { BrowserType } from './enums/browserType.enum'; export { CipherType } from './enums/cipherType.enum'; +export { EncryptionType } from './enums/encryptionType.enum'; diff --git a/tsconfig.json b/tsconfig.json index 4087a2c91b..0bd6c7ad4c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,12 +3,10 @@ "noImplicitAny": true, "module": "es6", "target": "ES2016", - "allowJs": true, - "sourceMap": true, - "types": [ - ] + "declaration": true, + "outDir": "./dist" }, - "exclude": [ - "node_modules" + "include": [ + "src/**/*" ] } From 9cc6103436479e534b8e56dccdc0d0652dd781cf Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 6 Jan 2018 08:41:20 -0500 Subject: [PATCH 0007/1626] commonjs module --- package.json | 2 +- tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c0f1a8c0d9..d5b88b1588 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/jslib", - "version": "0.0.5", + "version": "0.0.7", "main": "dist/index.js", "types": "dist/index.d.ts", "devDependencies": { diff --git a/tsconfig.json b/tsconfig.json index 0bd6c7ad4c..9f357fdd1f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "noImplicitAny": true, - "module": "es6", + "module": "commonjs", "target": "ES2016", "declaration": true, "outDir": "./dist" From 215e3f2ad5230621602e7ca86c84145c7b39ee9c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 6 Jan 2018 09:07:41 -0500 Subject: [PATCH 0008/1626] update ignores --- .npmignore | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.npmignore b/.npmignore index ba91551e84..7a9831588b 100644 --- a/.npmignore +++ b/.npmignore @@ -1,2 +1,7 @@ +.vs +.idea +.editorconfig tsconfig.json +tslint.json +*.sln src diff --git a/package.json b/package.json index d5b88b1588..17768dd224 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/jslib", - "version": "0.0.7", + "version": "0.0.10", "main": "dist/index.js", "types": "dist/index.d.ts", "devDependencies": { From 41f037580897e649ec4571667495c9b0b8f2af96 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 6 Jan 2018 09:19:39 -0500 Subject: [PATCH 0009/1626] npm scripts --- package.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/package.json b/package.json index 17768dd224..7113db334b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,12 @@ { "name": "@bitwarden/jslib", "version": "0.0.10", + "scripts": { + "dev": "tsc", + "dev:watch": "tsc --watch", + "lint": "tslint src/**/*.ts || true", + "lint:fix": "tslint src/**/*.ts --fix" + }, "main": "dist/index.js", "types": "dist/index.d.ts", "devDependencies": { From 2d03c486b64851a2d710377e001f06082a077e21 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Sat, 6 Jan 2018 16:04:16 +0100 Subject: [PATCH 0010/1626] Add multiple build targets, for umd and es5. (#1) * Add multiple build targets, for umd, es5 and es6. * Remove lodash.camelcase. --- .npmignore | 7 ------- package.json | 36 +++++++++++++++++++++++++++--------- rollup.config.ts | 34 ++++++++++++++++++++++++++++++++++ tsconfig.json | 21 +++++++++++++++++---- 4 files changed, 78 insertions(+), 20 deletions(-) delete mode 100644 .npmignore create mode 100644 rollup.config.ts diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 7a9831588b..0000000000 --- a/.npmignore +++ /dev/null @@ -1,7 +0,0 @@ -.vs -.idea -.editorconfig -tsconfig.json -tslint.json -*.sln -src diff --git a/package.json b/package.json index 7113db334b..9d1ea5c341 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,34 @@ { "name": "@bitwarden/jslib", - "version": "0.0.10", - "scripts": { - "dev": "tsc", - "dev:watch": "tsc --watch", - "lint": "tslint src/**/*.ts || true", - "lint:fix": "tslint src/**/*.ts --fix" + "version": "0.0.11", + "description": "", + "keywords": [], + "main": "dist/index.umd.js", + "module": "dist/index.es5.js", + "typings": "dist/types/index.d.ts", + "files": [ + "dist" + ], + "license": "GPL-3.0", + "engines": { + "node": ">=6.0.0" + }, + "scripts": { + "lint": "tslint 'src/**/*.ts'", + "prebuild": "rimraf dist", + "build": "tsc && tsc --module commonjs --outDir dist/lib && rollup -c rollup.config.ts && typedoc --out dist/docs --target es6 --theme minimal --mode file src", + "start": "concurrently \"tsc -w\" \"rollup -c rollup.config.ts -w\"" }, - "main": "dist/index.js", - "types": "dist/index.d.ts", "devDependencies": { + "@types/node": "^8.0.0", + "concurrently": "^3.5.1", + "rimraf": "^2.6.2", + "rollup": "^0.53.0", + "rollup-plugin-commonjs": "^8.2.6", + "rollup-plugin-node-resolve": "^3.0.0", + "rollup-plugin-sourcemaps": "^0.4.2", "tslint": "^5.8.0", - "typescript": "^2.5.3" + "typedoc": "^0.9.0", + "typescript": "^2.6.2" } } diff --git a/rollup.config.ts b/rollup.config.ts new file mode 100644 index 0000000000..dfe12895ed --- /dev/null +++ b/rollup.config.ts @@ -0,0 +1,34 @@ +// Based upon https://github.com/alexjoverm/typescript-library-starter/blob/master/rollup.config.ts + +import commonjs from 'rollup-plugin-commonjs'; +import resolve from 'rollup-plugin-node-resolve'; +import sourceMaps from 'rollup-plugin-sourcemaps'; + +const pkg = require('./package.json'); + +const libraryName = 'index'; + +export default { + input: `dist/es/${libraryName}.js`, + output: [ + { file: pkg.main, name: libraryName, format: 'umd' }, + { file: pkg.module, format: 'es' }, + ], + sourcemap: true, + // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash') + external: [], + watch: { + include: 'dist/es/**', + }, + plugins: [ + // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) + commonjs(), + // Allow node_modules resolution, so you can use 'external' to control + // which external modules to include in the bundle + // https://github.com/rollup/rollup-plugin-node-resolve#usage + resolve(), + + // Resolve source maps to the original source + sourceMaps(), + ], +}; diff --git a/tsconfig.json b/tsconfig.json index 9f357fdd1f..0db9afbea2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,25 @@ { "compilerOptions": { - "noImplicitAny": true, - "module": "commonjs", + "moduleResolution": "node", "target": "ES2016", + "module": "es6", + "strict": true, + "sourceMap": true, "declaration": true, - "outDir": "./dist" + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "declarationDir": "dist/types", + "outDir": "dist/es", + "typeRoots": [ + "node_modules/@types" + ], + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true }, "include": [ - "src/**/*" + "src" ] } From e314aa1651941931efc38b494ad3247dbd04fb42 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 6 Jan 2018 10:05:54 -0500 Subject: [PATCH 0011/1626] package description --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9d1ea5c341..5220ca4a95 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "@bitwarden/jslib", "version": "0.0.11", - "description": "", - "keywords": [], + "description": "Common code used across bitwarden JavaScript projects.", + "keywords": [ "bitwarden" ], "main": "dist/index.umd.js", "module": "dist/index.es5.js", "typings": "dist/types/index.d.ts", From a26fbbc6b19ce70095d7a1144ac9ec89bd3adf92 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 6 Jan 2018 10:23:16 -0500 Subject: [PATCH 0012/1626] formatting --- package.json | 6 ++++-- rollup.config.ts | 44 ++++++++++++++++++++++---------------------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 5220ca4a95..e3b4287290 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,9 @@ "name": "@bitwarden/jslib", "version": "0.0.11", "description": "Common code used across bitwarden JavaScript projects.", - "keywords": [ "bitwarden" ], + "keywords": [ + "bitwarden" + ], "main": "dist/index.umd.js", "module": "dist/index.es5.js", "typings": "dist/types/index.d.ts", @@ -15,7 +17,7 @@ }, "scripts": { "lint": "tslint 'src/**/*.ts'", - "prebuild": "rimraf dist", + "prebuild": "rimraf dist/**/*", "build": "tsc && tsc --module commonjs --outDir dist/lib && rollup -c rollup.config.ts && typedoc --out dist/docs --target es6 --theme minimal --mode file src", "start": "concurrently \"tsc -w\" \"rollup -c rollup.config.ts -w\"" }, diff --git a/rollup.config.ts b/rollup.config.ts index dfe12895ed..32b87a0af2 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -5,30 +5,30 @@ import resolve from 'rollup-plugin-node-resolve'; import sourceMaps from 'rollup-plugin-sourcemaps'; const pkg = require('./package.json'); - const libraryName = 'index'; export default { - input: `dist/es/${libraryName}.js`, - output: [ - { file: pkg.main, name: libraryName, format: 'umd' }, - { file: pkg.module, format: 'es' }, - ], - sourcemap: true, - // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash') - external: [], - watch: { - include: 'dist/es/**', - }, - plugins: [ - // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) - commonjs(), - // Allow node_modules resolution, so you can use 'external' to control - // which external modules to include in the bundle - // https://github.com/rollup/rollup-plugin-node-resolve#usage - resolve(), + input: `dist/es/${libraryName}.js`, + output: [ + { file: pkg.main, name: libraryName, format: 'umd' }, + { file: pkg.module, format: 'es' }, + ], + sourcemap: true, + // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash') + external: [], + watch: { + include: 'dist/es/**', + }, + plugins: [ + // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) + commonjs(), - // Resolve source maps to the original source - sourceMaps(), - ], + // Allow node_modules resolution, so you can use 'external' to control + // which external modules to include in the bundle + // https://github.com/rollup/rollup-plugin-node-resolve#usage + resolve(), + + // Resolve source maps to the original source + sourceMaps(), + ], }; From 1cebe27dbee59a789891ac7831ba458a79b9be02 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 6 Jan 2018 11:39:46 -0500 Subject: [PATCH 0013/1626] remove rollup and just build for es --- package.json | 21 ++++++--------------- rollup.config.ts | 34 ---------------------------------- 2 files changed, 6 insertions(+), 49 deletions(-) delete mode 100644 rollup.config.ts diff --git a/package.json b/package.json index e3b4287290..acf4010e15 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,25 @@ { "name": "@bitwarden/jslib", - "version": "0.0.11", + "version": "0.0.12", "description": "Common code used across bitwarden JavaScript projects.", "keywords": [ "bitwarden" ], - "main": "dist/index.umd.js", - "module": "dist/index.es5.js", + "module": "dist/es/index.js", "typings": "dist/types/index.d.ts", "files": [ "dist" ], "license": "GPL-3.0", - "engines": { - "node": ">=6.0.0" - }, "scripts": { - "lint": "tslint 'src/**/*.ts'", "prebuild": "rimraf dist/**/*", - "build": "tsc && tsc --module commonjs --outDir dist/lib && rollup -c rollup.config.ts && typedoc --out dist/docs --target es6 --theme minimal --mode file src", - "start": "concurrently \"tsc -w\" \"rollup -c rollup.config.ts -w\"" + "build": "tsc && typedoc --out dist/docs --target es6 --theme minimal --mode file src", + "start": "tsc -watch", + "lint": "tslint src/**/*.ts || true", + "lint:fix": "tslint src/**/*.ts --fix" }, "devDependencies": { - "@types/node": "^8.0.0", - "concurrently": "^3.5.1", "rimraf": "^2.6.2", - "rollup": "^0.53.0", - "rollup-plugin-commonjs": "^8.2.6", - "rollup-plugin-node-resolve": "^3.0.0", - "rollup-plugin-sourcemaps": "^0.4.2", "tslint": "^5.8.0", "typedoc": "^0.9.0", "typescript": "^2.6.2" diff --git a/rollup.config.ts b/rollup.config.ts deleted file mode 100644 index 32b87a0af2..0000000000 --- a/rollup.config.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Based upon https://github.com/alexjoverm/typescript-library-starter/blob/master/rollup.config.ts - -import commonjs from 'rollup-plugin-commonjs'; -import resolve from 'rollup-plugin-node-resolve'; -import sourceMaps from 'rollup-plugin-sourcemaps'; - -const pkg = require('./package.json'); -const libraryName = 'index'; - -export default { - input: `dist/es/${libraryName}.js`, - output: [ - { file: pkg.main, name: libraryName, format: 'umd' }, - { file: pkg.module, format: 'es' }, - ], - sourcemap: true, - // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash') - external: [], - watch: { - include: 'dist/es/**', - }, - plugins: [ - // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) - commonjs(), - - // Allow node_modules resolution, so you can use 'external' to control - // which external modules to include in the bundle - // https://github.com/rollup/rollup-plugin-node-resolve#usage - resolve(), - - // Resolve source maps to the original source - sourceMaps(), - ], -}; From 415c56bc2c7a350eb0280c9d772f65ba54a4a286 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 6 Jan 2018 13:58:24 -0500 Subject: [PATCH 0014/1626] update package --- package.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index acf4010e15..ffe4e1a62d 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,22 @@ { "name": "@bitwarden/jslib", - "version": "0.0.12", + "version": "0.0.13", "description": "Common code used across bitwarden JavaScript projects.", "keywords": [ "bitwarden" ], + "author": "8bit Solutions LLC", + "homepage": "https://bitwarden.com", + "repository": { + "type": "git", + "url": "https://github.com/bitwarden/jslib" + }, + "license": "GPL-3.0", "module": "dist/es/index.js", "typings": "dist/types/index.d.ts", "files": [ "dist" ], - "license": "GPL-3.0", "scripts": { "prebuild": "rimraf dist/**/*", "build": "tsc && typedoc --out dist/docs --target es6 --theme minimal --mode file src", From 0f0b092ed7cc47202a486b8d84ada10258e58396 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 6 Jan 2018 14:27:30 -0500 Subject: [PATCH 0015/1626] add more enums to jslib --- src/enums/deviceType.enum.ts | 8 ++++++++ src/enums/fieldType.enum.ts | 5 +++++ src/enums/secureNoteType.enum.ts | 3 +++ src/index.ts | 3 +++ 4 files changed, 19 insertions(+) create mode 100644 src/enums/deviceType.enum.ts create mode 100644 src/enums/fieldType.enum.ts create mode 100644 src/enums/secureNoteType.enum.ts diff --git a/src/enums/deviceType.enum.ts b/src/enums/deviceType.enum.ts new file mode 100644 index 0000000000..3c00cf9ae2 --- /dev/null +++ b/src/enums/deviceType.enum.ts @@ -0,0 +1,8 @@ +export enum DeviceType { + Chrome = 2, + Firefox = 3, + Opera = 4, + Edge = 5, + Vivaldi = 19, + Safari = 20, +} diff --git a/src/enums/fieldType.enum.ts b/src/enums/fieldType.enum.ts new file mode 100644 index 0000000000..c28b26c1da --- /dev/null +++ b/src/enums/fieldType.enum.ts @@ -0,0 +1,5 @@ +export enum FieldType { + Text = 0, + Hidden = 1, + Boolean = 2, +} diff --git a/src/enums/secureNoteType.enum.ts b/src/enums/secureNoteType.enum.ts new file mode 100644 index 0000000000..c7f3e44a78 --- /dev/null +++ b/src/enums/secureNoteType.enum.ts @@ -0,0 +1,3 @@ +export enum SecureNoteType { + Generic = 0, +} diff --git a/src/index.ts b/src/index.ts index 2fd4b680bb..6657b1a4f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,5 @@ export { CipherType } from './enums/cipherType.enum'; +export { DeviceType } from './enums/deviceType.enum'; export { EncryptionType } from './enums/encryptionType.enum'; +export { FieldType } from './enums/fieldType.enum'; +export { SecureNoteType } from './enums/secureNoteType.enum'; From 108eafaea3b564b4b96cc4e1b8f080f99b70d05c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 6 Jan 2018 15:47:23 -0500 Subject: [PATCH 0016/1626] moved service abstractions to lib --- src/index.ts | 5 +++++ src/services/abstractions/messaging.service.ts | 3 +++ .../abstractions/platformUtils.service.ts | 18 ++++++++++++++++++ src/services/abstractions/storage.service.ts | 5 +++++ src/services/abstractions/utils.service.ts | 4 ++++ 5 files changed, 35 insertions(+) create mode 100644 src/services/abstractions/messaging.service.ts create mode 100644 src/services/abstractions/platformUtils.service.ts create mode 100644 src/services/abstractions/storage.service.ts create mode 100644 src/services/abstractions/utils.service.ts diff --git a/src/index.ts b/src/index.ts index 6657b1a4f7..76e0a577cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,3 +3,8 @@ export { DeviceType } from './enums/deviceType.enum'; export { EncryptionType } from './enums/encryptionType.enum'; export { FieldType } from './enums/fieldType.enum'; export { SecureNoteType } from './enums/secureNoteType.enum'; + +export { MessagingService } from './services/abstractions/messaging.service'; +export { PlatformUtilsService } from './services/abstractions/platformUtils.service'; +export { StorageService } from './services/abstractions/storage.service'; +export { UtilsService } from './services/abstractions/utils.service'; diff --git a/src/services/abstractions/messaging.service.ts b/src/services/abstractions/messaging.service.ts new file mode 100644 index 0000000000..6bafce93e4 --- /dev/null +++ b/src/services/abstractions/messaging.service.ts @@ -0,0 +1,3 @@ +export interface MessagingService { + send(subscriber: string, arg?: any): void; +} diff --git a/src/services/abstractions/platformUtils.service.ts b/src/services/abstractions/platformUtils.service.ts new file mode 100644 index 0000000000..063c18dcdd --- /dev/null +++ b/src/services/abstractions/platformUtils.service.ts @@ -0,0 +1,18 @@ +import { DeviceType } from '../../enums/deviceType.enum'; + +export interface PlatformUtilsService { + getDevice(): DeviceType; + getDeviceString(): string; + isFirefox(): boolean; + isChrome(): boolean; + isEdge(): boolean; + isOpera(): boolean; + analyticsId(): string; + initListSectionItemListeners(doc: Document, angular: any): void; + getDomain(uriString: string): string; + inSidebar(theWindow: Window): boolean; + inTab(theWindow: Window): boolean; + inPopout(theWindow: Window): boolean; + inPopup(theWindow: Window): boolean; + isViewOpen(): boolean; +} diff --git a/src/services/abstractions/storage.service.ts b/src/services/abstractions/storage.service.ts new file mode 100644 index 0000000000..5d070d0a55 --- /dev/null +++ b/src/services/abstractions/storage.service.ts @@ -0,0 +1,5 @@ +export interface StorageService { + get(key: string): Promise; + save(key: string, obj: any): Promise; + remove(key: string): Promise; +} diff --git a/src/services/abstractions/utils.service.ts b/src/services/abstractions/utils.service.ts new file mode 100644 index 0000000000..a251eedeaa --- /dev/null +++ b/src/services/abstractions/utils.service.ts @@ -0,0 +1,4 @@ +export interface UtilsService { + copyToClipboard(text: string, doc?: Document): void; + getHostname(uriString: string): string; +} From 2b4cd3fba71c2382a5cfab0549c8e173a457ebfb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 6 Jan 2018 22:13:11 -0500 Subject: [PATCH 0017/1626] re-organize into "barrels". add utils service --- src/abstractions/index.ts | 4 + .../abstractions/messaging.service.ts | 0 .../abstractions/platformUtils.service.ts | 2 +- .../abstractions/storage.service.ts | 0 .../abstractions/utils.service.ts | 0 .../{cipherType.enum.ts => cipherType.ts} | 0 .../{deviceType.enum.ts => deviceType.ts} | 0 ...cryptionType.enum.ts => encryptionType.ts} | 0 src/enums/{fieldType.enum.ts => fieldType.ts} | 0 src/enums/index.ts | 5 + ...cureNoteType.enum.ts => secureNoteType.ts} | 0 src/globals.d.ts | 2 + src/index.ts | 13 +- src/services/index.ts | 1 + src/services/utils.service.ts | 155 ++++++++++++++++++ tsconfig.json | 1 - 16 files changed, 172 insertions(+), 11 deletions(-) create mode 100644 src/abstractions/index.ts rename src/{services => }/abstractions/messaging.service.ts (100%) rename src/{services => }/abstractions/platformUtils.service.ts (90%) rename src/{services => }/abstractions/storage.service.ts (100%) rename src/{services => }/abstractions/utils.service.ts (100%) rename src/enums/{cipherType.enum.ts => cipherType.ts} (100%) rename src/enums/{deviceType.enum.ts => deviceType.ts} (100%) rename src/enums/{encryptionType.enum.ts => encryptionType.ts} (100%) rename src/enums/{fieldType.enum.ts => fieldType.ts} (100%) create mode 100644 src/enums/index.ts rename src/enums/{secureNoteType.enum.ts => secureNoteType.ts} (100%) create mode 100644 src/globals.d.ts create mode 100644 src/services/index.ts create mode 100644 src/services/utils.service.ts diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts new file mode 100644 index 0000000000..664cbb0dcf --- /dev/null +++ b/src/abstractions/index.ts @@ -0,0 +1,4 @@ +export { MessagingService } from './messaging.service'; +export { PlatformUtilsService } from './platformUtils.service'; +export { StorageService } from './storage.service'; +export { UtilsService } from './utils.service'; diff --git a/src/services/abstractions/messaging.service.ts b/src/abstractions/messaging.service.ts similarity index 100% rename from src/services/abstractions/messaging.service.ts rename to src/abstractions/messaging.service.ts diff --git a/src/services/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts similarity index 90% rename from src/services/abstractions/platformUtils.service.ts rename to src/abstractions/platformUtils.service.ts index 063c18dcdd..f4b29c23c5 100644 --- a/src/services/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -1,4 +1,4 @@ -import { DeviceType } from '../../enums/deviceType.enum'; +import { DeviceType } from '../enums/deviceType.enum'; export interface PlatformUtilsService { getDevice(): DeviceType; diff --git a/src/services/abstractions/storage.service.ts b/src/abstractions/storage.service.ts similarity index 100% rename from src/services/abstractions/storage.service.ts rename to src/abstractions/storage.service.ts diff --git a/src/services/abstractions/utils.service.ts b/src/abstractions/utils.service.ts similarity index 100% rename from src/services/abstractions/utils.service.ts rename to src/abstractions/utils.service.ts diff --git a/src/enums/cipherType.enum.ts b/src/enums/cipherType.ts similarity index 100% rename from src/enums/cipherType.enum.ts rename to src/enums/cipherType.ts diff --git a/src/enums/deviceType.enum.ts b/src/enums/deviceType.ts similarity index 100% rename from src/enums/deviceType.enum.ts rename to src/enums/deviceType.ts diff --git a/src/enums/encryptionType.enum.ts b/src/enums/encryptionType.ts similarity index 100% rename from src/enums/encryptionType.enum.ts rename to src/enums/encryptionType.ts diff --git a/src/enums/fieldType.enum.ts b/src/enums/fieldType.ts similarity index 100% rename from src/enums/fieldType.enum.ts rename to src/enums/fieldType.ts diff --git a/src/enums/index.ts b/src/enums/index.ts new file mode 100644 index 0000000000..d1fff1d889 --- /dev/null +++ b/src/enums/index.ts @@ -0,0 +1,5 @@ +export { CipherType } from './cipherType'; +export { DeviceType } from './deviceType'; +export { EncryptionType } from './encryptionType'; +export { FieldType } from './fieldType'; +export { SecureNoteType } from './secureNoteType'; diff --git a/src/enums/secureNoteType.enum.ts b/src/enums/secureNoteType.ts similarity index 100% rename from src/enums/secureNoteType.enum.ts rename to src/enums/secureNoteType.ts diff --git a/src/globals.d.ts b/src/globals.d.ts new file mode 100644 index 0000000000..4859a0869e --- /dev/null +++ b/src/globals.d.ts @@ -0,0 +1,2 @@ +declare function escape(s: string): string; +declare function unescape(s: string): string; diff --git a/src/index.ts b/src/index.ts index 76e0a577cd..9d88a216d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,5 @@ -export { CipherType } from './enums/cipherType.enum'; -export { DeviceType } from './enums/deviceType.enum'; -export { EncryptionType } from './enums/encryptionType.enum'; -export { FieldType } from './enums/fieldType.enum'; -export { SecureNoteType } from './enums/secureNoteType.enum'; +import * as Abstractions from './abstractions'; +import * as Enums from './enums'; +import * as Services from './services'; -export { MessagingService } from './services/abstractions/messaging.service'; -export { PlatformUtilsService } from './services/abstractions/platformUtils.service'; -export { StorageService } from './services/abstractions/storage.service'; -export { UtilsService } from './services/abstractions/utils.service'; +export { Abstractions, Enums, Services }; diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 0000000000..f93851e07e --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1 @@ +export { UtilsService } from './utils.service'; diff --git a/src/services/utils.service.ts b/src/services/utils.service.ts new file mode 100644 index 0000000000..359a743a91 --- /dev/null +++ b/src/services/utils.service.ts @@ -0,0 +1,155 @@ +import { UtilsService as UtilsServiceAbstraction } from '../abstractions/utils.service'; + +export class UtilsService implements UtilsServiceAbstraction { + static copyToClipboard(text: string, doc?: Document): void { + doc = doc || document; + if ((window as any).clipboardData && (window as any).clipboardData.setData) { + // IE specific code path to prevent textarea being shown while dialog is visible. + (window as any).clipboardData.setData('Text', text); + } else if (doc.queryCommandSupported && doc.queryCommandSupported('copy')) { + const textarea = doc.createElement('textarea'); + textarea.textContent = text; + // Prevent scrolling to bottom of page in MS Edge. + textarea.style.position = 'fixed'; + doc.body.appendChild(textarea); + textarea.select(); + + try { + // Security exception may be thrown by some browsers. + doc.execCommand('copy'); + } catch (e) { + // tslint:disable-next-line + console.warn('Copy to clipboard failed.', e); + } finally { + doc.body.removeChild(textarea); + } + } + } + + static urlBase64Decode(str: string): string { + let output = str.replace(/-/g, '+').replace(/_/g, '/'); + switch (output.length % 4) { + case 0: + break; + case 2: + output += '=='; + break; + case 3: + output += '='; + break; + default: + throw new Error('Illegal base64url string!'); + } + + return decodeURIComponent(escape(window.atob(output))); + } + + // ref: http://stackoverflow.com/a/2117523/1090359 + static newGuid(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + // tslint:disable-next-line + const r = Math.random() * 16 | 0; + // tslint:disable-next-line + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } + + // EFForg/OpenWireless + // ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js + static secureRandomNumber(min: number, max: number): number { + let rval = 0; + const range = max - min + 1; + const bitsNeeded = Math.ceil(Math.log2(range)); + if (bitsNeeded > 53) { + throw new Error('We cannot generate numbers larger than 53 bits.'); + } + + const bytesNeeded = Math.ceil(bitsNeeded / 8); + const mask = Math.pow(2, bitsNeeded) - 1; + // 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111 + + // Create byte array and fill with N random numbers + const byteArray = new Uint8Array(bytesNeeded); + window.crypto.getRandomValues(byteArray); + + let p = (bytesNeeded - 1) * 8; + for (let i = 0; i < bytesNeeded; i++) { + rval += byteArray[i] * Math.pow(2, p); + p -= 8; + } + + // Use & to apply the mask and reduce the number of recursive lookups + // tslint:disable-next-line + rval = rval & mask; + + if (rval >= range) { + // Integer out of acceptable range + return UtilsService.secureRandomNumber(min, max); + } + + // Return an integer that falls within the range + return min + rval; + } + + static fromB64ToArray(str: string): Uint8Array { + const binaryString = window.atob(str); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; + } + + static fromUtf8ToArray(str: string): Uint8Array { + const strUtf8 = unescape(encodeURIComponent(str)); + const arr = new Uint8Array(strUtf8.length); + for (let i = 0; i < strUtf8.length; i++) { + arr[i] = strUtf8.charCodeAt(i); + } + return arr; + } + + static fromBufferToB64(buffer: ArrayBuffer): string { + let binary = ''; + const bytes = new Uint8Array(buffer); + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + return window.btoa(binary); + } + + static fromBufferToUtf8(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer); + const encodedString = String.fromCharCode.apply(null, bytes); + return decodeURIComponent(escape(encodedString)); + } + + static getHostname(uriString: string): string { + if (uriString == null) { + return null; + } + + uriString = uriString.trim(); + if (uriString === '') { + return null; + } + + if (uriString.startsWith('http://') || uriString.startsWith('https://')) { + try { + const url = new URL(uriString); + return url.hostname; + } catch (e) { } + } + + return null; + } + + getHostname(uriString: string): string { + return UtilsService.getHostname(uriString); + } + + copyToClipboard(text: string, doc?: Document) { + UtilsService.copyToClipboard(text, doc); + } +} diff --git a/tsconfig.json b/tsconfig.json index 0db9afbea2..60f0d3494f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,6 @@ "moduleResolution": "node", "target": "ES2016", "module": "es6", - "strict": true, "sourceMap": true, "declaration": true, "allowSyntheticDefaultImports": true, From 66aa1a783dc804eab82c6317081d0c183ec1ba31 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 6 Jan 2018 22:19:10 -0500 Subject: [PATCH 0018/1626] new script for publishing --- package.json | 5 +++-- src/abstractions/platformUtils.service.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index ffe4e1a62d..8303323515 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/jslib", - "version": "0.0.13", + "version": "0.0.14", "description": "Common code used across bitwarden JavaScript projects.", "keywords": [ "bitwarden" @@ -22,7 +22,8 @@ "build": "tsc && typedoc --out dist/docs --target es6 --theme minimal --mode file src", "start": "tsc -watch", "lint": "tslint src/**/*.ts || true", - "lint:fix": "tslint src/**/*.ts --fix" + "lint:fix": "tslint src/**/*.ts --fix", + "publish": "npm run build && npm publish --access public" }, "devDependencies": { "rimraf": "^2.6.2", diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index f4b29c23c5..7e500f684b 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -1,4 +1,4 @@ -import { DeviceType } from '../enums/deviceType.enum'; +import { DeviceType } from '../enums/deviceType'; export interface PlatformUtilsService { getDevice(): DeviceType; From af9b95936fb93a57be06006077d3e15eae2a5b86 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 6 Jan 2018 23:22:54 -0500 Subject: [PATCH 0019/1626] pub script --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8303323515..572fa374f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/jslib", - "version": "0.0.14", + "version": "0.0.18", "description": "Common code used across bitwarden JavaScript projects.", "keywords": [ "bitwarden" @@ -23,7 +23,7 @@ "start": "tsc -watch", "lint": "tslint src/**/*.ts || true", "lint:fix": "tslint src/**/*.ts --fix", - "publish": "npm run build && npm publish --access public" + "pub": "npm run build && npm publish --access public" }, "devDependencies": { "rimraf": "^2.6.2", From 7c848edf3ca72d6ee37cfacf0b00b7dbda37a523 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 8 Jan 2018 14:11:28 -0500 Subject: [PATCH 0020/1626] added models, crypto, and constants services --- package.json | 5 + src/abstractions/crypto.service.ts | 28 + src/abstractions/index.ts | 1 + src/index.ts | 6 +- src/models/data/attachmentData.ts | 20 + src/models/data/cardData.ts | 20 + src/models/data/cipherData.ts | 87 +++ src/models/data/collectionData.ts | 16 + src/models/data/fieldData.ts | 16 + src/models/data/folderData.ts | 18 + src/models/data/identityData.ts | 44 ++ src/models/data/index.ts | 9 + src/models/data/loginData.ts | 16 + src/models/data/secureNoteData.ts | 12 + src/models/domain/attachment.ts | 43 ++ src/models/domain/card.ts | 43 ++ src/models/domain/cipher.ts | 192 ++++++ src/models/domain/cipherString.ts | 116 ++++ src/models/domain/collection.ts | 37 ++ src/models/domain/domain.ts | 46 ++ src/models/domain/encryptedObject.ts | 8 + src/models/domain/environmentUrls.ts | 5 + src/models/domain/field.ts | 39 ++ src/models/domain/folder.ts | 34 + src/models/domain/identity.ts | 79 +++ src/models/domain/index.ts | 15 + src/models/domain/login.ts | 37 ++ src/models/domain/passwordHistory.ts | 9 + src/models/domain/secureNote.ts | 27 + src/models/domain/symmetricCryptoKey.ts | 80 +++ .../domain/symmetricCryptoKeyBuffers.ts | 9 + src/models/request/cipherRequest.ts | 89 +++ src/models/request/deviceRequest.ts | 20 + src/models/request/deviceTokenRequest.ts | 10 + src/models/request/folderRequest.ts | 12 + src/models/request/index.ts | 8 + src/models/request/passwordHintRequest.ts | 10 + src/models/request/registerRequest.ts | 18 + src/models/request/tokenRequest.ts | 49 ++ src/models/request/twoFactorEmailRequest.ts | 12 + src/models/response/attachmentResponse.ts | 18 + src/models/response/cipherResponse.ts | 44 ++ src/models/response/collectionResponse.ts | 14 + src/models/response/deviceResponse.ts | 20 + src/models/response/domainsResponse.ts | 20 + src/models/response/errorResponse.ts | 37 ++ src/models/response/folderResponse.ts | 14 + src/models/response/globalDomainResponse.ts | 14 + src/models/response/identityTokenResponse.ts | 24 + src/models/response/index.ts | 14 + src/models/response/keysResponse.ts | 12 + src/models/response/listResponse.ts | 10 + .../response/profileOrganizationResponse.ts | 30 + src/models/response/profileResponse.ts | 39 ++ src/models/response/syncResponse.ts | 44 ++ src/services/constants.service.ts | 116 ++++ src/services/crypto.service.ts | 602 ++++++++++++++++++ src/services/index.ts | 2 + tsconfig.json | 6 +- 59 files changed, 2419 insertions(+), 6 deletions(-) create mode 100644 src/abstractions/crypto.service.ts create mode 100644 src/models/data/attachmentData.ts create mode 100644 src/models/data/cardData.ts create mode 100644 src/models/data/cipherData.ts create mode 100644 src/models/data/collectionData.ts create mode 100644 src/models/data/fieldData.ts create mode 100644 src/models/data/folderData.ts create mode 100644 src/models/data/identityData.ts create mode 100644 src/models/data/index.ts create mode 100644 src/models/data/loginData.ts create mode 100644 src/models/data/secureNoteData.ts create mode 100644 src/models/domain/attachment.ts create mode 100644 src/models/domain/card.ts create mode 100644 src/models/domain/cipher.ts create mode 100644 src/models/domain/cipherString.ts create mode 100644 src/models/domain/collection.ts create mode 100644 src/models/domain/domain.ts create mode 100644 src/models/domain/encryptedObject.ts create mode 100644 src/models/domain/environmentUrls.ts create mode 100644 src/models/domain/field.ts create mode 100644 src/models/domain/folder.ts create mode 100644 src/models/domain/identity.ts create mode 100644 src/models/domain/index.ts create mode 100644 src/models/domain/login.ts create mode 100644 src/models/domain/passwordHistory.ts create mode 100644 src/models/domain/secureNote.ts create mode 100644 src/models/domain/symmetricCryptoKey.ts create mode 100644 src/models/domain/symmetricCryptoKeyBuffers.ts create mode 100644 src/models/request/cipherRequest.ts create mode 100644 src/models/request/deviceRequest.ts create mode 100644 src/models/request/deviceTokenRequest.ts create mode 100644 src/models/request/folderRequest.ts create mode 100644 src/models/request/index.ts create mode 100644 src/models/request/passwordHintRequest.ts create mode 100644 src/models/request/registerRequest.ts create mode 100644 src/models/request/tokenRequest.ts create mode 100644 src/models/request/twoFactorEmailRequest.ts create mode 100644 src/models/response/attachmentResponse.ts create mode 100644 src/models/response/cipherResponse.ts create mode 100644 src/models/response/collectionResponse.ts create mode 100644 src/models/response/deviceResponse.ts create mode 100644 src/models/response/domainsResponse.ts create mode 100644 src/models/response/errorResponse.ts create mode 100644 src/models/response/folderResponse.ts create mode 100644 src/models/response/globalDomainResponse.ts create mode 100644 src/models/response/identityTokenResponse.ts create mode 100644 src/models/response/index.ts create mode 100644 src/models/response/keysResponse.ts create mode 100644 src/models/response/listResponse.ts create mode 100644 src/models/response/profileOrganizationResponse.ts create mode 100644 src/models/response/profileResponse.ts create mode 100644 src/models/response/syncResponse.ts create mode 100644 src/services/constants.service.ts create mode 100644 src/services/crypto.service.ts diff --git a/package.json b/package.json index 572fa374f4..c7017da888 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 0000000000..10b4e1ac4e --- /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 664cbb0dcf..6191317b07 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 9d88a216d8..2edf7e05e4 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 0000000000..2ff3ab423c --- /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 0000000000..f0b9f63f2e --- /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 0000000000..a0917ed314 --- /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 0000000000..f2d5fc9f03 --- /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 0000000000..920d1c56fa --- /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 0000000000..6f03781cc7 --- /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 0000000000..ee849166ee --- /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 0000000000..7a5a86df2e --- /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 0000000000..de0aecc133 --- /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 0000000000..bf861a4a60 --- /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 0000000000..d77152b007 --- /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 0000000000..3e42f7affb --- /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 0000000000..5c3c1076d9 --- /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 0000000000..eb37e08df0 --- /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 0000000000..0a5079e53a --- /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 0000000000..cdb220776b --- /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 0000000000..e28911e384 --- /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 0000000000..868cd43218 --- /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 0000000000..c806e2dd12 --- /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 0000000000..180cd44e4f --- /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 0000000000..ede933ac30 --- /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 0000000000..698585a3c9 --- /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 0000000000..8ed1e1f7c7 --- /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 0000000000..35e7892dcd --- /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 0000000000..46609a5b70 --- /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 0000000000..093685f053 --- /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 0000000000..f27d8aed2e --- /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 0000000000..4829b20d27 --- /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 0000000000..5aaec5c77e --- /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 0000000000..69ef20bb9a --- /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 0000000000..7ff9794b18 --- /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 0000000000..51acd28f4c --- /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 0000000000..4feb92944b --- /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 0000000000..4b80fb7884 --- /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 0000000000..4cf7564615 --- /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 0000000000..d540b08ecf --- /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 0000000000..df5138650c --- /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 0000000000..b2e597e23a --- /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 0000000000..8b0247720d --- /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 0000000000..5e76635d16 --- /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 0000000000..d65077147b --- /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 0000000000..4d976932cd --- /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 0000000000..c5ff0ada70 --- /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 0000000000..8e9a45df11 --- /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 0000000000..2d188707c0 --- /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 0000000000..00f85d119d --- /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 0000000000..cb96dad51e --- /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 0000000000..9cf5455ada --- /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 0000000000..484857745a --- /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 0000000000..69e4f3e9cb --- /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 0000000000..8acdc5202d --- /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 0000000000..30206c902c --- /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 0000000000..adce2e7911 --- /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 f93851e07e..81f2bb9e3d 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 60f0d3494f..e647a0bd85 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" From 1d9f28e18548593cbc1027db432ed88233d7528f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 8 Jan 2018 16:21:49 -0500 Subject: [PATCH 0021/1626] update crypto interface --- src/abstractions/crypto.service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 10b4e1ac4e..3a7e406f1a 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -15,6 +15,11 @@ export interface CryptoService { getPrivateKey(): Promise; getOrgKeys(): Promise>; getOrgKey(orgId: string): Promise; + clearKey(): Promise; + clearKeyHash(): Promise; + clearEncKey(memoryOnly?: boolean): Promise; + clearPrivateKey(memoryOnly?: boolean): Promise; + clearOrgKeys(memoryOnly?: boolean): Promise; clearKeys(): Promise; toggleKey(): Promise; makeKey(password: string, salt: string): SymmetricCryptoKey; From 2b387058bdcd84467b01ee2a0ea0be2dbb83bb1e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 8 Jan 2018 16:22:05 -0500 Subject: [PATCH 0022/1626] rename exported models --- src/models/data/index.ts | 18 +++++++++--------- src/models/request/index.ts | 16 ++++++++-------- src/models/response/index.ts | 28 ++++++++++++++-------------- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/models/data/index.ts b/src/models/data/index.ts index 7a5a86df2e..bd1ae1b6a9 100644 --- a/src/models/data/index.ts +++ b/src/models/data/index.ts @@ -1,9 +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'; +export { AttachmentData as Attachment } from './attachmentData'; +export { CardData as Card } from './cardData'; +export { CipherData as Cipher } from './cipherData'; +export { CollectionData as Collection } from './collectionData'; +export { FieldData as Field } from './fieldData'; +export { FolderData as Folder } from './folderData'; +export { IdentityData as Identity } from './identityData'; +export { LoginData as Login } from './loginData'; +export { SecureNoteData as SecureNote } from './secureNoteData'; diff --git a/src/models/request/index.ts b/src/models/request/index.ts index 51acd28f4c..eb2977b41e 100644 --- a/src/models/request/index.ts +++ b/src/models/request/index.ts @@ -1,8 +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'; +export { CipherRequest as Cipher } from './cipherRequest'; +export { DeviceRequest as Device } from './deviceRequest'; +export { DeviceTokenRequest as DeviceToken } from './deviceTokenRequest'; +export { FolderRequest as Folder } from './folderRequest'; +export { PasswordHintRequest as PasswordHint } from './passwordHintRequest'; +export { RegisterRequest as Register } from './registerRequest'; +export { TokenRequest as Token } from './tokenRequest'; +export { TwoFactorEmailRequest as TwoFactorEmail } from './twoFactorEmailRequest'; diff --git a/src/models/response/index.ts b/src/models/response/index.ts index 00f85d119d..35c5f13af9 100644 --- a/src/models/response/index.ts +++ b/src/models/response/index.ts @@ -1,14 +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'; +export { AttachmentResponse as Attachment } from './attachmentResponse'; +export { CipherResponse as Cipher } from './cipherResponse'; +export { CollectionResponse as Collection } from './collectionResponse'; +export { DeviceResponse as Device } from './deviceResponse'; +export { DomainsResponse as Domains } from './domainsResponse'; +export { ErrorResponse as Error } from './errorResponse'; +export { FolderResponse as Folder } from './folderResponse'; +export { GlobalDomainResponse as GlobalDomain } from './globalDomainResponse'; +export { IdentityTokenResponse as IdentityToken } from './identityTokenResponse'; +export { KeysResponse as Keys } from './keysResponse'; +export { ListResponse as List } from './listResponse'; +export { ProfileOrganizationResponse as ProfileOrganization } from './profileOrganizationResponse'; +export { ProfileResponse as Profile } from './profileResponse'; +export { SyncResponse as Sync } from './syncResponse'; From 5c243e01eca82ddc169ebe91ae34a9465d043db9 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 8 Jan 2018 16:22:19 -0500 Subject: [PATCH 0023/1626] fix constants export --- src/services/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/index.ts b/src/services/index.ts index 81f2bb9e3d..87ec7493d2 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,3 +1,3 @@ -export { ConstantsService } from './crypto.service'; +export { ConstantsService } from './constants.service'; export { CryptoService } from './crypto.service'; export { UtilsService } from './utils.service'; From 005ec447a7781f63e49bb92c0837e47656f99b34 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 8 Jan 2018 16:22:31 -0500 Subject: [PATCH 0024/1626] unused service test --- src/index.ts | 2 ++ src/services/unused.service.ts | 5 +++++ 2 files changed, 7 insertions(+) create mode 100644 src/services/unused.service.ts diff --git a/src/index.ts b/src/index.ts index 2edf7e05e4..35287cfbeb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,3 +7,5 @@ import * as Response from './models/response'; import * as Services from './services'; export { Abstractions, Enums, Data, Domain, Request, Response, Services }; + +export { UnusedService } from './services/unused.service'; diff --git a/src/services/unused.service.ts b/src/services/unused.service.ts new file mode 100644 index 0000000000..90f4079eb4 --- /dev/null +++ b/src/services/unused.service.ts @@ -0,0 +1,5 @@ +export class UnusedService { + private unusedMethod(): string { + return 'unusedstring'; + } +} From 0c202037fb7a1b4b9b0166acd8d068e7d88e8df3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 8 Jan 2018 16:56:53 -0500 Subject: [PATCH 0025/1626] package references ts directly --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index c7017da888..31de738d7e 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,10 @@ "url": "https://github.com/bitwarden/jslib" }, "license": "GPL-3.0", - "module": "dist/es/index.js", - "typings": "dist/types/index.d.ts", + "module": "src/index.ts", + "typings": "src/index.ts", "files": [ - "dist" + "src" ], "scripts": { "prebuild": "rimraf dist/**/*", From 24e5378c9796962746465145678fc6bab966edbe Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 8 Jan 2018 22:27:25 -0500 Subject: [PATCH 0026/1626] v bump --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 31de738d7e..57ee2d9e5c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/jslib", - "version": "0.0.18", + "version": "0.0.20", "description": "Common code used across bitwarden JavaScript projects.", "keywords": [ "bitwarden" @@ -15,7 +15,8 @@ "module": "src/index.ts", "typings": "src/index.ts", "files": [ - "src" + "src", + "dist" ], "scripts": { "prebuild": "rimraf dist/**/*", From b7a0fbfef5f30b473703e5624fdaddc3de14bfb5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 14:26:29 -0500 Subject: [PATCH 0027/1626] no aliasing --- src/models/data/index.ts | 18 ++++++++-------- src/models/domain/symmetricCryptoKey.ts | 11 +++++----- src/models/request/index.ts | 16 +++++++------- src/models/response/index.ts | 28 ++++++++++++------------- 4 files changed, 37 insertions(+), 36 deletions(-) diff --git a/src/models/data/index.ts b/src/models/data/index.ts index bd1ae1b6a9..7a5a86df2e 100644 --- a/src/models/data/index.ts +++ b/src/models/data/index.ts @@ -1,9 +1,9 @@ -export { AttachmentData as Attachment } from './attachmentData'; -export { CardData as Card } from './cardData'; -export { CipherData as Cipher } from './cipherData'; -export { CollectionData as Collection } from './collectionData'; -export { FieldData as Field } from './fieldData'; -export { FolderData as Folder } from './folderData'; -export { IdentityData as Identity } from './identityData'; -export { LoginData as Login } from './loginData'; -export { SecureNoteData as SecureNote } from './secureNoteData'; +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/domain/symmetricCryptoKey.ts b/src/models/domain/symmetricCryptoKey.ts index 093685f053..9db5740710 100644 --- a/src/models/domain/symmetricCryptoKey.ts +++ b/src/models/domain/symmetricCryptoKey.ts @@ -1,5 +1,3 @@ -import * as forge from 'node-forge'; - import { EncryptionType } from '../../enums/encryptionType'; import { SymmetricCryptoKeyBuffers } from './symmetricCryptoKeyBuffers'; @@ -13,17 +11,20 @@ export class SymmetricCryptoKey { macKey: string; encType: EncryptionType; keyBuf: SymmetricCryptoKeyBuffers; + forge: any; constructor(keyBytes: string, b64KeyBytes?: boolean, encType?: EncryptionType) { + this.forge = (window as any).forge; + if (b64KeyBytes) { - keyBytes = forge.util.decode64(keyBytes); + keyBytes = this.forge.util.decode64(keyBytes); } if (!keyBytes) { throw new Error('Must provide keyBytes'); } - const buffer = (forge as any).util.createBuffer(keyBytes); + const buffer = this.forge.util.createBuffer(keyBytes); if (!buffer || buffer.length() === 0) { throw new Error('Couldn\'t make buffer'); } @@ -41,7 +42,7 @@ export class SymmetricCryptoKey { } this.key = keyBytes; - this.keyB64 = forge.util.encode64(keyBytes); + this.keyB64 = this.forge.util.encode64(keyBytes); this.encType = encType; if (encType === EncryptionType.AesCbc256_B64 && bufferLength === 32) { diff --git a/src/models/request/index.ts b/src/models/request/index.ts index eb2977b41e..51acd28f4c 100644 --- a/src/models/request/index.ts +++ b/src/models/request/index.ts @@ -1,8 +1,8 @@ -export { CipherRequest as Cipher } from './cipherRequest'; -export { DeviceRequest as Device } from './deviceRequest'; -export { DeviceTokenRequest as DeviceToken } from './deviceTokenRequest'; -export { FolderRequest as Folder } from './folderRequest'; -export { PasswordHintRequest as PasswordHint } from './passwordHintRequest'; -export { RegisterRequest as Register } from './registerRequest'; -export { TokenRequest as Token } from './tokenRequest'; -export { TwoFactorEmailRequest as TwoFactorEmail } from './twoFactorEmailRequest'; +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/response/index.ts b/src/models/response/index.ts index 35c5f13af9..00f85d119d 100644 --- a/src/models/response/index.ts +++ b/src/models/response/index.ts @@ -1,14 +1,14 @@ -export { AttachmentResponse as Attachment } from './attachmentResponse'; -export { CipherResponse as Cipher } from './cipherResponse'; -export { CollectionResponse as Collection } from './collectionResponse'; -export { DeviceResponse as Device } from './deviceResponse'; -export { DomainsResponse as Domains } from './domainsResponse'; -export { ErrorResponse as Error } from './errorResponse'; -export { FolderResponse as Folder } from './folderResponse'; -export { GlobalDomainResponse as GlobalDomain } from './globalDomainResponse'; -export { IdentityTokenResponse as IdentityToken } from './identityTokenResponse'; -export { KeysResponse as Keys } from './keysResponse'; -export { ListResponse as List } from './listResponse'; -export { ProfileOrganizationResponse as ProfileOrganization } from './profileOrganizationResponse'; -export { ProfileResponse as Profile } from './profileResponse'; -export { SyncResponse as Sync } from './syncResponse'; +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'; From bf35cf3588f95e8e4daec60c9a48853a73f01cf4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 15:37:51 -0500 Subject: [PATCH 0028/1626] remove unused service test --- src/index.ts | 2 -- src/services/unused.service.ts | 5 ----- 2 files changed, 7 deletions(-) delete mode 100644 src/services/unused.service.ts diff --git a/src/index.ts b/src/index.ts index 35287cfbeb..2edf7e05e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,5 +7,3 @@ import * as Response from './models/response'; import * as Services from './services'; export { Abstractions, Enums, Data, Domain, Request, Response, Services }; - -export { UnusedService } from './services/unused.service'; diff --git a/src/services/unused.service.ts b/src/services/unused.service.ts deleted file mode 100644 index 90f4079eb4..0000000000 --- a/src/services/unused.service.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class UnusedService { - private unusedMethod(): string { - return 'unusedstring'; - } -} From 9a74f3e4eaa3367dacaee4f30d5eeeb7ad162a37 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 15:38:33 -0500 Subject: [PATCH 0029/1626] restore cryptokey class with node ref --- src/models/domain/symmetricCryptoKey.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/models/domain/symmetricCryptoKey.ts b/src/models/domain/symmetricCryptoKey.ts index 9db5740710..4e3a5d6c2d 100644 --- a/src/models/domain/symmetricCryptoKey.ts +++ b/src/models/domain/symmetricCryptoKey.ts @@ -1,3 +1,5 @@ +import * as forge from 'node-forge'; + import { EncryptionType } from '../../enums/encryptionType'; import { SymmetricCryptoKeyBuffers } from './symmetricCryptoKeyBuffers'; @@ -11,20 +13,17 @@ export class SymmetricCryptoKey { macKey: string; encType: EncryptionType; keyBuf: SymmetricCryptoKeyBuffers; - forge: any; constructor(keyBytes: string, b64KeyBytes?: boolean, encType?: EncryptionType) { - this.forge = (window as any).forge; - if (b64KeyBytes) { - keyBytes = this.forge.util.decode64(keyBytes); + keyBytes = forge.util.decode64(keyBytes); } if (!keyBytes) { throw new Error('Must provide keyBytes'); } - const buffer = this.forge.util.createBuffer(keyBytes); + const buffer = (forge as any).util.createBuffer(keyBytes); if (!buffer || buffer.length() === 0) { throw new Error('Couldn\'t make buffer'); } @@ -42,7 +41,7 @@ export class SymmetricCryptoKey { } this.key = keyBytes; - this.keyB64 = this.forge.util.encode64(keyBytes); + this.keyB64 = forge.util.encode64(keyBytes); this.encType = encType; if (encType === EncryptionType.AesCbc256_B64 && bufferLength === 32) { @@ -59,7 +58,7 @@ export class SymmetricCryptoKey { } } - getBuffers() { + getBuffers(): SymmetricCryptoKeyBuffers { if (this.keyBuf) { return this.keyBuf; } From 7ce34ac1399fa041d79c1fe1a57036c3626dd321 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 16:19:55 -0500 Subject: [PATCH 0030/1626] add api, appid, and token services --- src/abstractions/api.service.ts | 37 +++ src/abstractions/appId.service.ts | 4 + src/abstractions/index.ts | 3 + src/abstractions/token.service.ts | 23 ++ src/services/api.service.ts | 447 ++++++++++++++++++++++++++++++ src/services/appId.service.ts | 28 ++ src/services/index.ts | 3 + src/services/token.service.ts | 176 ++++++++++++ 8 files changed, 721 insertions(+) create mode 100644 src/abstractions/api.service.ts create mode 100644 src/abstractions/appId.service.ts create mode 100644 src/abstractions/token.service.ts create mode 100644 src/services/api.service.ts create mode 100644 src/services/appId.service.ts create mode 100644 src/services/token.service.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts new file mode 100644 index 0000000000..9829f5abc2 --- /dev/null +++ b/src/abstractions/api.service.ts @@ -0,0 +1,37 @@ +import { EnvironmentUrls } from '../models/domain/environmentUrls'; + +import { CipherRequest } from '../models/request/cipherRequest'; +import { FolderRequest } from '../models/request/folderRequest'; +import { PasswordHintRequest } from '../models/request/passwordHintRequest'; +import { RegisterRequest } from '../models/request/registerRequest'; +import { TokenRequest } from '../models/request/tokenRequest'; +import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; + +import { CipherResponse } from '../models/response/cipherResponse'; +import { FolderResponse } from '../models/response/folderResponse'; +import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; +import { SyncResponse } from '../models/response/syncResponse'; + +export interface ApiService { + urlsSet: boolean; + baseUrl: string; + identityBaseUrl: string; + deviceType: string; + logoutCallback: Function; + setUrls(urls: EnvironmentUrls); + postIdentityToken(request: TokenRequest): Promise; + refreshIdentityToken(): Promise; + postTwoFactorEmail(request: TwoFactorEmailRequest): Promise; + getAccountRevisionDate(): Promise; + postPasswordHint(request: PasswordHintRequest): Promise; + postRegister(request: RegisterRequest): Promise; + postFolder(request: FolderRequest): Promise; + putFolder(id: string, request: FolderRequest): Promise; + deleteFolder(id: string): Promise; + postCipher(request: CipherRequest): Promise; + putCipher(id: string, request: CipherRequest): Promise; + deleteCipher(id: string): Promise; + postCipherAttachment(id: string, data: FormData): Promise; + deleteCipherAttachment(id: string, attachmentId: string): Promise; + getSync(): Promise; +} diff --git a/src/abstractions/appId.service.ts b/src/abstractions/appId.service.ts new file mode 100644 index 0000000000..72d3b11948 --- /dev/null +++ b/src/abstractions/appId.service.ts @@ -0,0 +1,4 @@ +export interface AppIdService { + getAppId(): Promise; + getAnonymousAppId(): Promise; +} diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index 6191317b07..b3d6ecb527 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -1,5 +1,8 @@ +export { ApiService } from './api.service'; +export { AppIdService } from './appId.service'; export { CryptoService } from './crypto.service'; export { MessagingService } from './messaging.service'; export { PlatformUtilsService } from './platformUtils.service'; export { StorageService } from './storage.service'; +export { TokenService } from './token.service'; export { UtilsService } from './utils.service'; diff --git a/src/abstractions/token.service.ts b/src/abstractions/token.service.ts new file mode 100644 index 0000000000..03f2434c9a --- /dev/null +++ b/src/abstractions/token.service.ts @@ -0,0 +1,23 @@ +export interface TokenService { + token: string; + decodedToken: any; + refreshToken: string; + setTokens(accessToken: string, refreshToken: string): Promise; + setToken(token: string): Promise; + getToken(): Promise; + setRefreshToken(refreshToken: string): Promise; + getRefreshToken(): Promise; + setTwoFactorToken(token: string, email: string): Promise; + getTwoFactorToken(email: string): Promise; + clearTwoFactorToken(email: string): Promise; + clearToken(): Promise; + decodeToken(): any; + getTokenExpirationDate(): Date; + tokenSecondsRemaining(offsetSeconds?: number): number; + tokenNeedsRefresh(minutes?: number): boolean; + getUserId(): string; + getEmail(): string; + getName(): string; + getPremium(): boolean; + getIssuer(): string; +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts new file mode 100644 index 0000000000..5e41da5236 --- /dev/null +++ b/src/services/api.service.ts @@ -0,0 +1,447 @@ +import { ConstantsService } from './constants.service'; + +import { ApiService as ApiServiceInterface } from '../abstractions/api.service'; +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; +import { TokenService } from '../abstractions/token.service'; + +import { EnvironmentUrls } from '../models/domain/environmentUrls'; + +import { CipherRequest } from '../models/request/cipherRequest'; +import { FolderRequest } from '../models/request/folderRequest'; +import { PasswordHintRequest } from '../models/request/passwordHintRequest'; +import { RegisterRequest } from '../models/request/registerRequest'; +import { TokenRequest } from '../models/request/tokenRequest'; +import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; + +import { CipherResponse } from '../models/response/cipherResponse'; +import { ErrorResponse } from '../models/response/errorResponse'; +import { FolderResponse } from '../models/response/folderResponse'; +import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; +import { SyncResponse } from '../models/response/syncResponse'; + +export class ApiService implements ApiServiceInterface { + urlsSet: boolean = false; + baseUrl: string; + identityBaseUrl: string; + deviceType: string; + logoutCallback: Function; + + constructor(private tokenService: TokenService, platformUtilsService: PlatformUtilsService, + logoutCallback: Function) { + this.logoutCallback = logoutCallback; + this.deviceType = platformUtilsService.getDevice().toString(); + } + + setUrls(urls: EnvironmentUrls) { + this.urlsSet = true; + + if (urls.base != null) { + this.baseUrl = urls.base + '/api'; + this.identityBaseUrl = urls.base + '/identity'; + return; + } + + if (urls.api != null && urls.identity != null) { + this.baseUrl = urls.api; + this.identityBaseUrl = urls.identity; + return; + } + + /* tslint:disable */ + // Desktop + //this.baseUrl = 'http://localhost:4000'; + //this.identityBaseUrl = 'http://localhost:33656'; + + // Desktop HTTPS + //this.baseUrl = 'https://localhost:44377'; + //this.identityBaseUrl = 'https://localhost:44392'; + + // Desktop external + //this.baseUrl = 'http://192.168.1.3:4000'; + //this.identityBaseUrl = 'http://192.168.1.3:33656'; + + // Preview + //this.baseUrl = 'https://preview-api.bitwarden.com'; + //this.identityBaseUrl = 'https://preview-identity.bitwarden.com'; + + // Production + this.baseUrl = 'https://api.bitwarden.com'; + this.identityBaseUrl = 'https://identity.bitwarden.com'; + /* tslint:enable */ + } + + // Auth APIs + + async postIdentityToken(request: TokenRequest): Promise { + const response = await fetch(new Request(this.identityBaseUrl + '/connect/token', { + body: this.qsStringify(request.toIdentityToken()), + cache: 'no-cache', + headers: new Headers({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', + 'Accept': 'application/json', + 'Device-Type': this.deviceType, + }), + method: 'POST', + })); + + let responseJson: any = null; + const typeHeader = response.headers.get('content-type'); + if (typeHeader != null && typeHeader.indexOf('application/json') > -1) { + responseJson = await response.json(); + } + + if (responseJson != null) { + if (response.status === 200) { + return new IdentityTokenResponse(responseJson); + } else if (response.status === 400 && responseJson.TwoFactorProviders2 && + Object.keys(responseJson.TwoFactorProviders2).length) { + await this.tokenService.clearTwoFactorToken(request.email); + return responseJson.TwoFactorProviders2; + } + } + + return Promise.reject(new ErrorResponse(responseJson, response.status, true)); + } + + async refreshIdentityToken(): Promise { + try { + await this.doRefreshToken(); + } catch (e) { + return Promise.reject(null); + } + } + + // Two Factor APIs + + async postTwoFactorEmail(request: TwoFactorEmailRequest): Promise { + const response = await fetch(new Request(this.baseUrl + '/two-factor/send-email-login', { + body: JSON.stringify(request), + cache: 'no-cache', + headers: new Headers({ + 'Content-Type': 'application/json; charset=utf-8', + 'Device-Type': this.deviceType, + }), + method: 'POST', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + // Account APIs + + async getAccountRevisionDate(): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/accounts/revision-date', { + cache: 'no-cache', + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Device-Type': this.deviceType, + }), + })); + + if (response.status === 200) { + return (await response.json() as number); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async postPasswordHint(request: PasswordHintRequest): Promise { + const response = await fetch(new Request(this.baseUrl + '/accounts/password-hint', { + body: JSON.stringify(request), + cache: 'no-cache', + headers: new Headers({ + 'Content-Type': 'application/json; charset=utf-8', + 'Device-Type': this.deviceType, + }), + method: 'POST', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async postRegister(request: RegisterRequest): Promise { + const response = await fetch(new Request(this.baseUrl + '/accounts/register', { + body: JSON.stringify(request), + cache: 'no-cache', + headers: new Headers({ + 'Content-Type': 'application/json; charset=utf-8', + 'Device-Type': this.deviceType, + }), + method: 'POST', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + // Folder APIs + + async postFolder(request: FolderRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/folders', { + body: JSON.stringify(request), + cache: 'no-cache', + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Content-Type': 'application/json; charset=utf-8', + 'Device-Type': this.deviceType, + }), + method: 'POST', + })); + + if (response.status === 200) { + const responseJson = await response.json(); + return new FolderResponse(responseJson); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async putFolder(id: string, request: FolderRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/folders/' + id, { + body: JSON.stringify(request), + cache: 'no-cache', + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Content-Type': 'application/json; charset=utf-8', + 'Device-Type': this.deviceType, + }), + method: 'PUT', + })); + + if (response.status === 200) { + const responseJson = await response.json(); + return new FolderResponse(responseJson); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async deleteFolder(id: string): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/folders/' + id, { + cache: 'no-cache', + headers: new Headers({ + 'Authorization': authHeader, + 'Device-Type': this.deviceType, + }), + method: 'DELETE', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + // Cipher APIs + + async postCipher(request: CipherRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers', { + body: JSON.stringify(request), + cache: 'no-cache', + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Content-Type': 'application/json; charset=utf-8', + 'Device-Type': this.deviceType, + }), + method: 'POST', + })); + + if (response.status === 200) { + const responseJson = await response.json(); + return new CipherResponse(responseJson); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async putCipher(id: string, request: CipherRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { + body: JSON.stringify(request), + cache: 'no-cache', + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Content-Type': 'application/json; charset=utf-8', + 'Device-Type': this.deviceType, + }), + method: 'PUT', + })); + + if (response.status === 200) { + const responseJson = await response.json(); + return new CipherResponse(responseJson); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async deleteCipher(id: string): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { + cache: 'no-cache', + headers: new Headers({ + 'Authorization': authHeader, + 'Device-Type': this.deviceType, + }), + method: 'DELETE', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + // Attachments APIs + + async postCipherAttachment(id: string, data: FormData): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment', { + body: data, + cache: 'no-cache', + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Device-Type': this.deviceType, + }), + method: 'POST', + })); + + if (response.status === 200) { + const responseJson = await response.json(); + return new CipherResponse(responseJson); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async deleteCipherAttachment(id: string, attachmentId: string): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment/' + attachmentId, { + cache: 'no-cache', + headers: new Headers({ + 'Authorization': authHeader, + 'Device-Type': this.deviceType, + }), + method: 'DELETE', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + // Sync APIs + + async getSync(): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/sync', { + cache: 'no-cache', + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Device-Type': this.deviceType, + }), + })); + + if (response.status === 200) { + const responseJson = await response.json(); + return new SyncResponse(responseJson); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + // Helpers + + private async handleError(response: Response, tokenError: boolean): Promise { + if ((tokenError && response.status === 400) || response.status === 401 || response.status === 403) { + this.logoutCallback(true); + return null; + } + + let responseJson: any = null; + const typeHeader = response.headers.get('content-type'); + if (typeHeader != null && typeHeader.indexOf('application/json') > -1) { + responseJson = await response.json(); + } + + return new ErrorResponse(responseJson, response.status, tokenError); + } + + private async handleTokenState(): Promise { + let accessToken: string; + if (this.tokenService.tokenNeedsRefresh()) { + const tokenResponse = await this.doRefreshToken(); + accessToken = tokenResponse.accessToken; + } else { + accessToken = await this.tokenService.getToken(); + } + + return 'Bearer ' + accessToken; + } + + private async doRefreshToken(): Promise { + const refreshToken = await this.tokenService.getRefreshToken(); + if (refreshToken == null || refreshToken === '') { + throw new Error(); + } + + const response = await fetch(new Request(this.identityBaseUrl + '/connect/token', { + body: this.qsStringify({ + grant_type: 'refresh_token', + client_id: 'browser', + refresh_token: refreshToken, + }), + cache: 'no-cache', + headers: new Headers({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', + 'Accept': 'application/json', + 'Device-Type': this.deviceType, + }), + method: 'POST', + })); + + if (response.status === 200) { + const responseJson = await response.json(); + const tokenResponse = new IdentityTokenResponse(responseJson); + await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken); + return tokenResponse; + } else { + const error = await this.handleError(response, true); + return Promise.reject(error); + } + } + + private qsStringify(params: any): string { + return Object.keys(params).map((key) => { + return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); + }).join('&'); + } +} diff --git a/src/services/appId.service.ts b/src/services/appId.service.ts new file mode 100644 index 0000000000..430a375571 --- /dev/null +++ b/src/services/appId.service.ts @@ -0,0 +1,28 @@ +import { UtilsService } from './utils.service'; + +import { AppIdService as AppIdServiceInterface } from '../abstractions/appId.service'; +import { StorageService } from '../abstractions/storage.service'; + +export class AppIdService implements AppIdServiceInterface { + constructor(private storageService: StorageService) { + } + + getAppId(): Promise { + return this.makeAndGetAppId('appId'); + } + + getAnonymousAppId(): Promise { + return this.makeAndGetAppId('anonymousAppId'); + } + + private async makeAndGetAppId(key: string) { + const existingId = await this.storageService.get(key); + if (existingId != null) { + return existingId; + } + + const guid = UtilsService.newGuid(); + await this.storageService.save(key, guid); + return guid; + } +} diff --git a/src/services/index.ts b/src/services/index.ts index 87ec7493d2..dd4122a826 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,3 +1,6 @@ +export { ApiService } from './api.service'; +export { AppIdService } from './appId.service'; export { ConstantsService } from './constants.service'; export { CryptoService } from './crypto.service'; +export { TokenService } from './token.service'; export { UtilsService } from './utils.service'; diff --git a/src/services/token.service.ts b/src/services/token.service.ts new file mode 100644 index 0000000000..552767e5e0 --- /dev/null +++ b/src/services/token.service.ts @@ -0,0 +1,176 @@ +import { ConstantsService } from './constants.service'; +import { UtilsService } from './utils.service'; + +import { StorageService } from '../abstractions/storage.service'; +import { TokenService as TokenServiceInterface } from '../abstractions/token.service'; + +const Keys = { + accessToken: 'accessToken', + refreshToken: 'refreshToken', + twoFactorTokenPrefix: 'twoFactorToken_', +}; + +export class TokenService implements TokenServiceInterface { + token: string; + decodedToken: any; + refreshToken: string; + + constructor(private storageService: StorageService) { + } + + setTokens(accessToken: string, refreshToken: string): Promise { + return Promise.all([ + this.setToken(accessToken), + this.setRefreshToken(refreshToken), + ]); + } + + setToken(token: string): Promise { + this.token = token; + this.decodedToken = null; + return this.storageService.save(Keys.accessToken, token); + } + + async getToken(): Promise { + if (this.token != null) { + return this.token; + } + + this.token = await this.storageService.get(Keys.accessToken); + return this.token; + } + + setRefreshToken(refreshToken: string): Promise { + this.refreshToken = refreshToken; + return this.storageService.save(Keys.refreshToken, refreshToken); + } + + async getRefreshToken(): Promise { + if (this.refreshToken != null) { + return this.refreshToken; + } + + this.refreshToken = await this.storageService.get(Keys.refreshToken); + return this.refreshToken; + } + + setTwoFactorToken(token: string, email: string): Promise { + return this.storageService.save(Keys.twoFactorTokenPrefix + email, token); + } + + getTwoFactorToken(email: string): Promise { + return this.storageService.get(Keys.twoFactorTokenPrefix + email); + } + + clearTwoFactorToken(email: string): Promise { + return this.storageService.remove(Keys.twoFactorTokenPrefix + email); + } + + clearToken(): Promise { + this.token = null; + this.decodedToken = null; + this.refreshToken = null; + + return Promise.all([ + this.storageService.remove(Keys.accessToken), + this.storageService.remove(Keys.refreshToken), + ]); + } + + // jwthelper methods + // ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js + + decodeToken(): any { + if (this.decodedToken) { + return this.decodedToken; + } + + if (this.token == null) { + throw new Error('Token not found.'); + } + + const parts = this.token.split('.'); + if (parts.length !== 3) { + throw new Error('JWT must have 3 parts'); + } + + const decoded = UtilsService.urlBase64Decode(parts[1]); + if (decoded == null) { + throw new Error('Cannot decode the token'); + } + + this.decodedToken = JSON.parse(decoded); + return this.decodedToken; + } + + getTokenExpirationDate(): Date { + const decoded = this.decodeToken(); + if (typeof decoded.exp === 'undefined') { + return null; + } + + const d = new Date(0); // The 0 here is the key, which sets the date to the epoch + d.setUTCSeconds(decoded.exp); + return d; + } + + tokenSecondsRemaining(offsetSeconds: number = 0): number { + const d = this.getTokenExpirationDate(); + if (d == null) { + return 0; + } + + const msRemaining = d.valueOf() - (new Date().valueOf() + (offsetSeconds * 1000)); + return Math.round(msRemaining / 1000); + } + + tokenNeedsRefresh(minutes: number = 5): boolean { + const sRemaining = this.tokenSecondsRemaining(); + return sRemaining < (60 * minutes); + } + + getUserId(): string { + const decoded = this.decodeToken(); + if (typeof decoded.sub === 'undefined') { + throw new Error('No user id found'); + } + + return decoded.sub as string; + } + + getEmail(): string { + const decoded = this.decodeToken(); + if (typeof decoded.email === 'undefined') { + throw new Error('No email found'); + } + + return decoded.email as string; + } + + getName(): string { + const decoded = this.decodeToken(); + if (typeof decoded.name === 'undefined') { + throw new Error('No name found'); + } + + return decoded.name as string; + } + + getPremium(): boolean { + const decoded = this.decodeToken(); + if (typeof decoded.premium === 'undefined') { + return false; + } + + return decoded.premium as boolean; + } + + getIssuer(): string { + const decoded = this.decodeToken(); + if (typeof decoded.iss === 'undefined') { + throw new Error('No issuer found'); + } + + return decoded.iss as string; + } +} From 07e4758deee4f98d776f7d62953f54abebfe2dc7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 16:23:27 -0500 Subject: [PATCH 0031/1626] return type for setUrls --- src/abstractions/api.service.ts | 2 +- src/services/api.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 9829f5abc2..6059f3a4a5 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -18,7 +18,7 @@ export interface ApiService { identityBaseUrl: string; deviceType: string; logoutCallback: Function; - setUrls(urls: EnvironmentUrls); + setUrls(urls: EnvironmentUrls): void; postIdentityToken(request: TokenRequest): Promise; refreshIdentityToken(): Promise; postTwoFactorEmail(request: TwoFactorEmailRequest): Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 5e41da5236..e9838ed095 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -32,7 +32,7 @@ export class ApiService implements ApiServiceInterface { this.deviceType = platformUtilsService.getDevice().toString(); } - setUrls(urls: EnvironmentUrls) { + setUrls(urls: EnvironmentUrls): void { this.urlsSet = true; if (urls.base != null) { From 564d024dd1d88124fa626bee5c370f9f8d21a294 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 17:37:50 -0500 Subject: [PATCH 0032/1626] convert user service to jslib --- src/abstractions/index.ts | 1 + src/abstractions/user.service.ts | 12 +++++ src/services/index.ts | 1 + src/services/user.service.ts | 80 ++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 src/abstractions/user.service.ts create mode 100644 src/services/user.service.ts diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index b3d6ecb527..b2348f7326 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -5,4 +5,5 @@ export { MessagingService } from './messaging.service'; export { PlatformUtilsService } from './platformUtils.service'; export { StorageService } from './storage.service'; export { TokenService } from './token.service'; +export { UserService } from './user.service'; export { UtilsService } from './utils.service'; diff --git a/src/abstractions/user.service.ts b/src/abstractions/user.service.ts new file mode 100644 index 0000000000..7afbd0b748 --- /dev/null +++ b/src/abstractions/user.service.ts @@ -0,0 +1,12 @@ +export interface UserService { + userId: string; + email: string; + stamp: string; + setUserIdAndEmail(userId: string, email: string): Promise; + setSecurityStamp(stamp: string): Promise; + getUserId(): Promise; + getEmail(): Promise; + getSecurityStamp(): Promise; + clear(): Promise; + isAuthenticated(): Promise; +} diff --git a/src/services/index.ts b/src/services/index.ts index dd4122a826..1ef71d40bb 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -3,4 +3,5 @@ export { AppIdService } from './appId.service'; export { ConstantsService } from './constants.service'; export { CryptoService } from './crypto.service'; export { TokenService } from './token.service'; +export { UserService } from './user.service'; export { UtilsService } from './utils.service'; diff --git a/src/services/user.service.ts b/src/services/user.service.ts new file mode 100644 index 0000000000..aa2b19e179 --- /dev/null +++ b/src/services/user.service.ts @@ -0,0 +1,80 @@ +import { StorageService } from '../abstractions/storage.service'; +import { TokenService } from '../abstractions/token.service'; +import { UserService as UserServiceInterface } from '../abstractions/user.service'; + +const Keys = { + userId: 'userId', + userEmail: 'userEmail', + stamp: 'securityStamp', +}; + +export class UserService implements UserServiceInterface { + userId: string; + email: string; + stamp: string; + + constructor(private tokenService: TokenService, private storageService: StorageService) { + } + + setUserIdAndEmail(userId: string, email: string): Promise { + this.email = email; + this.userId = userId; + + return Promise.all([ + this.storageService.save(Keys.userEmail, email), + this.storageService.save(Keys.userId, userId), + ]); + } + + setSecurityStamp(stamp: string): Promise { + this.stamp = stamp; + return this.storageService.save(Keys.stamp, stamp); + } + + async getUserId(): Promise { + if (this.userId != null) { + return this.userId; + } + + this.userId = await this.storageService.get(Keys.userId); + return this.userId; + } + + async getEmail(): Promise { + if (this.email != null) { + return this.email; + } + + this.email = await this.storageService.get(Keys.userEmail); + return this.email; + } + + async getSecurityStamp(): Promise { + if (this.stamp != null) { + return this.stamp; + } + + this.stamp = await this.storageService.get(Keys.stamp); + return this.stamp; + } + + async clear(): Promise { + await Promise.all([ + this.storageService.remove(Keys.userId), + this.storageService.remove(Keys.userEmail), + this.storageService.remove(Keys.stamp), + ]); + + this.userId = this.email = this.stamp = null; + } + + async isAuthenticated(): Promise { + const token = await this.tokenService.getToken(); + if (token == null) { + return false; + } + + const userId = await this.getUserId(); + return userId != null; + } +} From 9b327683b4c0b8504724aca213e08d484c290804 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 17:45:21 -0500 Subject: [PATCH 0033/1626] convert totp service to jslib --- src/abstractions/index.ts | 1 + src/abstractions/totp.service.ts | 4 ++ src/services/index.ts | 1 + src/services/totp.service.ts | 118 +++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+) create mode 100644 src/abstractions/totp.service.ts create mode 100644 src/services/totp.service.ts diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index b2348f7326..d026e4c372 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -5,5 +5,6 @@ export { MessagingService } from './messaging.service'; export { PlatformUtilsService } from './platformUtils.service'; export { StorageService } from './storage.service'; export { TokenService } from './token.service'; +export { TotpService } from './totp.service'; export { UserService } from './user.service'; export { UtilsService } from './utils.service'; diff --git a/src/abstractions/totp.service.ts b/src/abstractions/totp.service.ts new file mode 100644 index 0000000000..2e8fa79aa7 --- /dev/null +++ b/src/abstractions/totp.service.ts @@ -0,0 +1,4 @@ +export interface TotpService { + getCode(keyb32: string): Promise; + isAutoCopyEnabled(): Promise; +} diff --git a/src/services/index.ts b/src/services/index.ts index 1ef71d40bb..67e7b6aa59 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -3,5 +3,6 @@ export { AppIdService } from './appId.service'; export { ConstantsService } from './constants.service'; export { CryptoService } from './crypto.service'; export { TokenService } from './token.service'; +export { TotpService } from './totp.service'; export { UserService } from './user.service'; export { UtilsService } from './utils.service'; diff --git a/src/services/totp.service.ts b/src/services/totp.service.ts new file mode 100644 index 0000000000..ac6cc4cb78 --- /dev/null +++ b/src/services/totp.service.ts @@ -0,0 +1,118 @@ +import { ConstantsService } from './constants.service'; + +import { StorageService } from '../abstractions/storage.service'; +import { TotpService as TotpServiceInterface } from '../abstractions/totp.service'; + +const b32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + +const TotpAlgorithm = { + name: 'HMAC', + hash: { name: 'SHA-1' }, +}; + +export class TotpService implements TotpServiceInterface { + constructor(private storageService: StorageService) { + } + + async getCode(keyb32: string): Promise { + const epoch = Math.round(new Date().getTime() / 1000.0); + const timeHex = this.leftpad(this.dec2hex(Math.floor(epoch / 30)), 16, '0'); + const timeBytes = this.hex2bytes(timeHex); + const keyBytes = this.b32tobytes(keyb32); + + if (!keyBytes.length || !timeBytes.length) { + return null; + } + + const hashHex = await this.sign(keyBytes, timeBytes); + if (!hashHex) { + return null; + } + + const offset = this.hex2dec(hashHex.substring(hashHex.length - 1)); + // tslint:disable-next-line + let otp = (this.hex2dec(hashHex.substr(offset * 2, 8)) & this.hex2dec('7fffffff')) + ''; + otp = (otp).substr(otp.length - 6, 6); + return otp; + } + + async isAutoCopyEnabled(): Promise { + return !(await this.storageService.get(ConstantsService.disableAutoTotpCopyKey)); + } + + // Helpers + + private leftpad(s: string, l: number, p: string): string { + if (l + 1 >= s.length) { + s = Array(l + 1 - s.length).join(p) + s; + } + return s; + } + + private dec2hex(d: number): string { + return (d < 15.5 ? '0' : '') + Math.round(d).toString(16); + } + + private hex2dec(s: string): number { + return parseInt(s, 16); + } + + private hex2bytes(s: string): Uint8Array { + const bytes = new Uint8Array(s.length / 2); + for (let i = 0; i < s.length; i += 2) { + bytes[i / 2] = parseInt(s.substr(i, 2), 16); + } + return bytes; + } + + private buff2hex(buff: ArrayBuffer): string { + const bytes = new Uint8Array(buff); + const hex: string[] = []; + bytes.forEach((b) => { + // tslint:disable-next-line + hex.push((b >>> 4).toString(16)); + // tslint:disable-next-line + hex.push((b & 0xF).toString(16)); + }); + return hex.join(''); + } + + private b32tohex(s: string): string { + s = s.toUpperCase(); + let cleanedInput = ''; + + for (let i = 0; i < s.length; i++) { + if (b32Chars.indexOf(s[i]) < 0) { + continue; + } + + cleanedInput += s[i]; + } + s = cleanedInput; + + let bits = ''; + let hex = ''; + for (let i = 0; i < s.length; i++) { + const byteIndex = b32Chars.indexOf(s.charAt(i)); + if (byteIndex < 0) { + continue; + } + bits += this.leftpad(byteIndex.toString(2), 5, '0'); + } + for (let i = 0; i + 4 <= bits.length; i += 4) { + const chunk = bits.substr(i, 4); + hex = hex + parseInt(chunk, 2).toString(16); + } + return hex; + } + + private b32tobytes(s: string): Uint8Array { + return this.hex2bytes(this.b32tohex(s)); + } + + private async sign(keyBytes: Uint8Array, timeBytes: Uint8Array) { + const key = await window.crypto.subtle.importKey('raw', keyBytes, TotpAlgorithm, false, ['sign']); + const signature = await window.crypto.subtle.sign(TotpAlgorithm, key, timeBytes); + return this.buff2hex(signature); + } +} From 38502b5447c67680ea077104bae3d039d624cab3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 17:55:32 -0500 Subject: [PATCH 0034/1626] convert password generator to jslib --- src/abstractions/index.ts | 1 + .../passwordGeneration.service.ts | 12 + src/services/index.ts | 1 + src/services/passwordGeneration.service.ts | 243 ++++++++++++++++++ 4 files changed, 257 insertions(+) create mode 100644 src/abstractions/passwordGeneration.service.ts create mode 100644 src/services/passwordGeneration.service.ts diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index d026e4c372..9804abdf32 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -2,6 +2,7 @@ export { ApiService } from './api.service'; export { AppIdService } from './appId.service'; export { CryptoService } from './crypto.service'; export { MessagingService } from './messaging.service'; +export { PasswordGenerationService } from './passwordGeneration.service'; export { PlatformUtilsService } from './platformUtils.service'; export { StorageService } from './storage.service'; export { TokenService } from './token.service'; diff --git a/src/abstractions/passwordGeneration.service.ts b/src/abstractions/passwordGeneration.service.ts new file mode 100644 index 0000000000..1f2433842b --- /dev/null +++ b/src/abstractions/passwordGeneration.service.ts @@ -0,0 +1,12 @@ +import { PasswordHistory } from '../models/domain/passwordHistory'; + +export interface PasswordGenerationService { + optionsCache: any; + history: PasswordHistory[]; + generatePassword(options: any): string; + getOptions(): any; + saveOptions(options: any): Promise; + getHistory(): PasswordHistory[]; + addHistory(password: string): Promise; + clear(): Promise; +} diff --git a/src/services/index.ts b/src/services/index.ts index 67e7b6aa59..084707f27d 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -2,6 +2,7 @@ export { ApiService } from './api.service'; export { AppIdService } from './appId.service'; export { ConstantsService } from './constants.service'; export { CryptoService } from './crypto.service'; +export { PasswordGenerationService } from './passwordGeneration.service'; export { TokenService } from './token.service'; export { TotpService } from './totp.service'; export { UserService } from './user.service'; diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts new file mode 100644 index 0000000000..771494d0bc --- /dev/null +++ b/src/services/passwordGeneration.service.ts @@ -0,0 +1,243 @@ +import { CipherString } from '../models/domain/cipherString'; +import { PasswordHistory } from '../models/domain/passwordHistory'; + +import { UtilsService } from './utils.service'; + +import { CryptoService } from '../abstractions/crypto.service'; +import { + PasswordGenerationService as PasswordGenerationServiceInterface, +} from '../abstractions/passwordGeneration.service'; +import { StorageService } from '../abstractions/storage.service'; + +const DefaultOptions = { + length: 14, + ambiguous: false, + number: true, + minNumber: 1, + uppercase: true, + minUppercase: 1, + lowercase: true, + minLowercase: 1, + special: false, + minSpecial: 1, +}; + +const Keys = { + options: 'passwordGenerationOptions', + history: 'generatedPasswordHistory', +}; + +const MaxPasswordsInHistory = 100; + +export class PasswordGenerationService implements PasswordGenerationServiceInterface { + static generatePassword(options: any): string { + // overload defaults with given options + const o = Object.assign({}, DefaultOptions, options); + + // sanitize + if (o.uppercase && o.minUppercase < 0) { + o.minUppercase = 1; + } + if (o.lowercase && o.minLowercase < 0) { + o.minLowercase = 1; + } + if (o.number && o.minNumber < 0) { + o.minNumber = 1; + } + if (o.special && o.minSpecial < 0) { + o.minSpecial = 1; + } + + if (!o.length || o.length < 1) { + o.length = 10; + } + + const minLength: number = o.minUppercase + o.minLowercase + o.minNumber + o.minSpecial; + if (o.length < minLength) { + o.length = minLength; + } + + const positions: string[] = []; + if (o.lowercase && o.minLowercase > 0) { + for (let i = 0; i < o.minLowercase; i++) { + positions.push('l'); + } + } + if (o.uppercase && o.minUppercase > 0) { + for (let i = 0; i < o.minUppercase; i++) { + positions.push('u'); + } + } + if (o.number && o.minNumber > 0) { + for (let i = 0; i < o.minNumber; i++) { + positions.push('n'); + } + } + if (o.special && o.minSpecial > 0) { + for (let i = 0; i < o.minSpecial; i++) { + positions.push('s'); + } + } + while (positions.length < o.length) { + positions.push('a'); + } + + // shuffle + positions.sort(() => { + return UtilsService.secureRandomNumber(0, 1) * 2 - 1; + }); + + // build out the char sets + let allCharSet = ''; + + let lowercaseCharSet = 'abcdefghijkmnopqrstuvwxyz'; + if (o.ambiguous) { + lowercaseCharSet += 'l'; + } + if (o.lowercase) { + allCharSet += lowercaseCharSet; + } + + let uppercaseCharSet = 'ABCDEFGHIJKLMNPQRSTUVWXYZ'; + if (o.ambiguous) { + uppercaseCharSet += 'O'; + } + if (o.uppercase) { + allCharSet += uppercaseCharSet; + } + + let numberCharSet = '23456789'; + if (o.ambiguous) { + numberCharSet += '01'; + } + if (o.number) { + allCharSet += numberCharSet; + } + + const specialCharSet = '!@#$%^&*'; + if (o.special) { + allCharSet += specialCharSet; + } + + let password = ''; + for (let i = 0; i < o.length; i++) { + let positionChars: string; + switch (positions[i]) { + case 'l': + positionChars = lowercaseCharSet; + break; + case 'u': + positionChars = uppercaseCharSet; + break; + case 'n': + positionChars = numberCharSet; + break; + case 's': + positionChars = specialCharSet; + break; + case 'a': + positionChars = allCharSet; + break; + } + + const randomCharIndex = UtilsService.secureRandomNumber(0, positionChars.length - 1); + password += positionChars.charAt(randomCharIndex); + } + + return password; + } + + optionsCache: any; + history: PasswordHistory[] = []; + + constructor(private cryptoService: CryptoService, + private storageService: StorageService) { + storageService.get(Keys.history).then((encrypted) => { + return this.decryptHistory(encrypted); + }).then((history) => { + this.history = history; + }); + } + + generatePassword(options: any) { + return PasswordGenerationService.generatePassword(options); + } + + async getOptions() { + if (this.optionsCache == null) { + const options = await this.storageService.get(Keys.options); + if (options == null) { + this.optionsCache = DefaultOptions; + } else { + this.optionsCache = options; + } + } + + return this.optionsCache; + } + + async saveOptions(options: any) { + await this.storageService.save(Keys.options, options); + this.optionsCache = options; + } + + getHistory() { + return this.history || new Array(); + } + + async addHistory(password: string): Promise { + // Prevent duplicates + if (this.matchesPrevious(password)) { + return; + } + + this.history.push(new PasswordHistory(password, Date.now())); + + // Remove old items. + if (this.history.length > MaxPasswordsInHistory) { + this.history.shift(); + } + + const newHistory = await this.encryptHistory(); + return await this.storageService.save(Keys.history, newHistory); + } + + async clear(): Promise { + this.history = []; + return await this.storageService.remove(Keys.history); + } + + private async encryptHistory(): Promise { + if (this.history == null || this.history.length === 0) { + return Promise.resolve([]); + } + + const promises = this.history.map(async (item) => { + const encrypted = await this.cryptoService.encrypt(item.password); + return new PasswordHistory(encrypted.encryptedString, item.date); + }); + + return await Promise.all(promises); + } + + private async decryptHistory(history: PasswordHistory[]): Promise { + if (history == null || history.length === 0) { + return Promise.resolve([]); + } + + const promises = history.map(async (item) => { + const decrypted = await this.cryptoService.decrypt(new CipherString(item.password)); + return new PasswordHistory(decrypted, item.date); + }); + + return await Promise.all(promises); + } + + private matchesPrevious(password: string): boolean { + if (this.history == null || this.history.length === 0) { + return false; + } + + return this.history[this.history.length - 1].password === password; + } +} From 3220252cb9d73930e64291f3874a81694caf1966 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 20:20:54 -0500 Subject: [PATCH 0035/1626] convert environment service to jslib --- src/abstractions/environment.service.ts | 10 +++ src/abstractions/index.ts | 1 + src/services/environment.service.ts | 89 +++++++++++++++++++++++++ src/services/index.ts | 1 + 4 files changed, 101 insertions(+) create mode 100644 src/abstractions/environment.service.ts create mode 100644 src/services/environment.service.ts diff --git a/src/abstractions/environment.service.ts b/src/abstractions/environment.service.ts new file mode 100644 index 0000000000..a021db8910 --- /dev/null +++ b/src/abstractions/environment.service.ts @@ -0,0 +1,10 @@ +export interface EnvironmentService { + baseUrl: string; + webVaultUrl: string; + apiUrl: string; + identityUrl: string; + iconsUrl: string; + + setUrlsFromStorage(): Promise; + setUrls(urls: any): Promise; +} diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index 9804abdf32..74062e753c 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -1,6 +1,7 @@ export { ApiService } from './api.service'; export { AppIdService } from './appId.service'; export { CryptoService } from './crypto.service'; +export { EnvironmentService } from './environment.service'; export { MessagingService } from './messaging.service'; export { PasswordGenerationService } from './passwordGeneration.service'; export { PlatformUtilsService } from './platformUtils.service'; diff --git a/src/services/environment.service.ts b/src/services/environment.service.ts new file mode 100644 index 0000000000..292ee89a4d --- /dev/null +++ b/src/services/environment.service.ts @@ -0,0 +1,89 @@ +import { EnvironmentUrls } from '../models/domain/environmentUrls'; + +import { ConstantsService } from './constants.service'; + +import { ApiService } from '../abstractions/api.service'; +import { EnvironmentService as EnvironmentServiceInterface } from '../abstractions/environment.service'; +import { StorageService } from '../abstractions/storage.service'; + +export class EnvironmentService { + baseUrl: string; + webVaultUrl: string; + apiUrl: string; + identityUrl: string; + iconsUrl: string; + + constructor(private apiService: ApiService, private storageService: StorageService) { + } + + async setUrlsFromStorage(): Promise { + const urlsObj: any = await this.storageService.get(ConstantsService.environmentUrlsKey); + const urls = urlsObj || { + base: null, + api: null, + identity: null, + icons: null, + webVault: null, + }; + + const envUrls = new EnvironmentUrls(); + + if (urls.base) { + this.baseUrl = envUrls.base = urls.base; + await this.apiService.setUrls(envUrls); + return; + } + + this.webVaultUrl = urls.webVault; + this.apiUrl = envUrls.api = urls.api; + this.identityUrl = envUrls.identity = urls.identity; + this.iconsUrl = urls.icons; + await this.apiService.setUrls(envUrls); + } + + async setUrls(urls: any): Promise { + urls.base = this.formatUrl(urls.base); + urls.webVault = this.formatUrl(urls.webVault); + urls.api = this.formatUrl(urls.api); + urls.identity = this.formatUrl(urls.identity); + urls.icons = this.formatUrl(urls.icons); + + await this.storageService.save(ConstantsService.environmentUrlsKey, { + base: urls.base, + api: urls.api, + identity: urls.identity, + webVault: urls.webVault, + icons: urls.icons, + }); + + this.baseUrl = urls.base; + this.webVaultUrl = urls.webVault; + this.apiUrl = urls.api; + this.identityUrl = urls.identity; + this.iconsUrl = urls.icons; + + const envUrls = new EnvironmentUrls(); + if (this.baseUrl) { + envUrls.base = this.baseUrl; + } else { + envUrls.api = this.apiUrl; + envUrls.identity = this.identityUrl; + } + + await this.apiService.setUrls(envUrls); + return urls; + } + + private formatUrl(url: string): string { + if (url == null || url === '') { + return null; + } + + url = url.replace(/\/+$/g, ''); + if (!url.startsWith('http://') && !url.startsWith('https://')) { + url = 'https://' + url; + } + + return url; + } +} diff --git a/src/services/index.ts b/src/services/index.ts index 084707f27d..a2fae25f3e 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -2,6 +2,7 @@ export { ApiService } from './api.service'; export { AppIdService } from './appId.service'; export { ConstantsService } from './constants.service'; export { CryptoService } from './crypto.service'; +export { EnvironmentService } from './environment.service'; export { PasswordGenerationService } from './passwordGeneration.service'; export { TokenService } from './token.service'; export { TotpService } from './totp.service'; From 0c7175d1bf3af534f5dcdef48cf0788ed5cae368 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 22:23:38 -0500 Subject: [PATCH 0036/1626] instance props reference static --- src/services/constants.service.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index 30206c902c..6ae5038d52 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -11,16 +11,15 @@ export class ConstantsService { 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'; + readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; + readonly disableGaKey: string = ConstantsService.disableGaKey; + readonly disableAddLoginNotificationKey: string = ConstantsService.disableAddLoginNotificationKey; + readonly disableContextMenuItemKey: string = ConstantsService.disableContextMenuItemKey; + readonly disableFaviconKey: string = ConstantsService.disableFaviconKey; + readonly disableAutoTotpCopyKey: string = ConstantsService.disableAutoTotpCopyKey; + readonly enableAutoFillOnPageLoadKey: string = ConstantsService.enableAutoFillOnPageLoadKey; + readonly lockOptionKey: string = ConstantsService.lockOptionKey; + readonly lastActiveKey: string = ConstantsService.lastActiveKey; // TODO: Convert these objects to enums readonly encType: any = { From 426c536b15bbb4acac5b936ca7d5b1ff8ff8ee06 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 22:28:29 -0500 Subject: [PATCH 0037/1626] move container service to jslib --- src/services/container.service.ts | 22 ++++++++++++++++++++++ src/services/index.ts | 1 + 2 files changed, 23 insertions(+) create mode 100644 src/services/container.service.ts diff --git a/src/services/container.service.ts b/src/services/container.service.ts new file mode 100644 index 0000000000..31896d8b40 --- /dev/null +++ b/src/services/container.service.ts @@ -0,0 +1,22 @@ +import { CryptoService } from '../abstractions/crypto.service'; +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; + +export class ContainerService { + constructor(private cryptoService: CryptoService, + private platformUtilsService: PlatformUtilsService) { + } + + attachToWindow(win: any) { + if (!win.BitwardenContainerService) { + win.BitwardenContainerService = this; + } + } + + getCryptoService(): CryptoService { + return this.cryptoService; + } + + getPlatformUtilsService(): PlatformUtilsService { + return this.platformUtilsService; + } +} diff --git a/src/services/index.ts b/src/services/index.ts index a2fae25f3e..bb199e0e9c 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,6 +1,7 @@ export { ApiService } from './api.service'; export { AppIdService } from './appId.service'; export { ConstantsService } from './constants.service'; +export { ContainerService } from './container.service'; export { CryptoService } from './crypto.service'; export { EnvironmentService } from './environment.service'; export { PasswordGenerationService } from './passwordGeneration.service'; From 4ea1a5f239953a9ed5a84310e7198fb1fe4460d3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 22:39:43 -0500 Subject: [PATCH 0038/1626] move folder service to jslib --- src/abstractions/folder.service.ts | 19 ++++ src/abstractions/index.ts | 1 + src/services/folder.service.ts | 163 +++++++++++++++++++++++++++++ src/services/index.ts | 1 + 4 files changed, 184 insertions(+) create mode 100644 src/abstractions/folder.service.ts create mode 100644 src/services/folder.service.ts diff --git a/src/abstractions/folder.service.ts b/src/abstractions/folder.service.ts new file mode 100644 index 0000000000..e02323115e --- /dev/null +++ b/src/abstractions/folder.service.ts @@ -0,0 +1,19 @@ +import { FolderData } from '../models/data'; + +import { Folder } from '../models/domain'; + +export interface FolderService { + decryptedFolderCache: any[]; + + clearCache(): void; + encrypt(model: any): Promise; + get(id: string): Promise; + getAll(): Promise; + getAllDecrypted(): Promise; + saveWithServer(folder: Folder): Promise; + upsert(folder: FolderData | FolderData[]): Promise; + replace(folders: { [id: string]: FolderData; }): Promise; + clear(userId: string): Promise; + delete(id: string | string[]): Promise; + deleteWithServer(id: string): Promise; +} diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index 74062e753c..7a662f6db5 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -2,6 +2,7 @@ export { ApiService } from './api.service'; export { AppIdService } from './appId.service'; export { CryptoService } from './crypto.service'; export { EnvironmentService } from './environment.service'; +export { FolderService } from './folder.service'; export { MessagingService } from './messaging.service'; export { PasswordGenerationService } from './passwordGeneration.service'; export { PlatformUtilsService } from './platformUtils.service'; diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts new file mode 100644 index 0000000000..6c51dafddc --- /dev/null +++ b/src/services/folder.service.ts @@ -0,0 +1,163 @@ +import { FolderData } from '../models/data'; + +import { Folder } from '../models/domain'; + +import { FolderRequest } from '../models/request'; + +import { FolderResponse } from '../models/response'; + +import { ApiService } from '../abstractions/api.service'; +import { CryptoService } from '../abstractions/crypto.service'; +import { FolderService as FolderServiceInterface } from '../abstractions/folder.service'; +import { StorageService } from '../abstractions/storage.service'; +import { UserService } from '../abstractions/user.service'; + +const Keys = { + foldersPrefix: 'folders_', +}; + +export class FolderService implements FolderServiceInterface { + decryptedFolderCache: any[]; + + constructor(private cryptoService: CryptoService, private userService: UserService, + private noneFolder: string, private apiService: ApiService, + private storageService: StorageService) { + } + + clearCache(): void { + this.decryptedFolderCache = null; + } + + async encrypt(model: any): Promise { + const folder = new Folder(); + folder.id = model.id; + folder.name = await this.cryptoService.encrypt(model.name); + return folder; + } + + async get(id: string): Promise { + const userId = await this.userService.getUserId(); + const folders = await this.storageService.get<{ [id: string]: FolderData; }>( + Keys.foldersPrefix + userId); + if (folders == null || !folders.hasOwnProperty(id)) { + return null; + } + + return new Folder(folders[id]); + } + + async getAll(): Promise { + const userId = await this.userService.getUserId(); + const folders = await this.storageService.get<{ [id: string]: FolderData; }>( + Keys.foldersPrefix + userId); + const response: Folder[] = []; + for (const id in folders) { + if (folders.hasOwnProperty(id)) { + response.push(new Folder(folders[id])); + } + } + return response; + } + + async getAllDecrypted(): Promise { + if (this.decryptedFolderCache != null) { + return this.decryptedFolderCache; + } + + const decFolders: any[] = [{ + id: null, + name: this.noneFolder, + }]; + + const key = await this.cryptoService.getKey(); + if (key == null) { + throw new Error('No key.'); + } + + const promises: Array> = []; + const folders = await this.getAll(); + folders.forEach((folder) => { + promises.push(folder.decrypt().then((f: any) => { + decFolders.push(f); + })); + }); + + await Promise.all(promises); + this.decryptedFolderCache = decFolders; + return this.decryptedFolderCache; + } + + async saveWithServer(folder: Folder): Promise { + const request = new FolderRequest(folder); + + let response: FolderResponse; + if (folder.id == null) { + response = await this.apiService.postFolder(request); + folder.id = response.id; + } else { + response = await this.apiService.putFolder(folder.id, request); + } + + const userId = await this.userService.getUserId(); + const data = new FolderData(response, userId); + await this.upsert(data); + } + + async upsert(folder: FolderData | FolderData[]): Promise { + const userId = await this.userService.getUserId(); + let folders = await this.storageService.get<{ [id: string]: FolderData; }>( + Keys.foldersPrefix + userId); + if (folders == null) { + folders = {}; + } + + if (folder instanceof FolderData) { + const f = folder as FolderData; + folders[f.id] = f; + } else { + (folder as FolderData[]).forEach((f) => { + folders[f.id] = f; + }); + } + + await this.storageService.save(Keys.foldersPrefix + userId, folders); + this.decryptedFolderCache = null; + } + + async replace(folders: { [id: string]: FolderData; }): Promise { + const userId = await this.userService.getUserId(); + await this.storageService.save(Keys.foldersPrefix + userId, folders); + this.decryptedFolderCache = null; + } + + async clear(userId: string): Promise { + await this.storageService.remove(Keys.foldersPrefix + userId); + this.decryptedFolderCache = null; + } + + async delete(id: string | string[]): Promise { + const userId = await this.userService.getUserId(); + const folders = await this.storageService.get<{ [id: string]: FolderData; }>( + Keys.foldersPrefix + userId); + if (folders == null) { + return; + } + + if (typeof id === 'string') { + const i = id as string; + delete folders[id]; + } else { + (id as string[]).forEach((i) => { + delete folders[i]; + }); + } + + await this.storageService.save(Keys.foldersPrefix + userId, folders); + this.decryptedFolderCache = null; + } + + async deleteWithServer(id: string): Promise { + await this.apiService.deleteFolder(id); + await this.delete(id); + } +} diff --git a/src/services/index.ts b/src/services/index.ts index bb199e0e9c..6fc897d773 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -4,6 +4,7 @@ export { ConstantsService } from './constants.service'; export { ContainerService } from './container.service'; export { CryptoService } from './crypto.service'; export { EnvironmentService } from './environment.service'; +export { FolderService } from './folder.service'; export { PasswordGenerationService } from './passwordGeneration.service'; export { TokenService } from './token.service'; export { TotpService } from './totp.service'; From e85eb143bf23aeb89c8994f8094582b5b31ba3ec Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 22:47:58 -0500 Subject: [PATCH 0039/1626] move settings service to jslib --- src/abstractions/index.ts | 1 + src/abstractions/settings.service.ts | 6 +++ src/services/index.ts | 1 + src/services/settings.service.ts | 62 ++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+) create mode 100644 src/abstractions/settings.service.ts create mode 100644 src/services/settings.service.ts diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index 7a662f6db5..476411d3c1 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -6,6 +6,7 @@ export { FolderService } from './folder.service'; export { MessagingService } from './messaging.service'; export { PasswordGenerationService } from './passwordGeneration.service'; export { PlatformUtilsService } from './platformUtils.service'; +export { SettingsService } from './settings.service'; export { StorageService } from './storage.service'; export { TokenService } from './token.service'; export { TotpService } from './totp.service'; diff --git a/src/abstractions/settings.service.ts b/src/abstractions/settings.service.ts new file mode 100644 index 0000000000..97d2bca975 --- /dev/null +++ b/src/abstractions/settings.service.ts @@ -0,0 +1,6 @@ +export interface SettingsService { + clearCache(): void; + getEquivalentDomains(): Promise; + setEquivalentDomains(equivalentDomains: string[][]): Promise; + clear(userId: string): Promise; +} diff --git a/src/services/index.ts b/src/services/index.ts index 6fc897d773..4183aa784e 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -6,6 +6,7 @@ export { CryptoService } from './crypto.service'; export { EnvironmentService } from './environment.service'; export { FolderService } from './folder.service'; export { PasswordGenerationService } from './passwordGeneration.service'; +export { SettingsService } from './settings.service'; export { TokenService } from './token.service'; export { TotpService } from './totp.service'; export { UserService } from './user.service'; diff --git a/src/services/settings.service.ts b/src/services/settings.service.ts new file mode 100644 index 0000000000..0f1227c7a8 --- /dev/null +++ b/src/services/settings.service.ts @@ -0,0 +1,62 @@ +import { SettingsService as SettingsServiceInterface } from '../abstractions/settings.service'; +import { StorageService } from '../abstractions/storage.service'; +import { UserService } from '../abstractions/user.service'; + +const Keys = { + settingsPrefix: 'settings_', + equivalentDomains: 'equivalentDomains', +}; + +export class SettingsService { + private settingsCache: any; + + constructor(private userService: UserService, private storageService: StorageService) { + } + + clearCache(): void { + this.settingsCache = null; + } + + getEquivalentDomains(): Promise { + return this.getSettingsKey(Keys.equivalentDomains); + } + + async setEquivalentDomains(equivalentDomains: string[][]): Promise { + await this.setSettingsKey(Keys.equivalentDomains, equivalentDomains); + } + + async clear(userId: string): Promise { + await this.storageService.remove(Keys.settingsPrefix + userId); + this.settingsCache = null; + } + + // Helpers + + private async getSettings(): Promise { + if (this.settingsCache == null) { + const userId = await this.userService.getUserId(); + this.settingsCache = this.storageService.get(Keys.settingsPrefix + userId); + } + return this.settingsCache; + } + + private async getSettingsKey(key: string): Promise { + const settings = await this.getSettings(); + if (settings != null && settings[key]) { + return settings[key]; + } + return null; + } + + private async setSettingsKey(key: string, value: any): Promise { + const userId = await this.userService.getUserId(); + let settings = await this.getSettings(); + if (!settings) { + settings = {}; + } + + settings[key] = value; + await this.storageService.save(Keys.settingsPrefix + userId, settings); + this.settingsCache = settings; + } +} From 6bdc683c0d3912b6fa5e67dbba5034b50dcc98ff Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 23:01:16 -0500 Subject: [PATCH 0040/1626] move cipherService to jslib --- src/abstractions/cipher.service.ts | 32 ++ src/abstractions/folder.service.ts | 4 +- src/abstractions/index.ts | 1 + src/services/cipher.service.ts | 518 +++++++++++++++++++++++++++++ src/services/folder.service.ts | 8 +- src/services/index.ts | 1 + 6 files changed, 558 insertions(+), 6 deletions(-) create mode 100644 src/abstractions/cipher.service.ts create mode 100644 src/services/cipher.service.ts diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts new file mode 100644 index 0000000000..d68aa99549 --- /dev/null +++ b/src/abstractions/cipher.service.ts @@ -0,0 +1,32 @@ +import { CipherData } from '../models/data'; + +import { Cipher } from '../models/domain'; +import { Field } from '../models/domain'; +import { SymmetricCryptoKey } from '../models/domain'; + +export interface CipherService { + decryptedCipherCache: any[]; + clearCache(): void; + encrypt(model: any): Promise; + encryptFields(fieldsModel: any[], key: SymmetricCryptoKey): Promise; + encryptField(fieldModel: any, key: SymmetricCryptoKey): Promise; + get(id: string): Promise; + getAll(): Promise; + getAllDecrypted(): Promise; + getAllDecryptedForGrouping(groupingId: string, folder?: boolean): Promise; + getAllDecryptedForDomain(domain: string, includeOtherTypes?: any[]): Promise; + getLastUsedForDomain(domain: string): Promise; + updateLastUsedDate(id: string): Promise; + saveNeverDomain(domain: string): Promise; + saveWithServer(cipher: Cipher): Promise; + saveAttachmentWithServer(cipher: Cipher, unencryptedFile: any): Promise; + upsert(cipher: CipherData | CipherData[]): Promise; + replace(ciphers: { [id: string]: CipherData; }): Promise; + clear(userId: string): Promise; + delete(id: string | string[]): Promise; + deleteWithServer(id: string): Promise; + deleteAttachment(id: string, attachmentId: string): Promise; + deleteAttachmentWithServer(id: string, attachmentId: string): Promise; + sortCiphersByLastUsed(a: any, b: any): number; + sortCiphersByLastUsedThenName(a: any, b: any): number; +} diff --git a/src/abstractions/folder.service.ts b/src/abstractions/folder.service.ts index e02323115e..7de2430534 100644 --- a/src/abstractions/folder.service.ts +++ b/src/abstractions/folder.service.ts @@ -1,6 +1,6 @@ -import { FolderData } from '../models/data'; +import { FolderData } from '../models/data/folderData'; -import { Folder } from '../models/domain'; +import { Folder } from '../models/domain/folder'; export interface FolderService { decryptedFolderCache: any[]; diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index 476411d3c1..7ef441f0c3 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -1,5 +1,6 @@ export { ApiService } from './api.service'; export { AppIdService } from './appId.service'; +export { CipherService } from './cipher.service'; export { CryptoService } from './crypto.service'; export { EnvironmentService } from './environment.service'; export { FolderService } from './folder.service'; diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts new file mode 100644 index 0000000000..364be57cac --- /dev/null +++ b/src/services/cipher.service.ts @@ -0,0 +1,518 @@ +import { CipherType } from '../enums/cipherType'; + +import { CipherData } from '../models/data/cipherData'; + +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 { CipherRequest } from '../models/request/cipherRequest'; + +import { CipherResponse } from '../models/response/cipherResponse'; +import { ErrorResponse } from '../models/response/errorResponse'; + +import { ConstantsService } from './constants.service'; + +import { ApiService } from '../abstractions/api.service'; +import { CipherService as CipherServiceInterface } from '../abstractions/cipher.service'; +import { CryptoService } from '../abstractions/crypto.service'; +import { SettingsService } from '../abstractions/settings.service'; +import { StorageService } from '../abstractions/storage.service'; +import { UserService } from '../abstractions/user.service'; + +const Keys = { + ciphersPrefix: 'ciphers_', + localData: 'sitesLocalData', + neverDomains: 'neverDomains', +}; + +export class CipherService implements CipherServiceInterface { + 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, + private storageService: StorageService) { + } + + 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; + cipher.collectionIds = model.collectionIds; + + 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 this.storageService.get(Keys.localData); + const ciphers = await this.storageService.get<{ [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 this.storageService.get(Keys.localData); + const ciphers = await this.storageService.get<{ [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: any[] = []; + const ciphers = await this.getAll(); + ciphers.forEach((cipher) => { + promises.push(cipher.decrypt().then((c: any) => { + decCiphers.push(c); + })); + }); + + await Promise.all(promises); + this.decryptedCipherCache = decCiphers; + return this.decryptedCipherCache; + } + + async getAllDecryptedForGrouping(groupingId: string, folder: boolean = true): Promise { + const ciphers = await this.getAllDecrypted(); + const ciphersToReturn: any[] = []; + + ciphers.forEach((cipher) => { + if (folder && cipher.folderId === groupingId) { + ciphersToReturn.push(cipher); + } else if (!folder && cipher.collectionIds != null && cipher.collectionIds.indexOf(groupingId) > -1) { + 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[] = []; + eqDomains.forEach((eqDomain) => { + 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: any[] = []; + + ciphers.forEach((cipher) => { + 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) { + return null; + } + + const sortedCiphers = ciphers.sort(CipherService.sortCiphersByLastUsed); + return sortedCiphers[0]; + } + + async updateLastUsedDate(id: string): Promise { + let ciphersLocalData = await this.storageService.get(Keys.localData); + if (!ciphersLocalData) { + ciphersLocalData = {}; + } + + if (ciphersLocalData[id]) { + ciphersLocalData[id].lastUsedDate = new Date().getTime(); + } else { + ciphersLocalData[id] = { + lastUsedDate: new Date().getTime(), + }; + } + + await this.storageService.save(Keys.localData, ciphersLocalData); + + if (this.decryptedCipherCache == null) { + return; + } + + for (let i = 0; i < this.decryptedCipherCache.length; i++) { + const cached = this.decryptedCipherCache[i]; + if (cached.id === id) { + cached.localData = ciphersLocalData[id]; + break; + } + } + } + + async saveNeverDomain(domain: string): Promise { + if (domain == null) { + return; + } + + let domains = await this.storageService.get<{ [id: string]: any; }>(Keys.neverDomains); + if (!domains) { + domains = {}; + } + domains[domain] = null; + await this.storageService.save(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, cipher.collectionIds); + 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); + + let response: CipherResponse; + try { + response = await self.apiService.postCipherAttachment(cipher.id, fd); + } catch (e) { + reject((e as ErrorResponse).getSingleMessage()); + return; + } + + const userId = await self.userService.getUserId(); + const data = new CipherData(response, userId, cipher.collectionIds); + 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 this.storageService.get<{ [id: string]: CipherData; }>( + Keys.ciphersPrefix + userId); + if (ciphers == null) { + ciphers = {}; + } + + if (cipher instanceof CipherData) { + const c = cipher as CipherData; + ciphers[c.id] = c; + } else { + (cipher as CipherData[]).forEach((c) => { + ciphers[c.id] = c; + }); + } + + await this.storageService.save(Keys.ciphersPrefix + userId, ciphers); + this.decryptedCipherCache = null; + } + + async replace(ciphers: { [id: string]: CipherData; }): Promise { + const userId = await this.userService.getUserId(); + await this.storageService.save(Keys.ciphersPrefix + userId, ciphers); + this.decryptedCipherCache = null; + } + + async clear(userId: string): Promise { + await this.storageService.remove(Keys.ciphersPrefix + userId); + this.decryptedCipherCache = null; + } + + async delete(id: string | string[]): Promise { + const userId = await this.userService.getUserId(); + const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( + Keys.ciphersPrefix + userId); + if (ciphers == null) { + return; + } + + if (typeof id === 'string') { + const i = id as string; + delete ciphers[id]; + } else { + (id as string[]).forEach((i) => { + delete ciphers[i]; + }); + } + + await this.storageService.save(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 this.storageService.get<{ [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 this.storageService.save(Keys.ciphersPrefix + userId, ciphers); + this.decryptedCipherCache = null; + } + + async deleteAttachmentWithServer(id: string, attachmentId: string): Promise { + try { + await this.apiService.deleteCipherAttachment(id, attachmentId); + } catch (e) { + return Promise.reject((e as ErrorResponse).getSingleMessage()); + } + await this.deleteAttachment(id, attachmentId); + } + + 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.'); + } + } +} diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index 6c51dafddc..a1ba9718ea 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -1,10 +1,10 @@ -import { FolderData } from '../models/data'; +import { FolderData } from '../models/data/folderData'; -import { Folder } from '../models/domain'; +import { Folder } from '../models/domain/folder'; -import { FolderRequest } from '../models/request'; +import { FolderRequest } from '../models/request/folderRequest'; -import { FolderResponse } from '../models/response'; +import { FolderResponse } from '../models/response/folderResponse'; import { ApiService } from '../abstractions/api.service'; import { CryptoService } from '../abstractions/crypto.service'; diff --git a/src/services/index.ts b/src/services/index.ts index 4183aa784e..990af9022b 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,5 +1,6 @@ export { ApiService } from './api.service'; export { AppIdService } from './appId.service'; +export { CipherService } from './cipher.service'; export { ConstantsService } from './constants.service'; export { ContainerService } from './container.service'; export { CryptoService } from './crypto.service'; From 8e64181a5d75af83979b221592003cf327f5a301 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 23:12:18 -0500 Subject: [PATCH 0041/1626] move collection service tio jslib --- src/abstractions/collection.service.ts | 16 ++++ src/abstractions/index.ts | 1 + src/services/collection.service.ts | 124 +++++++++++++++++++++++++ src/services/index.ts | 1 + 4 files changed, 142 insertions(+) create mode 100644 src/abstractions/collection.service.ts create mode 100644 src/services/collection.service.ts diff --git a/src/abstractions/collection.service.ts b/src/abstractions/collection.service.ts new file mode 100644 index 0000000000..e33532d3ce --- /dev/null +++ b/src/abstractions/collection.service.ts @@ -0,0 +1,16 @@ +import { CollectionData } from '../models/data/collectionData'; + +import { Collection } from '../models/domain/collection'; + +export interface CollectionService { + decryptedCollectionCache: any[]; + + clearCache(): void; + get(id: string): Promise; + getAll(): Promise; + getAllDecrypted(): Promise; + upsert(collection: CollectionData | CollectionData[]): Promise; + replace(collections: { [id: string]: CollectionData; }): Promise; + clear(userId: string): Promise; + delete(id: string | string[]): Promise; +} diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index 7ef441f0c3..893a189120 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -1,6 +1,7 @@ export { ApiService } from './api.service'; export { AppIdService } from './appId.service'; export { CipherService } from './cipher.service'; +export { CollectionService } from './collection.service'; export { CryptoService } from './crypto.service'; export { EnvironmentService } from './environment.service'; export { FolderService } from './folder.service'; diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts new file mode 100644 index 0000000000..c409925cd7 --- /dev/null +++ b/src/services/collection.service.ts @@ -0,0 +1,124 @@ +import { CollectionData } from '../models/data/collectionData'; + +import { Collection } from '../models/domain/collection'; + +import { CryptoService } from '../abstractions/crypto.service'; +import { StorageService } from '../abstractions/storage.service'; +import { UserService } from '../abstractions/user.service'; + +const Keys = { + collectionsPrefix: 'collections_', +}; + +export class CollectionService { + decryptedCollectionCache: any[]; + + constructor(private cryptoService: CryptoService, private userService: UserService, + private storageService: StorageService) { + } + + clearCache(): void { + this.decryptedCollectionCache = null; + } + + async get(id: string): Promise { + const userId = await this.userService.getUserId(); + const collections = await this.storageService.get<{ [id: string]: CollectionData; }>( + Keys.collectionsPrefix + userId); + if (collections == null || !collections.hasOwnProperty(id)) { + return null; + } + + return new Collection(collections[id]); + } + + async getAll(): Promise { + const userId = await this.userService.getUserId(); + const collections = await this.storageService.get<{ [id: string]: CollectionData; }>( + Keys.collectionsPrefix + userId); + const response: Collection[] = []; + for (const id in collections) { + if (collections.hasOwnProperty(id)) { + response.push(new Collection(collections[id])); + } + } + return response; + } + + async getAllDecrypted(): Promise { + if (this.decryptedCollectionCache != null) { + return this.decryptedCollectionCache; + } + + const key = await this.cryptoService.getKey(); + if (key == null) { + throw new Error('No key.'); + } + + const decFolders: any[] = []; + const promises: Array> = []; + const folders = await this.getAll(); + folders.forEach((folder) => { + promises.push(folder.decrypt().then((f: any) => { + decFolders.push(f); + })); + }); + + await Promise.all(promises); + this.decryptedCollectionCache = decFolders; + return this.decryptedCollectionCache; + } + + async upsert(collection: CollectionData | CollectionData[]): Promise { + const userId = await this.userService.getUserId(); + let collections = await this.storageService.get<{ [id: string]: CollectionData; }>( + Keys.collectionsPrefix + userId); + if (collections == null) { + collections = {}; + } + + if (collection instanceof CollectionData) { + const c = collection as CollectionData; + collections[c.id] = c; + } else { + (collection as CollectionData[]).forEach((c) => { + collections[c.id] = c; + }); + } + + await this.storageService.save(Keys.collectionsPrefix + userId, collections); + this.decryptedCollectionCache = null; + } + + async replace(collections: { [id: string]: CollectionData; }): Promise { + const userId = await this.userService.getUserId(); + await this.storageService.save(Keys.collectionsPrefix + userId, collections); + this.decryptedCollectionCache = null; + } + + async clear(userId: string): Promise { + await this.storageService.remove(Keys.collectionsPrefix + userId); + this.decryptedCollectionCache = null; + } + + async delete(id: string | string[]): Promise { + const userId = await this.userService.getUserId(); + const collections = await this.storageService.get<{ [id: string]: CollectionData; }>( + Keys.collectionsPrefix + userId); + if (collections == null) { + return; + } + + if (typeof id === 'string') { + const i = id as string; + delete collections[id]; + } else { + (id as string[]).forEach((i) => { + delete collections[i]; + }); + } + + await this.storageService.save(Keys.collectionsPrefix + userId, collections); + this.decryptedCollectionCache = null; + } +} diff --git a/src/services/index.ts b/src/services/index.ts index 990af9022b..0412506aef 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,6 +1,7 @@ export { ApiService } from './api.service'; export { AppIdService } from './appId.service'; export { CipherService } from './cipher.service'; +export { CollectionService } from './collection.service'; export { ConstantsService } from './constants.service'; export { ContainerService } from './container.service'; export { CryptoService } from './crypto.service'; From f383ee502787d519ff041e75b14f1dd4038477a3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 23:18:51 -0500 Subject: [PATCH 0042/1626] move lock service ti jslib --- src/abstractions/index.ts | 1 + src/abstractions/lock.service.ts | 4 ++ src/services/index.ts | 1 + src/services/lock.service.ts | 65 ++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+) create mode 100644 src/abstractions/lock.service.ts create mode 100644 src/services/lock.service.ts diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index 893a189120..1bd679e41c 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -5,6 +5,7 @@ export { CollectionService } from './collection.service'; export { CryptoService } from './crypto.service'; export { EnvironmentService } from './environment.service'; export { FolderService } from './folder.service'; +export { LockService } from './lock.service'; export { MessagingService } from './messaging.service'; export { PasswordGenerationService } from './passwordGeneration.service'; export { PlatformUtilsService } from './platformUtils.service'; diff --git a/src/abstractions/lock.service.ts b/src/abstractions/lock.service.ts new file mode 100644 index 0000000000..2606f617ac --- /dev/null +++ b/src/abstractions/lock.service.ts @@ -0,0 +1,4 @@ +export interface LockService { + checkLock(): Promise; + lock(): Promise; +} diff --git a/src/services/index.ts b/src/services/index.ts index 0412506aef..a01df662ae 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -7,6 +7,7 @@ export { ContainerService } from './container.service'; export { CryptoService } from './crypto.service'; export { EnvironmentService } from './environment.service'; export { FolderService } from './folder.service'; +export { LockService } from './lock.service'; export { PasswordGenerationService } from './passwordGeneration.service'; export { SettingsService } from './settings.service'; export { TokenService } from './token.service'; diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts new file mode 100644 index 0000000000..e3b841a9a3 --- /dev/null +++ b/src/services/lock.service.ts @@ -0,0 +1,65 @@ +import { ConstantsService } from './constants.service'; + +import { CipherService } from '../abstractions/cipher.service'; +import { CollectionService } from '../abstractions/collection.service'; +import { CryptoService } from '../abstractions/crypto.service'; +import { FolderService } from '../abstractions/folder.service'; +import { LockService as LockServiceInterface } from '../abstractions/lock.service'; +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; +import { StorageService } from '../abstractions/storage.service'; + +export class LockService implements LockServiceInterface { + constructor(private cipherService: CipherService, private folderService: FolderService, + private collectionService: CollectionService, private cryptoService: CryptoService, + private platformUtilsService: PlatformUtilsService, + private storageService: StorageService, + private setIcon: Function, private refreshBadgeAndMenu: Function) { + this.checkLock(); + setInterval(() => this.checkLock(), 10 * 1000); // check every 10 seconds + } + + async checkLock(): Promise { + if (this.platformUtilsService.isViewOpen()) { + // Do not lock + return; + } + + const key = await this.cryptoService.getKey(); + if (key == null) { + // no key so no need to lock + return; + } + + const lockOption = await this.storageService.get(ConstantsService.lockOptionKey); + if (lockOption == null || lockOption < 0) { + return; + } + + const lastActive = await this.storageService.get(ConstantsService.lastActiveKey); + if (lastActive == null) { + return; + } + + const lockOptionSeconds = lockOption * 60; + const diffSeconds = ((new Date()).getTime() - lastActive) / 1000; + if (diffSeconds >= lockOptionSeconds) { + // need to lock now + await this.lock(); + } + } + + async lock(): Promise { + await Promise.all([ + this.cryptoService.clearKey(), + this.cryptoService.clearOrgKeys(true), + this.cryptoService.clearPrivateKey(true), + this.cryptoService.clearEncKey(true), + this.setIcon(), + this.refreshBadgeAndMenu(), + ]); + + this.folderService.clearCache(); + this.cipherService.clearCache(); + this.collectionService.clearCache(); + } +} From 6be48873cc70430e5b7e93723e1b6f1469f688f3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 23:25:59 -0500 Subject: [PATCH 0043/1626] move sync service to jslib --- src/abstractions/index.ts | 1 + src/abstractions/sync.service.ts | 9 ++ src/services/index.ts | 1 + src/services/sync.service.ts | 181 +++++++++++++++++++++++++++++++ 4 files changed, 192 insertions(+) create mode 100644 src/abstractions/sync.service.ts create mode 100644 src/services/sync.service.ts diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index 1bd679e41c..d74fbfe8a9 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -11,6 +11,7 @@ export { PasswordGenerationService } from './passwordGeneration.service'; export { PlatformUtilsService } from './platformUtils.service'; export { SettingsService } from './settings.service'; export { StorageService } from './storage.service'; +export { SyncService } from './sync.service'; export { TokenService } from './token.service'; export { TotpService } from './totp.service'; export { UserService } from './user.service'; diff --git a/src/abstractions/sync.service.ts b/src/abstractions/sync.service.ts new file mode 100644 index 0000000000..553124a27a --- /dev/null +++ b/src/abstractions/sync.service.ts @@ -0,0 +1,9 @@ +export interface SyncService { + syncInProgress: boolean; + + getLastSync(); + setLastSync(date: Date); + syncStarted(); + syncCompleted(successfully: boolean); + fullSync(forceSync: boolean); +} diff --git a/src/services/index.ts b/src/services/index.ts index a01df662ae..0e6ab04f75 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -10,6 +10,7 @@ export { FolderService } from './folder.service'; export { LockService } from './lock.service'; export { PasswordGenerationService } from './passwordGeneration.service'; export { SettingsService } from './settings.service'; +export { SyncService } from './sync.service'; export { TokenService } from './token.service'; export { TotpService } from './totp.service'; export { UserService } from './user.service'; diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts new file mode 100644 index 0000000000..550974c4ba --- /dev/null +++ b/src/services/sync.service.ts @@ -0,0 +1,181 @@ +import { ApiService } from '../abstractions/api.service'; +import { CipherService } from '../abstractions/cipher.service'; +import { CollectionService } from '../abstractions/collection.service'; +import { CryptoService } from '../abstractions/crypto.service'; +import { FolderService } from '../abstractions/folder.service'; +import { MessagingService } from '../abstractions/messaging.service'; +import { SettingsService } from '../abstractions/settings.service'; +import { StorageService } from '../abstractions/storage.service'; +import { SyncService as SyncServiceInterface } from '../abstractions/sync.service'; +import { UserService } from '../abstractions/user.service'; + +import { CipherData } from '../models/data/cipherData'; +import { CollectionData } from '../models/data/collectionData'; +import { FolderData } from '../models/data/folderData'; + +import { CipherResponse } from '../models/response/cipherResponse'; +import { CollectionResponse } from '../models/response/collectionResponse'; +import { DomainsResponse } from '../models/response/domainsResponse'; +import { FolderResponse } from '../models/response/folderResponse'; +import { ProfileResponse } from '../models/response/profileResponse'; + +const Keys = { + lastSyncPrefix: 'lastSync_', +}; + +export class SyncService implements SyncServiceInterface { + syncInProgress: boolean = false; + + constructor(private userService: UserService, private apiService: ApiService, + private settingsService: SettingsService, private folderService: FolderService, + private cipherService: CipherService, private cryptoService: CryptoService, + private collectionService: CollectionService, private storageService: StorageService, + private messagingService: MessagingService, private logoutCallback: Function) { + } + + async getLastSync() { + const userId = await this.userService.getUserId(); + const lastSync = await this.storageService.get(Keys.lastSyncPrefix + userId); + if (lastSync) { + return new Date(lastSync); + } + + return null; + } + + async setLastSync(date: Date) { + const userId = await this.userService.getUserId(); + await this.storageService.save(Keys.lastSyncPrefix + userId, date.toJSON()); + } + + syncStarted() { + this.syncInProgress = true; + this.messagingService.send('syncStarted'); + } + + syncCompleted(successfully: boolean) { + this.syncInProgress = false; + this.messagingService.send('syncCompleted', { successfully: successfully }); + } + + async fullSync(forceSync: boolean) { + this.syncStarted(); + const isAuthenticated = await this.userService.isAuthenticated(); + if (!isAuthenticated) { + this.syncCompleted(false); + return false; + } + + const now = new Date(); + const needsSyncResult = await this.needsSyncing(forceSync); + const needsSync = needsSyncResult[0]; + const skipped = needsSyncResult[1]; + + if (skipped) { + this.syncCompleted(false); + return false; + } + + if (!needsSync) { + await this.setLastSync(now); + this.syncCompleted(false); + return false; + } + + const userId = await this.userService.getUserId(); + try { + const response = await this.apiService.getSync(); + + await this.syncProfile(response.profile); + await this.syncFolders(userId, response.folders); + await this.syncCollections(response.collections); + await this.syncCiphers(userId, response.ciphers); + await this.syncSettings(userId, response.domains); + + await this.setLastSync(now); + this.syncCompleted(true); + return true; + } catch (e) { + this.syncCompleted(false); + return false; + } + } + + // Helpers + + private async needsSyncing(forceSync: boolean) { + if (forceSync) { + return [true, false]; + } + + try { + const response = await this.apiService.getAccountRevisionDate(); + const accountRevisionDate = new Date(response); + const lastSync = await this.getLastSync(); + if (lastSync != null && accountRevisionDate <= lastSync) { + return [false, false]; + } + + return [true, false]; + } catch (e) { + return [false, true]; + } + } + + private async syncProfile(response: ProfileResponse) { + const stamp = await this.userService.getSecurityStamp(); + if (stamp != null && stamp !== response.securityStamp) { + if (this.logoutCallback != null) { + this.logoutCallback(true); + } + + throw new Error('Stamp has changed'); + } + + await this.cryptoService.setEncKey(response.key); + await this.cryptoService.setEncPrivateKey(response.privateKey); + await this.cryptoService.setOrgKeys(response.organizations); + await this.userService.setSecurityStamp(response.securityStamp); + } + + private async syncFolders(userId: string, response: FolderResponse[]) { + const folders: { [id: string]: FolderData; } = {}; + response.forEach((f) => { + folders[f.id] = new FolderData(f, userId); + }); + return await this.folderService.replace(folders); + } + + private async syncCollections(response: CollectionResponse[]) { + const collections: { [id: string]: CollectionData; } = {}; + response.forEach((c) => { + collections[c.id] = new CollectionData(c); + }); + return await this.collectionService.replace(collections); + } + + private async syncCiphers(userId: string, response: CipherResponse[]) { + const ciphers: { [id: string]: CipherData; } = {}; + response.forEach((c) => { + ciphers[c.id] = new CipherData(c, userId); + }); + return await this.cipherService.replace(ciphers); + } + + private async syncSettings(userId: string, response: DomainsResponse) { + let eqDomains: string[][] = []; + if (response != null && response.equivalentDomains != null) { + eqDomains = eqDomains.concat(response.equivalentDomains); + } + + if (response != null && response.globalEquivalentDomains != null) { + response.globalEquivalentDomains.forEach((global) => { + if (global.domains.length > 0) { + eqDomains.push(global.domains); + } + }); + } + + return this.settingsService.setEquivalentDomains(eqDomains); + } +} From 258178dc1659d4b35edbbdfb026c7f1078dab38b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 23:31:09 -0500 Subject: [PATCH 0044/1626] added missing implements --- src/abstractions/cipher.service.ts | 8 ++++---- src/services/collection.service.ts | 3 ++- src/services/environment.service.ts | 2 +- src/services/settings.service.ts | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index d68aa99549..58f092b696 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -1,8 +1,8 @@ -import { CipherData } from '../models/data'; +import { CipherData } from '../models/data/cipherData'; -import { Cipher } from '../models/domain'; -import { Field } from '../models/domain'; -import { SymmetricCryptoKey } from '../models/domain'; +import { Cipher } from '../models/domain/cipher'; +import { Field } from '../models/domain/field'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; export interface CipherService { decryptedCipherCache: any[]; diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index c409925cd7..35327f1e2a 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -2,6 +2,7 @@ import { CollectionData } from '../models/data/collectionData'; import { Collection } from '../models/domain/collection'; +import { CollectionService as CollectionServiceInterface } from '../abstractions/collection.service'; import { CryptoService } from '../abstractions/crypto.service'; import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; @@ -10,7 +11,7 @@ const Keys = { collectionsPrefix: 'collections_', }; -export class CollectionService { +export class CollectionService implements CollectionServiceInterface { decryptedCollectionCache: any[]; constructor(private cryptoService: CryptoService, private userService: UserService, diff --git a/src/services/environment.service.ts b/src/services/environment.service.ts index 292ee89a4d..2ba63ad6bf 100644 --- a/src/services/environment.service.ts +++ b/src/services/environment.service.ts @@ -6,7 +6,7 @@ import { ApiService } from '../abstractions/api.service'; import { EnvironmentService as EnvironmentServiceInterface } from '../abstractions/environment.service'; import { StorageService } from '../abstractions/storage.service'; -export class EnvironmentService { +export class EnvironmentService implements EnvironmentServiceInterface { baseUrl: string; webVaultUrl: string; apiUrl: string; diff --git a/src/services/settings.service.ts b/src/services/settings.service.ts index 0f1227c7a8..87302a638c 100644 --- a/src/services/settings.service.ts +++ b/src/services/settings.service.ts @@ -7,7 +7,7 @@ const Keys = { equivalentDomains: 'equivalentDomains', }; -export class SettingsService { +export class SettingsService implements SettingsServiceInterface { private settingsCache: any; constructor(private userService: UserService, private storageService: StorageService) { From 808e74bcfbe6d548ff1e36ac3ae4483e50d9624c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jan 2018 23:34:33 -0500 Subject: [PATCH 0045/1626] added missing return types to sync service --- src/abstractions/sync.service.ts | 10 +++++----- src/services/sync.service.ts | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/abstractions/sync.service.ts b/src/abstractions/sync.service.ts index 553124a27a..a3287c39c9 100644 --- a/src/abstractions/sync.service.ts +++ b/src/abstractions/sync.service.ts @@ -1,9 +1,9 @@ export interface SyncService { syncInProgress: boolean; - getLastSync(); - setLastSync(date: Date); - syncStarted(); - syncCompleted(successfully: boolean); - fullSync(forceSync: boolean); + getLastSync(): Promise; + setLastSync(date: Date): Promise; + syncStarted(): void; + syncCompleted(successfully: boolean): void; + fullSync(forceSync: boolean): Promise; } diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index 550974c4ba..79e726c9aa 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -33,7 +33,7 @@ export class SyncService implements SyncServiceInterface { private messagingService: MessagingService, private logoutCallback: Function) { } - async getLastSync() { + async getLastSync(): Promise { const userId = await this.userService.getUserId(); const lastSync = await this.storageService.get(Keys.lastSyncPrefix + userId); if (lastSync) { @@ -43,7 +43,7 @@ export class SyncService implements SyncServiceInterface { return null; } - async setLastSync(date: Date) { + async setLastSync(date: Date): Promise { const userId = await this.userService.getUserId(); await this.storageService.save(Keys.lastSyncPrefix + userId, date.toJSON()); } @@ -58,7 +58,7 @@ export class SyncService implements SyncServiceInterface { this.messagingService.send('syncCompleted', { successfully: successfully }); } - async fullSync(forceSync: boolean) { + async fullSync(forceSync: boolean): Promise { this.syncStarted(); const isAuthenticated = await this.userService.isAuthenticated(); if (!isAuthenticated) { From ce628ba1dba8f0c1164fb3f62f92cbdc890da89d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 11 Jan 2018 14:00:32 -0500 Subject: [PATCH 0046/1626] rename to bitwardenContainerService --- src/models/domain/cipher.ts | 4 ++-- src/models/domain/cipherString.ts | 4 ++-- src/services/container.service.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/models/domain/cipher.ts b/src/models/domain/cipher.ts index 5c3c1076d9..cae31fb260 100644 --- a/src/models/domain/cipher.ts +++ b/src/models/domain/cipher.ts @@ -117,13 +117,13 @@ class Cipher extends Domain { model.login = await this.login.decrypt(this.organizationId); model.subTitle = model.login.username; if (model.login.uri) { - const containerService = (window as any).BitwardenContainerService; + 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.'); + throw new Error('window.bitwardenContainerService not initialized.'); } } break; diff --git a/src/models/domain/cipherString.ts b/src/models/domain/cipherString.ts index eb37e08df0..2a5890e5a6 100644 --- a/src/models/domain/cipherString.ts +++ b/src/models/domain/cipherString.ts @@ -93,11 +93,11 @@ class CipherString { } let cryptoService: CryptoService; - const containerService = (window as any).BitwardenContainerService; + const containerService = (window as any).bitwardenContainerService; if (containerService) { cryptoService = containerService.getCryptoService(); } else { - throw new Error('window.BitwardenContainerService not initialized.'); + throw new Error('window.bitwardenContainerService not initialized.'); } return cryptoService.getOrgKey(orgId).then((orgKey: any) => { diff --git a/src/services/container.service.ts b/src/services/container.service.ts index 31896d8b40..fa9a6628ce 100644 --- a/src/services/container.service.ts +++ b/src/services/container.service.ts @@ -7,8 +7,8 @@ export class ContainerService { } attachToWindow(win: any) { - if (!win.BitwardenContainerService) { - win.BitwardenContainerService = this; + if (!win.bitwardenContainerService) { + win.bitwardenContainerService = this; } } From d4645c8c2ea6c23feddbbdf810ce64d7defe25e4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 11 Jan 2018 15:21:52 -0500 Subject: [PATCH 0047/1626] remove initListSectionItemListeners --- src/abstractions/platformUtils.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index 7e500f684b..3329761c2c 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -8,7 +8,6 @@ export interface PlatformUtilsService { isEdge(): boolean; isOpera(): boolean; analyticsId(): string; - initListSectionItemListeners(doc: Document, angular: any): void; getDomain(uriString: string): string; inSidebar(theWindow: Window): boolean; inTab(theWindow: Window): boolean; From 27d858f87957d2c73bab6e4ec1d1157cd01f7c20 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 11 Jan 2018 15:30:36 -0500 Subject: [PATCH 0048/1626] remove "in UI" functions --- src/abstractions/platformUtils.service.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index 3329761c2c..d9902c2eec 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -9,9 +9,5 @@ export interface PlatformUtilsService { isOpera(): boolean; analyticsId(): string; getDomain(uriString: string): string; - inSidebar(theWindow: Window): boolean; - inTab(theWindow: Window): boolean; - inPopout(theWindow: Window): boolean; - inPopup(theWindow: Window): boolean; isViewOpen(): boolean; } From b1e4c98f6da1c22e968c232d984fcfb5e1ccc13b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 11 Jan 2018 15:34:51 -0500 Subject: [PATCH 0049/1626] add safari to interface --- src/abstractions/platformUtils.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index d9902c2eec..633a5cc1d4 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -7,6 +7,7 @@ export interface PlatformUtilsService { isChrome(): boolean; isEdge(): boolean; isOpera(): boolean; + isSafari(): boolean; analyticsId(): string; getDomain(uriString: string): string; isViewOpen(): boolean; From 583b7c3b1551cae7bfe4dcd40f34452f59b969ea Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 12 Jan 2018 21:56:35 -0500 Subject: [PATCH 0050/1626] delay i18n evaluations --- src/services/constants.service.ts | 6 +++--- src/services/folder.service.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index 6ae5038d52..4a5f96dd60 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -56,12 +56,12 @@ export class ConstantsService { twoFactorProviderInfo: any[]; - constructor(i18nService: any, platformUtilsService: PlatformUtilsService) { - if (platformUtilsService.isEdge()) { + constructor(i18nService: any, delayLoad: number) { + if (delayLoad && delayLoad > 0) { // delay for i18n fetch setTimeout(() => { this.bootstrap(i18nService); - }, 1000); + }, delayLoad); } else { this.bootstrap(i18nService); } diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index a1ba9718ea..aa027fdd6a 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -20,7 +20,7 @@ export class FolderService implements FolderServiceInterface { decryptedFolderCache: any[]; constructor(private cryptoService: CryptoService, private userService: UserService, - private noneFolder: string, private apiService: ApiService, + private noneFolder: () => string, private apiService: ApiService, private storageService: StorageService) { } @@ -66,7 +66,7 @@ export class FolderService implements FolderServiceInterface { const decFolders: any[] = [{ id: null, - name: this.noneFolder, + name: this.noneFolder(), }]; const key = await this.cryptoService.getKey(); From 229c632c4fda1d96f852aa0d4d62259fcc403b32 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 13 Jan 2018 10:14:00 -0500 Subject: [PATCH 0051/1626] never domains key --- src/services/constants.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index 4a5f96dd60..4275d67874 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -10,6 +10,7 @@ export class ConstantsService { static readonly enableAutoFillOnPageLoadKey: string = 'enableAutoFillOnPageLoad'; static readonly lockOptionKey: string = 'lockOption'; static readonly lastActiveKey: string = 'lastActive'; + static readonly neverDomainsKey: string = 'neverDomains'; readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; readonly disableGaKey: string = ConstantsService.disableGaKey; @@ -20,6 +21,7 @@ export class ConstantsService { readonly enableAutoFillOnPageLoadKey: string = ConstantsService.enableAutoFillOnPageLoadKey; readonly lockOptionKey: string = ConstantsService.lockOptionKey; readonly lastActiveKey: string = ConstantsService.lastActiveKey; + readonly neverDomainsKey: string = ConstantsService.neverDomainsKey; // TODO: Convert these objects to enums readonly encType: any = { From 332a3b87c86c9ad7a709f58fbd1f471ac06be003 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 17 Jan 2018 13:38:44 -0500 Subject: [PATCH 0052/1626] add vivaldi to interface --- src/abstractions/platformUtils.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index 633a5cc1d4..c1da1e8cc4 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -7,6 +7,7 @@ export interface PlatformUtilsService { isChrome(): boolean; isEdge(): boolean; isOpera(): boolean; + isVivaldi(): boolean; isSafari(): boolean; analyticsId(): string; getDomain(uriString: string): string; From 59f2fc0e737c11379efa8597f4d4e9a8a6b3817d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 18 Jan 2018 00:08:10 -0500 Subject: [PATCH 0053/1626] add b64 output encoding option for decrypt --- src/services/crypto.service.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index adce2e7911..2b378c4202 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -148,8 +148,7 @@ export class CryptoService implements CryptoServiceInterface { return null; } - const privateKey = await this.decrypt(new CipherString(encPrivateKey), null, 'raw'); - const privateKeyB64 = forge.util.encode64(privateKey); + const privateKeyB64 = await this.decrypt(new CipherString(encPrivateKey), null, 'b64'); this.privateKey = UtilsService.fromB64ToArray(privateKeyB64).buffer; return this.privateKey; } @@ -327,8 +326,13 @@ export class CryptoService implements CryptoServiceInterface { if (outputEncoding === 'utf8') { return decipher.output.toString('utf8'); + } + + const decipherBytes = decipher.output.getBytes(); + if (outputEncoding === 'b64') { + return forge.util.encode64(decipherBytes); } else { - return decipher.output.getBytes(); + return decipherBytes; } } From 335c94e24c50fdf10d5fbec9f491c2c20677be1d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 18 Jan 2018 09:03:27 -0500 Subject: [PATCH 0054/1626] macBytes must exist if key has macKey --- src/services/crypto.service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 2b378c4202..d58df100fd 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -482,6 +482,11 @@ export class CryptoService implements CryptoServiceInterface { const keyForEnc = await this.getKeyForEncryption(key); const theKey = this.resolveLegacyKey(encType, keyForEnc); + if (theKey.macKey != null && macBytes == null) { + console.error('macBytes required.'); + return null; + } + if (encType !== theKey.encType) { // tslint:disable-next-line console.error('encType unavailable.'); From 0bbb11869164260d77cc63326f4794b90990299c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 18 Jan 2018 09:59:40 -0500 Subject: [PATCH 0055/1626] use random bytes for each HMAC comparison --- src/services/crypto.service.ts | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index d58df100fd..6761a89edf 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -414,7 +414,7 @@ export class CryptoService implements CryptoServiceInterface { 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); + const macsEqual = await this.macsEqual(macBytes, computedMacBytes); if (!macsEqual) { throw new Error('MAC failed.'); } @@ -495,7 +495,7 @@ export class CryptoService implements CryptoServiceInterface { if (theKey.macKey != null && macBytes != null) { const computedMacBytes = this.computeMac(ivBytes + ctBytes, theKey.macKey, false); - if (!this.macsEqual(theKey.macKey, computedMacBytes, macBytes)) { + if (!this.macsEqual(computedMacBytes, macBytes)) { // tslint:disable-next-line console.error('MAC failed.'); return null; @@ -528,7 +528,7 @@ export class CryptoService implements CryptoServiceInterface { return null; } - const macsMatch = await this.macsEqualWC(keyBuf.macKey, macBuf, computedMacBuf); + const macsMatch = await this.macsEqualWC(macBuf, computedMacBuf); if (macsMatch === false) { // tslint:disable-next-line console.error('MAC failed.'); @@ -553,10 +553,11 @@ export class CryptoService implements CryptoServiceInterface { // 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 { + // ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy + private macsEqual(mac1: string, mac2: string): boolean { const hmac = (forge as any).hmac.create(); - hmac.start('sha256', macKey); + hmac.start('sha256', this.getRandomBytes(32)); hmac.update(mac1); const mac1Bytes = hmac.digest().getBytes(); @@ -567,8 +568,10 @@ export class CryptoService implements CryptoServiceInterface { return mac1Bytes === mac2Bytes; } - private async macsEqualWC(macKeyBuf: ArrayBuffer, mac1Buf: ArrayBuffer, mac2Buf: ArrayBuffer): Promise { - const macKey = await Subtle.importKey('raw', macKeyBuf, SigningAlgorithm, false, ['sign']); + private async macsEqualWC(mac1Buf: ArrayBuffer, mac2Buf: ArrayBuffer): Promise { + const compareKey = new Uint8Array(32); + Crypto.getRandomValues(compareKey); + const macKey = await Subtle.importKey('raw', compareKey.buffer, SigningAlgorithm, false, ['sign']); const mac1 = await Subtle.sign(SigningAlgorithm, macKey, mac1Buf); const mac2 = await Subtle.sign(SigningAlgorithm, macKey, mac2Buf); @@ -608,4 +611,14 @@ export class CryptoService implements CryptoServiceInterface { return key; } + + private getRandomBytes(byteLength: number): string { + const bytes = new Uint32Array(byteLength / 4); + Crypto.getRandomValues(bytes); + const buffer = forge.util.createBuffer(); + for (let i = 0; i < bytes.length; i++) { + buffer.putInt32(bytes[i]); + } + return buffer.getBytes(); + } } From 8636033159aa5126719e665acee9ae733d94f433 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 18 Jan 2018 12:14:30 -0500 Subject: [PATCH 0056/1626] resolve link warning on missing mac --- src/services/crypto.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 6761a89edf..84391897f8 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -483,6 +483,7 @@ export class CryptoService implements CryptoServiceInterface { const theKey = this.resolveLegacyKey(encType, keyForEnc); if (theKey.macKey != null && macBytes == null) { + // tslint:disable-next-line console.error('macBytes required.'); return null; } From e753becd336c328b9fab5644a2dde9ba59f683a1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 19 Jan 2018 14:58:06 -0500 Subject: [PATCH 0057/1626] lazy load history --- .../passwordGeneration.service.ts | 2 +- src/services/passwordGeneration.service.ts | 54 +++++++++++-------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/abstractions/passwordGeneration.service.ts b/src/abstractions/passwordGeneration.service.ts index 1f2433842b..1531eb176b 100644 --- a/src/abstractions/passwordGeneration.service.ts +++ b/src/abstractions/passwordGeneration.service.ts @@ -6,7 +6,7 @@ export interface PasswordGenerationService { generatePassword(options: any): string; getOptions(): any; saveOptions(options: any): Promise; - getHistory(): PasswordHistory[]; + getHistory(): Promise; addHistory(password: string): Promise; clear(): Promise; } diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 771494d0bc..403bb104e2 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -150,13 +150,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceInter optionsCache: any; history: PasswordHistory[] = []; - constructor(private cryptoService: CryptoService, - private storageService: StorageService) { - storageService.get(Keys.history).then((encrypted) => { - return this.decryptHistory(encrypted); - }).then((history) => { - this.history = history; - }); + constructor(private cryptoService: CryptoService, private storageService: StorageService) { } generatePassword(options: any) { @@ -181,24 +175,42 @@ export class PasswordGenerationService implements PasswordGenerationServiceInter this.optionsCache = options; } - getHistory() { + async getHistory(): Promise { + const hasKey = (await this.cryptoService.getKey()) != null; + if (!hasKey) { + return new Array(); + } + + if (!this.history) { + const encrypted = await this.storageService.get(Keys.history); + this.history = await this.decryptHistory(encrypted); + } + return this.history || new Array(); } async addHistory(password: string): Promise { - // Prevent duplicates - if (this.matchesPrevious(password)) { + // Cannot add new history if no key is available + const hasKey = (await this.cryptoService.getKey()) != null; + if (!hasKey) { return; } - this.history.push(new PasswordHistory(password, Date.now())); + const currentHistory = await this.getHistory(); - // Remove old items. - if (this.history.length > MaxPasswordsInHistory) { - this.history.shift(); + // Prevent duplicates + if (this.matchesPrevious(password, currentHistory)) { + return; } - const newHistory = await this.encryptHistory(); + currentHistory.push(new PasswordHistory(password, Date.now())); + + // Remove old items. + if (currentHistory.length > MaxPasswordsInHistory) { + currentHistory.shift(); + } + + const newHistory = await this.encryptHistory(currentHistory); return await this.storageService.save(Keys.history, newHistory); } @@ -207,12 +219,12 @@ export class PasswordGenerationService implements PasswordGenerationServiceInter return await this.storageService.remove(Keys.history); } - private async encryptHistory(): Promise { - if (this.history == null || this.history.length === 0) { + private async encryptHistory(history: PasswordHistory[]): Promise { + if (history == null || history.length === 0) { return Promise.resolve([]); } - const promises = this.history.map(async (item) => { + const promises = history.map(async (item) => { const encrypted = await this.cryptoService.encrypt(item.password); return new PasswordHistory(encrypted.encryptedString, item.date); }); @@ -233,11 +245,11 @@ export class PasswordGenerationService implements PasswordGenerationServiceInter return await Promise.all(promises); } - private matchesPrevious(password: string): boolean { - if (this.history == null || this.history.length === 0) { + private matchesPrevious(password: string, history: PasswordHistory[]): boolean { + if (history == null || history.length === 0) { return false; } - return this.history[this.history.length - 1].password === password; + return history[history.length - 1].password === password; } } From e157d5be6d5789231ae3d66f0db096590d411403 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 20 Jan 2018 14:12:18 -0500 Subject: [PATCH 0058/1626] remove model window assignments --- src/models/data/attachmentData.ts | 5 +---- src/models/data/cardData.ts | 5 +---- src/models/data/cipherData.ts | 5 +---- src/models/data/collectionData.ts | 5 +---- src/models/data/fieldData.ts | 5 +---- src/models/data/folderData.ts | 5 +---- src/models/data/identityData.ts | 5 +---- src/models/data/loginData.ts | 5 +---- src/models/data/secureNoteData.ts | 5 +---- src/models/domain/attachment.ts | 5 +---- src/models/domain/card.ts | 5 +---- src/models/domain/cipher.ts | 5 +---- src/models/domain/cipherString.ts | 5 +---- src/models/domain/collection.ts | 5 +---- src/models/domain/field.ts | 5 +---- src/models/domain/folder.ts | 5 +---- src/models/domain/identity.ts | 5 +---- src/models/domain/login.ts | 5 +---- src/models/domain/secureNote.ts | 5 +---- src/models/request/cipherRequest.ts | 5 +---- src/models/request/deviceRequest.ts | 5 +---- src/models/request/deviceTokenRequest.ts | 5 +---- src/models/request/folderRequest.ts | 5 +---- src/models/request/passwordHintRequest.ts | 5 +---- src/models/request/registerRequest.ts | 5 +---- src/models/request/tokenRequest.ts | 5 +---- src/models/request/twoFactorEmailRequest.ts | 5 +---- src/models/response/attachmentResponse.ts | 5 +---- src/models/response/cipherResponse.ts | 5 +---- src/models/response/collectionResponse.ts | 5 +---- src/models/response/deviceResponse.ts | 5 +---- src/models/response/domainsResponse.ts | 5 +---- src/models/response/errorResponse.ts | 5 +---- src/models/response/folderResponse.ts | 5 +---- src/models/response/globalDomainResponse.ts | 5 +---- src/models/response/identityTokenResponse.ts | 5 +---- src/models/response/keysResponse.ts | 5 +---- src/models/response/listResponse.ts | 5 +---- src/models/response/profileOrganizationResponse.ts | 5 +---- src/models/response/profileResponse.ts | 5 +---- src/models/response/syncResponse.ts | 5 +---- 41 files changed, 41 insertions(+), 164 deletions(-) diff --git a/src/models/data/attachmentData.ts b/src/models/data/attachmentData.ts index 2ff3ab423c..85aa452e1e 100644 --- a/src/models/data/attachmentData.ts +++ b/src/models/data/attachmentData.ts @@ -1,6 +1,6 @@ import { AttachmentResponse } from '../response/attachmentResponse'; -class AttachmentData { +export class AttachmentData { id: string; url: string; fileName: string; @@ -15,6 +15,3 @@ class AttachmentData { 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 index f0b9f63f2e..238621c72a 100644 --- a/src/models/data/cardData.ts +++ b/src/models/data/cardData.ts @@ -1,4 +1,4 @@ -class CardData { +export class CardData { cardholderName: string; brand: string; number: string; @@ -15,6 +15,3 @@ class CardData { 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 index a0917ed314..0a4f83c520 100644 --- a/src/models/data/cipherData.ts +++ b/src/models/data/cipherData.ts @@ -9,7 +9,7 @@ import { SecureNoteData } from './secureNoteData'; import { CipherResponse } from '../response/cipherResponse'; -class CipherData { +export class CipherData { id: string; organizationId: string; folderId: string; @@ -82,6 +82,3 @@ class CipherData { } } } - -export { CipherData }; -(window as any).CipherData = CipherData; diff --git a/src/models/data/collectionData.ts b/src/models/data/collectionData.ts index f2d5fc9f03..3b05b9af22 100644 --- a/src/models/data/collectionData.ts +++ b/src/models/data/collectionData.ts @@ -1,6 +1,6 @@ import { CollectionResponse } from '../response/collectionResponse'; -class CollectionData { +export class CollectionData { id: string; organizationId: string; name: string; @@ -11,6 +11,3 @@ class CollectionData { 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 index 920d1c56fa..cd0fb2c42b 100644 --- a/src/models/data/fieldData.ts +++ b/src/models/data/fieldData.ts @@ -1,6 +1,6 @@ import { FieldType } from '../../enums/fieldType'; -class FieldData { +export class FieldData { type: FieldType; name: string; value: string; @@ -11,6 +11,3 @@ class FieldData { 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 index 6f03781cc7..509267c9d4 100644 --- a/src/models/data/folderData.ts +++ b/src/models/data/folderData.ts @@ -1,6 +1,6 @@ import { FolderResponse } from '../response/folderResponse'; -class FolderData { +export class FolderData { id: string; userId: string; name: string; @@ -13,6 +13,3 @@ class FolderData { 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 index ee849166ee..34f2099595 100644 --- a/src/models/data/identityData.ts +++ b/src/models/data/identityData.ts @@ -1,4 +1,4 @@ -class IdentityData { +export class IdentityData { title: string; firstName: string; middleName: string; @@ -39,6 +39,3 @@ class IdentityData { this.licenseNumber = data.LicenseNumber; } } - -export { IdentityData }; -(window as any).IdentityData = IdentityData; diff --git a/src/models/data/loginData.ts b/src/models/data/loginData.ts index de0aecc133..2052a7a889 100644 --- a/src/models/data/loginData.ts +++ b/src/models/data/loginData.ts @@ -1,4 +1,4 @@ -class LoginData { +export class LoginData { uri: string; username: string; password: string; @@ -11,6 +11,3 @@ class LoginData { 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 index bf861a4a60..4b27125a13 100644 --- a/src/models/data/secureNoteData.ts +++ b/src/models/data/secureNoteData.ts @@ -1,12 +1,9 @@ import { SecureNoteType } from '../../enums/secureNoteType'; -class SecureNoteData { +export 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 index d77152b007..28859be366 100644 --- a/src/models/domain/attachment.ts +++ b/src/models/domain/attachment.ts @@ -3,7 +3,7 @@ import { AttachmentData } from '../data/attachmentData'; import { CipherString } from './cipherString'; import Domain from './domain'; -class Attachment extends Domain { +export class Attachment extends Domain { id: string; url: string; size: number; @@ -38,6 +38,3 @@ class Attachment extends Domain { }, orgId); } } - -export { Attachment }; -(window as any).Attachment = Attachment; diff --git a/src/models/domain/card.ts b/src/models/domain/card.ts index 3e42f7affb..ee6fbd0616 100644 --- a/src/models/domain/card.ts +++ b/src/models/domain/card.ts @@ -3,7 +3,7 @@ import { CardData } from '../data/cardData'; import { CipherString } from './cipherString'; import Domain from './domain'; -class Card extends Domain { +export class Card extends Domain { cardholderName: CipherString; brand: CipherString; number: CipherString; @@ -38,6 +38,3 @@ class Card extends Domain { }, orgId); } } - -export { Card }; -(window as any).Card = Card; diff --git a/src/models/domain/cipher.ts b/src/models/domain/cipher.ts index cae31fb260..9dfbafdac0 100644 --- a/src/models/domain/cipher.ts +++ b/src/models/domain/cipher.ts @@ -13,7 +13,7 @@ import { Identity } from './identity'; import { Login } from './login'; import { SecureNote } from './secureNote'; -class Cipher extends Domain { +export class Cipher extends Domain { id: string; organizationId: string; folderId: string; @@ -187,6 +187,3 @@ class Cipher extends Domain { return model; } } - -export { Cipher }; -(window as any).Cipher = Cipher; diff --git a/src/models/domain/cipherString.ts b/src/models/domain/cipherString.ts index 2a5890e5a6..d19d191065 100644 --- a/src/models/domain/cipherString.ts +++ b/src/models/domain/cipherString.ts @@ -2,7 +2,7 @@ import { EncryptionType } from '../../enums/encryptionType'; import { CryptoService } from '../../abstractions/crypto.service'; -class CipherString { +export class CipherString { encryptedString?: string; encryptionType?: EncryptionType; decryptedValue?: string; @@ -111,6 +111,3 @@ class CipherString { }); } } - -export { CipherString }; -(window as any).CipherString = CipherString; diff --git a/src/models/domain/collection.ts b/src/models/domain/collection.ts index 0a5079e53a..4857ec2c44 100644 --- a/src/models/domain/collection.ts +++ b/src/models/domain/collection.ts @@ -3,7 +3,7 @@ import { CollectionData } from '../data/collectionData'; import { CipherString } from './cipherString'; import Domain from './domain'; -class Collection extends Domain { +export class Collection extends Domain { id: string; organizationId: string; name: CipherString; @@ -32,6 +32,3 @@ class Collection extends Domain { }, this.organizationId); } } - -export { Collection }; -(window as any).Collection = Collection; diff --git a/src/models/domain/field.ts b/src/models/domain/field.ts index c806e2dd12..d118ad2463 100644 --- a/src/models/domain/field.ts +++ b/src/models/domain/field.ts @@ -5,7 +5,7 @@ import { FieldData } from '../data/fieldData'; import { CipherString } from './cipherString'; import Domain from './domain'; -class Field extends Domain { +export class Field extends Domain { name: CipherString; vault: CipherString; type: FieldType; @@ -34,6 +34,3 @@ class Field extends Domain { }, orgId); } } - -export { Field }; -(window as any).Field = Field; diff --git a/src/models/domain/folder.ts b/src/models/domain/folder.ts index 180cd44e4f..f25d1f8314 100644 --- a/src/models/domain/folder.ts +++ b/src/models/domain/folder.ts @@ -3,7 +3,7 @@ import { FolderData } from '../data/folderData'; import { CipherString } from './cipherString'; import Domain from './domain'; -class Folder extends Domain { +export class Folder extends Domain { id: string; name: CipherString; @@ -29,6 +29,3 @@ class Folder extends Domain { }, null); } } - -export { Folder }; -(window as any).Folder = Folder; diff --git a/src/models/domain/identity.ts b/src/models/domain/identity.ts index ede933ac30..2b8323e113 100644 --- a/src/models/domain/identity.ts +++ b/src/models/domain/identity.ts @@ -3,7 +3,7 @@ import { IdentityData } from '../data/identityData'; import { CipherString } from './cipherString'; import Domain from './domain'; -class Identity extends Domain { +export class Identity extends Domain { title: CipherString; firstName: CipherString; middleName: CipherString; @@ -74,6 +74,3 @@ class Identity extends Domain { }, orgId); } } - -export { Identity }; -(window as any).Identity = Identity; diff --git a/src/models/domain/login.ts b/src/models/domain/login.ts index 8ed1e1f7c7..5d0f50c378 100644 --- a/src/models/domain/login.ts +++ b/src/models/domain/login.ts @@ -3,7 +3,7 @@ import { LoginData } from '../data/loginData'; import { CipherString } from './cipherString'; import Domain from './domain'; -class Login extends Domain { +export class Login extends Domain { uri: CipherString; username: CipherString; password: CipherString; @@ -32,6 +32,3 @@ class Login extends Domain { }, orgId); } } - -export { Login }; -(window as any).Login = Login; diff --git a/src/models/domain/secureNote.ts b/src/models/domain/secureNote.ts index 46609a5b70..8feb7de3ad 100644 --- a/src/models/domain/secureNote.ts +++ b/src/models/domain/secureNote.ts @@ -4,7 +4,7 @@ import { SecureNoteData } from '../data/secureNoteData'; import Domain from './domain'; -class SecureNote extends Domain { +export class SecureNote extends Domain { type: SecureNoteType; constructor(obj?: SecureNoteData, alreadyEncrypted: boolean = false) { @@ -22,6 +22,3 @@ class SecureNote extends Domain { }; } } - -export { SecureNote }; -(window as any).SecureNote = SecureNote; diff --git a/src/models/request/cipherRequest.ts b/src/models/request/cipherRequest.ts index 4829b20d27..232883f323 100644 --- a/src/models/request/cipherRequest.ts +++ b/src/models/request/cipherRequest.ts @@ -1,6 +1,6 @@ import { CipherType } from '../../enums/cipherType'; -class CipherRequest { +export class CipherRequest { type: CipherType; folderId: string; organizationId: string; @@ -84,6 +84,3 @@ class CipherRequest { } } } - -export { CipherRequest }; -(window as any).CipherRequest = CipherRequest; diff --git a/src/models/request/deviceRequest.ts b/src/models/request/deviceRequest.ts index 5aaec5c77e..2aaa42cbe0 100644 --- a/src/models/request/deviceRequest.ts +++ b/src/models/request/deviceRequest.ts @@ -2,7 +2,7 @@ import { DeviceType } from '../../enums/deviceType'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; -class DeviceRequest { +export class DeviceRequest { type: DeviceType; name: string; identifier: string; @@ -15,6 +15,3 @@ class DeviceRequest { this.pushToken = null; } } - -export { DeviceRequest }; -(window as any).DeviceRequest = DeviceRequest; diff --git a/src/models/request/deviceTokenRequest.ts b/src/models/request/deviceTokenRequest.ts index 69ef20bb9a..5d663f6096 100644 --- a/src/models/request/deviceTokenRequest.ts +++ b/src/models/request/deviceTokenRequest.ts @@ -1,10 +1,7 @@ -class DeviceTokenRequest { +export 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 index 7ff9794b18..54ec76cac5 100644 --- a/src/models/request/folderRequest.ts +++ b/src/models/request/folderRequest.ts @@ -1,12 +1,9 @@ import { Folder } from '../domain/folder'; -class FolderRequest { +export 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/passwordHintRequest.ts b/src/models/request/passwordHintRequest.ts index 4feb92944b..35b37f4f36 100644 --- a/src/models/request/passwordHintRequest.ts +++ b/src/models/request/passwordHintRequest.ts @@ -1,10 +1,7 @@ -class PasswordHintRequest { +export 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 index 4b80fb7884..fc815d5bb7 100644 --- a/src/models/request/registerRequest.ts +++ b/src/models/request/registerRequest.ts @@ -1,4 +1,4 @@ -class RegisterRequest { +export class RegisterRequest { name: string; email: string; masterPasswordHash: string; @@ -13,6 +13,3 @@ class RegisterRequest { this.key = key; } } - -export { RegisterRequest }; -(window as any).RegisterRequest = RegisterRequest; diff --git a/src/models/request/tokenRequest.ts b/src/models/request/tokenRequest.ts index 4cf7564615..6cce0f23b0 100644 --- a/src/models/request/tokenRequest.ts +++ b/src/models/request/tokenRequest.ts @@ -1,6 +1,6 @@ import { DeviceRequest } from './deviceRequest'; -class TokenRequest { +export class TokenRequest { email: string; masterPasswordHash: string; token: string; @@ -44,6 +44,3 @@ class TokenRequest { return obj; } } - -export { TokenRequest }; -(window as any).TokenRequest = TokenRequest; diff --git a/src/models/request/twoFactorEmailRequest.ts b/src/models/request/twoFactorEmailRequest.ts index d540b08ecf..b506ccfa43 100644 --- a/src/models/request/twoFactorEmailRequest.ts +++ b/src/models/request/twoFactorEmailRequest.ts @@ -1,4 +1,4 @@ -class TwoFactorEmailRequest { +export class TwoFactorEmailRequest { email: string; masterPasswordHash: string; @@ -7,6 +7,3 @@ class TwoFactorEmailRequest { this.masterPasswordHash = masterPasswordHash; } } - -export { TwoFactorEmailRequest }; -(window as any).TwoFactorEmailRequest = TwoFactorEmailRequest; diff --git a/src/models/response/attachmentResponse.ts b/src/models/response/attachmentResponse.ts index df5138650c..1ad530bd6d 100644 --- a/src/models/response/attachmentResponse.ts +++ b/src/models/response/attachmentResponse.ts @@ -1,4 +1,4 @@ -class AttachmentResponse { +export class AttachmentResponse { id: string; url: string; fileName: string; @@ -13,6 +13,3 @@ class AttachmentResponse { 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 index b2e597e23a..241b8530a0 100644 --- a/src/models/response/cipherResponse.ts +++ b/src/models/response/cipherResponse.ts @@ -1,6 +1,6 @@ import { AttachmentResponse } from './attachmentResponse'; -class CipherResponse { +export class CipherResponse { id: string; organizationId: string; folderId: string; @@ -39,6 +39,3 @@ class CipherResponse { } } } - -export { CipherResponse }; -(window as any).CipherResponse = CipherResponse; diff --git a/src/models/response/collectionResponse.ts b/src/models/response/collectionResponse.ts index 8b0247720d..77b80f57ca 100644 --- a/src/models/response/collectionResponse.ts +++ b/src/models/response/collectionResponse.ts @@ -1,4 +1,4 @@ -class CollectionResponse { +export class CollectionResponse { id: string; organizationId: string; name: string; @@ -9,6 +9,3 @@ class CollectionResponse { 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 index 5e76635d16..23d0135b76 100644 --- a/src/models/response/deviceResponse.ts +++ b/src/models/response/deviceResponse.ts @@ -1,6 +1,6 @@ import { DeviceType } from '../../enums/deviceType'; -class DeviceResponse { +export class DeviceResponse { id: string; name: number; identifier: string; @@ -15,6 +15,3 @@ class DeviceResponse { 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 index d65077147b..11ecfe1fbc 100644 --- a/src/models/response/domainsResponse.ts +++ b/src/models/response/domainsResponse.ts @@ -1,6 +1,6 @@ import { GlobalDomainResponse } from './globalDomainResponse'; -class DomainsResponse { +export class DomainsResponse { equivalentDomains: string[][]; globalEquivalentDomains: GlobalDomainResponse[] = []; @@ -15,6 +15,3 @@ class DomainsResponse { } } } - -export { DomainsResponse }; -(window as any).DomainsResponse = DomainsResponse; diff --git a/src/models/response/errorResponse.ts b/src/models/response/errorResponse.ts index 4d976932cd..4297427711 100644 --- a/src/models/response/errorResponse.ts +++ b/src/models/response/errorResponse.ts @@ -1,4 +1,4 @@ -class ErrorResponse { +export class ErrorResponse { message: string; validationErrors: { [key: string]: string[]; }; statusCode: number; @@ -32,6 +32,3 @@ class ErrorResponse { return this.message; } } - -export { ErrorResponse }; -(window as any).ErrorResponse = ErrorResponse; diff --git a/src/models/response/folderResponse.ts b/src/models/response/folderResponse.ts index c5ff0ada70..1e3b4570cc 100644 --- a/src/models/response/folderResponse.ts +++ b/src/models/response/folderResponse.ts @@ -1,4 +1,4 @@ -class FolderResponse { +export class FolderResponse { id: string; name: string; revisionDate: string; @@ -9,6 +9,3 @@ class FolderResponse { 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 index 8e9a45df11..75f5e4ce61 100644 --- a/src/models/response/globalDomainResponse.ts +++ b/src/models/response/globalDomainResponse.ts @@ -1,4 +1,4 @@ -class GlobalDomainResponse { +export class GlobalDomainResponse { type: number; domains: string[]; excluded: number[]; @@ -9,6 +9,3 @@ class GlobalDomainResponse { 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 index 2d188707c0..2252c76c91 100644 --- a/src/models/response/identityTokenResponse.ts +++ b/src/models/response/identityTokenResponse.ts @@ -1,4 +1,4 @@ -class IdentityTokenResponse { +export class IdentityTokenResponse { accessToken: string; expiresIn: number; refreshToken: string; @@ -19,6 +19,3 @@ class IdentityTokenResponse { this.twoFactorToken = response.TwoFactorToken; } } - -export { IdentityTokenResponse }; -(window as any).IdentityTokenResponse = IdentityTokenResponse; diff --git a/src/models/response/keysResponse.ts b/src/models/response/keysResponse.ts index cb96dad51e..a357f97c44 100644 --- a/src/models/response/keysResponse.ts +++ b/src/models/response/keysResponse.ts @@ -1,4 +1,4 @@ -class KeysResponse { +export class KeysResponse { privateKey: string; publicKey: string; @@ -7,6 +7,3 @@ class KeysResponse { 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 index 9cf5455ada..4a9715c39c 100644 --- a/src/models/response/listResponse.ts +++ b/src/models/response/listResponse.ts @@ -1,10 +1,7 @@ -class ListResponse { +export 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 index 484857745a..05b2b980b5 100644 --- a/src/models/response/profileOrganizationResponse.ts +++ b/src/models/response/profileOrganizationResponse.ts @@ -1,4 +1,4 @@ -class ProfileOrganizationResponse { +export class ProfileOrganizationResponse { id: string; name: string; useGroups: boolean; @@ -25,6 +25,3 @@ class ProfileOrganizationResponse { 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 index 69e4f3e9cb..c9d2d3d221 100644 --- a/src/models/response/profileResponse.ts +++ b/src/models/response/profileResponse.ts @@ -1,6 +1,6 @@ import { ProfileOrganizationResponse } from './profileOrganizationResponse'; -class ProfileResponse { +export class ProfileResponse { id: string; name: string; email: string; @@ -34,6 +34,3 @@ class ProfileResponse { } } } - -export { ProfileResponse }; -(window as any).ProfileResponse = ProfileResponse; diff --git a/src/models/response/syncResponse.ts b/src/models/response/syncResponse.ts index 8acdc5202d..58c0280739 100644 --- a/src/models/response/syncResponse.ts +++ b/src/models/response/syncResponse.ts @@ -4,7 +4,7 @@ import { DomainsResponse } from './domainsResponse'; import { FolderResponse } from './folderResponse'; import { ProfileResponse } from './profileResponse'; -class SyncResponse { +export class SyncResponse { profile?: ProfileResponse; folders: FolderResponse[] = []; collections: CollectionResponse[] = []; @@ -39,6 +39,3 @@ class SyncResponse { } } } - -export { SyncResponse }; -(window as any).SyncResponse = SyncResponse; From 6c9e0c4cd3fad28578525903de11cb5804ec01f7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 23 Jan 2018 15:58:13 -0500 Subject: [PATCH 0059/1626] added auth service --- src/abstractions/auth.service.ts | 5 ++ src/abstractions/index.ts | 1 + src/enums/deviceType.ts | 3 ++ src/services/auth.service.ts | 78 ++++++++++++++++++++++++++++++++ src/services/index.ts | 1 + 5 files changed, 88 insertions(+) create mode 100644 src/abstractions/auth.service.ts create mode 100644 src/services/auth.service.ts diff --git a/src/abstractions/auth.service.ts b/src/abstractions/auth.service.ts new file mode 100644 index 0000000000..56c93b07cc --- /dev/null +++ b/src/abstractions/auth.service.ts @@ -0,0 +1,5 @@ +export abstract class AuthService { + logIn: (email: string, masterPassword: string, twoFactorProvider?: number, + twoFactorToken?: string, remember?: boolean) => any; + logOut: (callback: Function) => void; +} diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index d74fbfe8a9..69f1295104 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -1,5 +1,6 @@ export { ApiService } from './api.service'; export { AppIdService } from './appId.service'; +export { AuthService } from './auth.service'; export { CipherService } from './cipher.service'; export { CollectionService } from './collection.service'; export { CryptoService } from './crypto.service'; diff --git a/src/enums/deviceType.ts b/src/enums/deviceType.ts index 3c00cf9ae2..c41ff92933 100644 --- a/src/enums/deviceType.ts +++ b/src/enums/deviceType.ts @@ -3,6 +3,9 @@ export enum DeviceType { Firefox = 3, Opera = 4, Edge = 5, + Windows = 6, + MacOs = 7, + Linux = 8, Vivaldi = 19, Safari = 20, } diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts new file mode 100644 index 0000000000..4b123a5885 --- /dev/null +++ b/src/services/auth.service.ts @@ -0,0 +1,78 @@ +import { DeviceRequest } from '../models/request/deviceRequest'; +import { TokenRequest } from '../models/request/tokenRequest'; + +import { ConstantsService } from '../services/constants.service'; + +import { ApiService } from '../abstractions/api.service'; +import { AppIdService } from '../abstractions/appId.service'; +import { CryptoService } from '../abstractions/crypto.service'; +import { MessagingService } from '../abstractions/messaging.service'; +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; +import { TokenService } from '../abstractions/token.service'; +import { UserService } from '../abstractions/user.service'; + +export class AuthService { + constructor(public cryptoService: CryptoService, public apiService: ApiService, public userService: UserService, + public tokenService: TokenService, public $rootScope: any, public appIdService: AppIdService, + public platformUtilsService: PlatformUtilsService, public constantsService: ConstantsService, + public messagingService: MessagingService) { + } + + async logIn(email: string, masterPassword: string, twoFactorProvider?: number, + twoFactorToken?: string, remember?: boolean) { + email = email.toLowerCase(); + + const key = this.cryptoService.makeKey(masterPassword, email); + const appId = await this.appIdService.getAppId(); + const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email); + const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); + + const deviceRequest = new DeviceRequest(appId, this.platformUtilsService); + + let request: TokenRequest; + + if (twoFactorToken != null && twoFactorProvider != null) { + request = new TokenRequest(email, hashedPassword, twoFactorProvider, twoFactorToken, remember, + deviceRequest); + } else if (storedTwoFactorToken) { + request = new TokenRequest(email, hashedPassword, this.constantsService.twoFactorProvider.remember, + storedTwoFactorToken, false, deviceRequest); + } else { + request = new TokenRequest(email, hashedPassword, null, null, false, deviceRequest); + } + + const response = await this.apiService.postIdentityToken(request); + if (!response) { + return; + } + + if (!response.accessToken) { + // two factor required + return { + twoFactor: true, + twoFactorProviders: response, + }; + } + + if (response.twoFactorToken) { + this.tokenService.setTwoFactorToken(response.twoFactorToken, email); + } + + await this.tokenService.setTokens(response.accessToken, response.refreshToken); + await this.cryptoService.setKey(key); + await this.cryptoService.setKeyHash(hashedPassword); + await this.userService.setUserIdAndEmail(this.tokenService.getUserId(), this.tokenService.getEmail()); + await this.cryptoService.setEncKey(response.key); + await this.cryptoService.setEncPrivateKey(response.privateKey); + + this.messagingService.send('loggedIn'); + return { + twoFactor: false, + twoFactorProviders: null, + }; + } + + logOut(callback: Function) { + callback(); + } +} diff --git a/src/services/index.ts b/src/services/index.ts index 0e6ab04f75..f66088a65b 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,5 +1,6 @@ export { ApiService } from './api.service'; export { AppIdService } from './appId.service'; +export { AuthService } from './auth.service'; export { CipherService } from './cipher.service'; export { CollectionService } from './collection.service'; export { ConstantsService } from './constants.service'; From edc3bd1f81d480ca7bef9cdaf57deabd7152f8c4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 23 Jan 2018 17:29:15 -0500 Subject: [PATCH 0060/1626] convert services to abstractions --- src/abstractions/auth.service.ts | 4 +- src/abstractions/cipher.service.ts | 49 +++++++++++++------------ src/abstractions/collection.service.ts | 18 ++++----- src/abstractions/environment.service.ts | 6 +-- src/abstractions/folder.service.ts | 24 ++++++------ src/services/auth.service.ts | 2 +- src/services/cipher.service.ts | 4 +- src/services/collection.service.ts | 4 +- src/services/environment.service.ts | 4 +- src/services/folder.service.ts | 4 +- 10 files changed, 60 insertions(+), 59 deletions(-) diff --git a/src/abstractions/auth.service.ts b/src/abstractions/auth.service.ts index 56c93b07cc..45f7ca034d 100644 --- a/src/abstractions/auth.service.ts +++ b/src/abstractions/auth.service.ts @@ -1,5 +1,5 @@ export abstract class AuthService { - logIn: (email: string, masterPassword: string, twoFactorProvider?: number, - twoFactorToken?: string, remember?: boolean) => any; + logIn: (email: string, masterPassword: string, twoFactorProvider?: number, twoFactorToken?: string, + remember?: boolean) => Promise; logOut: (callback: Function) => void; } diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index 58f092b696..d639d99a01 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -4,29 +4,30 @@ import { Cipher } from '../models/domain/cipher'; import { Field } from '../models/domain/field'; import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; -export interface CipherService { +export abstract class CipherService { decryptedCipherCache: any[]; - clearCache(): void; - encrypt(model: any): Promise; - encryptFields(fieldsModel: any[], key: SymmetricCryptoKey): Promise; - encryptField(fieldModel: any, key: SymmetricCryptoKey): Promise; - get(id: string): Promise; - getAll(): Promise; - getAllDecrypted(): Promise; - getAllDecryptedForGrouping(groupingId: string, folder?: boolean): Promise; - getAllDecryptedForDomain(domain: string, includeOtherTypes?: any[]): Promise; - getLastUsedForDomain(domain: string): Promise; - updateLastUsedDate(id: string): Promise; - saveNeverDomain(domain: string): Promise; - saveWithServer(cipher: Cipher): Promise; - saveAttachmentWithServer(cipher: Cipher, unencryptedFile: any): Promise; - upsert(cipher: CipherData | CipherData[]): Promise; - replace(ciphers: { [id: string]: CipherData; }): Promise; - clear(userId: string): Promise; - delete(id: string | string[]): Promise; - deleteWithServer(id: string): Promise; - deleteAttachment(id: string, attachmentId: string): Promise; - deleteAttachmentWithServer(id: string, attachmentId: string): Promise; - sortCiphersByLastUsed(a: any, b: any): number; - sortCiphersByLastUsedThenName(a: any, b: any): number; + + clearCache: () => void; + encrypt: (model: any) => Promise; + encryptFields: (fieldsModel: any[], key: SymmetricCryptoKey) => Promise; + encryptField: (fieldModel: any, key: SymmetricCryptoKey) => Promise; + get: (id: string) => Promise; + getAll: () => Promise; + getAllDecrypted: () => Promise; + getAllDecryptedForGrouping: (groupingId: string, folder?: boolean) => Promise; + getAllDecryptedForDomain: (domain: string, includeOtherTypes?: any[]) => Promise; + getLastUsedForDomain: (domain: string) => Promise; + updateLastUsedDate: (id: string) => Promise; + saveNeverDomain: (domain: string) => Promise; + saveWithServer: (cipher: Cipher) => Promise; + saveAttachmentWithServer: (cipher: Cipher, unencryptedFile: any) => Promise; + upsert: (cipher: CipherData | CipherData[]) => Promise; + replace: (ciphers: { [id: string]: CipherData; }) => Promise; + clear: (userId: string) => Promise; + delete: (id: string | string[]) => Promise; + deleteWithServer: (id: string) => Promise; + deleteAttachment: (id: string, attachmentId: string) => Promise; + deleteAttachmentWithServer: (id: string, attachmentId: string) => Promise; + sortCiphersByLastUsed: (a: any, b: any) => number; + sortCiphersByLastUsedThenName: (a: any, b: any) => number; } diff --git a/src/abstractions/collection.service.ts b/src/abstractions/collection.service.ts index e33532d3ce..0f7f25e7ab 100644 --- a/src/abstractions/collection.service.ts +++ b/src/abstractions/collection.service.ts @@ -2,15 +2,15 @@ import { CollectionData } from '../models/data/collectionData'; import { Collection } from '../models/domain/collection'; -export interface CollectionService { +export abstract class CollectionService { decryptedCollectionCache: any[]; - clearCache(): void; - get(id: string): Promise; - getAll(): Promise; - getAllDecrypted(): Promise; - upsert(collection: CollectionData | CollectionData[]): Promise; - replace(collections: { [id: string]: CollectionData; }): Promise; - clear(userId: string): Promise; - delete(id: string | string[]): Promise; + clearCache: () => void; + get: (id: string) => Promise; + getAll: () => Promise; + getAllDecrypted: () => Promise; + upsert: (collection: CollectionData | CollectionData[]) => Promise; + replace: (collections: { [id: string]: CollectionData; }) => Promise; + clear: (userId: string) => Promise; + delete: (id: string | string[]) => Promise; } diff --git a/src/abstractions/environment.service.ts b/src/abstractions/environment.service.ts index a021db8910..67a8857fde 100644 --- a/src/abstractions/environment.service.ts +++ b/src/abstractions/environment.service.ts @@ -1,10 +1,10 @@ -export interface EnvironmentService { +export abstract class EnvironmentService { baseUrl: string; webVaultUrl: string; apiUrl: string; identityUrl: string; iconsUrl: string; - setUrlsFromStorage(): Promise; - setUrls(urls: any): Promise; + setUrlsFromStorage:() => Promise; + setUrls: (urls: any) => Promise; } diff --git a/src/abstractions/folder.service.ts b/src/abstractions/folder.service.ts index 7de2430534..7b75f6aebc 100644 --- a/src/abstractions/folder.service.ts +++ b/src/abstractions/folder.service.ts @@ -2,18 +2,18 @@ import { FolderData } from '../models/data/folderData'; import { Folder } from '../models/domain/folder'; -export interface FolderService { +export abstract class FolderService { decryptedFolderCache: any[]; - clearCache(): void; - encrypt(model: any): Promise; - get(id: string): Promise; - getAll(): Promise; - getAllDecrypted(): Promise; - saveWithServer(folder: Folder): Promise; - upsert(folder: FolderData | FolderData[]): Promise; - replace(folders: { [id: string]: FolderData; }): Promise; - clear(userId: string): Promise; - delete(id: string | string[]): Promise; - deleteWithServer(id: string): Promise; + clearCache: () => void; + encrypt: (model: any) => Promise; + get: (id: string) => Promise; + getAll: () => Promise; + getAllDecrypted: () => Promise; + saveWithServer: (folder: Folder) => Promise; + upsert: (folder: FolderData | FolderData[]) => Promise; + replace: (folders: { [id: string]: FolderData; }) => Promise; + clear: (userId: string) => Promise; + delete: (id: string | string[]) => Promise; + deleteWithServer: (id: string) => Promise; } diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 4b123a5885..2d2328c6a3 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -13,7 +13,7 @@ import { UserService } from '../abstractions/user.service'; export class AuthService { constructor(public cryptoService: CryptoService, public apiService: ApiService, public userService: UserService, - public tokenService: TokenService, public $rootScope: any, public appIdService: AppIdService, + public tokenService: TokenService, public appIdService: AppIdService, public platformUtilsService: PlatformUtilsService, public constantsService: ConstantsService, public messagingService: MessagingService) { } diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 364be57cac..9f6e8fba3e 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -15,7 +15,7 @@ import { ErrorResponse } from '../models/response/errorResponse'; import { ConstantsService } from './constants.service'; import { ApiService } from '../abstractions/api.service'; -import { CipherService as CipherServiceInterface } from '../abstractions/cipher.service'; +import { CipherService as CipherServiceAbstraction } from '../abstractions/cipher.service'; import { CryptoService } from '../abstractions/crypto.service'; import { SettingsService } from '../abstractions/settings.service'; import { StorageService } from '../abstractions/storage.service'; @@ -27,7 +27,7 @@ const Keys = { neverDomains: 'neverDomains', }; -export class CipherService implements CipherServiceInterface { +export class CipherService implements CipherServiceAbstraction { 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; diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index 35327f1e2a..f85dfcf8b1 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -2,7 +2,7 @@ import { CollectionData } from '../models/data/collectionData'; import { Collection } from '../models/domain/collection'; -import { CollectionService as CollectionServiceInterface } from '../abstractions/collection.service'; +import { CollectionService as CollectionServiceAbstraction } from '../abstractions/collection.service'; import { CryptoService } from '../abstractions/crypto.service'; import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; @@ -11,7 +11,7 @@ const Keys = { collectionsPrefix: 'collections_', }; -export class CollectionService implements CollectionServiceInterface { +export class CollectionService implements CollectionServiceAbstraction { decryptedCollectionCache: any[]; constructor(private cryptoService: CryptoService, private userService: UserService, diff --git a/src/services/environment.service.ts b/src/services/environment.service.ts index 2ba63ad6bf..e1544489f1 100644 --- a/src/services/environment.service.ts +++ b/src/services/environment.service.ts @@ -3,10 +3,10 @@ import { EnvironmentUrls } from '../models/domain/environmentUrls'; import { ConstantsService } from './constants.service'; import { ApiService } from '../abstractions/api.service'; -import { EnvironmentService as EnvironmentServiceInterface } from '../abstractions/environment.service'; +import { EnvironmentService as EnvironmentServiceAbstraction } from '../abstractions/environment.service'; import { StorageService } from '../abstractions/storage.service'; -export class EnvironmentService implements EnvironmentServiceInterface { +export class EnvironmentService implements EnvironmentServiceAbstraction { baseUrl: string; webVaultUrl: string; apiUrl: string; diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index aa027fdd6a..0f8096849a 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -8,7 +8,7 @@ import { FolderResponse } from '../models/response/folderResponse'; import { ApiService } from '../abstractions/api.service'; import { CryptoService } from '../abstractions/crypto.service'; -import { FolderService as FolderServiceInterface } from '../abstractions/folder.service'; +import { FolderService as FolderServiceAbstraction } from '../abstractions/folder.service'; import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; @@ -16,7 +16,7 @@ const Keys = { foldersPrefix: 'folders_', }; -export class FolderService implements FolderServiceInterface { +export class FolderService implements FolderServiceAbstraction { decryptedFolderCache: any[]; constructor(private cryptoService: CryptoService, private userService: UserService, From 5557c5b638f75cdc04523a15e67767547a2fc31d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 24 Jan 2018 09:27:08 -0500 Subject: [PATCH 0061/1626] foramtting fixes --- src/abstractions/environment.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abstractions/environment.service.ts b/src/abstractions/environment.service.ts index 67a8857fde..1f6bc390bc 100644 --- a/src/abstractions/environment.service.ts +++ b/src/abstractions/environment.service.ts @@ -5,6 +5,6 @@ export abstract class EnvironmentService { identityUrl: string; iconsUrl: string; - setUrlsFromStorage:() => Promise; + setUrlsFromStorage: () => Promise; setUrls: (urls: any) => Promise; } From 7e1c883f0318c2f64da0f18682814abe789adccb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 24 Jan 2018 11:33:15 -0500 Subject: [PATCH 0062/1626] view model types --- src/index.ts | 3 +- src/models/domain/attachment.ts | 13 ++--- src/models/domain/card.ts | 6 ++- src/models/domain/cipher.ts | 52 ++------------------ src/models/domain/domain.ts | 17 ++++--- src/models/domain/field.ts | 10 ++-- src/models/domain/identity.ts | 6 ++- src/models/domain/login.ts | 6 ++- src/models/domain/secureNote.ts | 8 +-- src/models/view/attachmentView.ts | 18 +++++++ src/models/view/cardView.ts | 16 ++++++ src/models/view/cipherView.ts | 80 ++++++++++++++++++++++++++++++ src/models/view/fieldView.ts | 15 ++++++ src/models/view/identityView.ts | 28 +++++++++++ src/models/view/index.ts | 8 +++ src/models/view/loginView.ts | 34 +++++++++++++ src/models/view/secureNoteView.ts | 13 +++++ src/models/view/view.ts | 2 + src/services/cipher.service.ts | 82 +++++++++++++++++-------------- 19 files changed, 299 insertions(+), 118 deletions(-) create mode 100644 src/models/view/attachmentView.ts create mode 100644 src/models/view/cardView.ts create mode 100644 src/models/view/cipherView.ts create mode 100644 src/models/view/fieldView.ts create mode 100644 src/models/view/identityView.ts create mode 100644 src/models/view/index.ts create mode 100644 src/models/view/loginView.ts create mode 100644 src/models/view/secureNoteView.ts create mode 100644 src/models/view/view.ts diff --git a/src/index.ts b/src/index.ts index 2edf7e05e4..da1f6af9e0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,5 +5,6 @@ import * as Domain from './models/domain'; import * as Request from './models/request'; import * as Response from './models/response'; import * as Services from './services'; +import * as View from './models/view'; -export { Abstractions, Enums, Data, Domain, Request, Response, Services }; +export { Abstractions, Enums, Data, Domain, Request, Response, Services, View }; diff --git a/src/models/domain/attachment.ts b/src/models/domain/attachment.ts index 28859be366..7861066e0e 100644 --- a/src/models/domain/attachment.ts +++ b/src/models/domain/attachment.ts @@ -3,6 +3,8 @@ import { AttachmentData } from '../data/attachmentData'; import { CipherString } from './cipherString'; import Domain from './domain'; +import { AttachmentView } from '../view/attachmentView'; + export class Attachment extends Domain { id: string; url: string; @@ -25,15 +27,8 @@ export class Attachment extends Domain { }, 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, { + decrypt(orgId: string): Promise { + return this.decryptObj(new AttachmentView(this), { fileName: null, }, orgId); } diff --git a/src/models/domain/card.ts b/src/models/domain/card.ts index ee6fbd0616..19a2ad9e50 100644 --- a/src/models/domain/card.ts +++ b/src/models/domain/card.ts @@ -3,6 +3,8 @@ import { CardData } from '../data/cardData'; import { CipherString } from './cipherString'; import Domain from './domain'; +import { CardView } from '../view/cardView'; + export class Card extends Domain { cardholderName: CipherString; brand: CipherString; @@ -27,8 +29,8 @@ export class Card extends Domain { }, alreadyEncrypted, []); } - decrypt(orgId: string): Promise { - return this.decryptObj({}, { + decrypt(orgId: string): Promise { + return this.decryptObj(new CardView(this), { cardholderName: null, brand: null, number: null, diff --git a/src/models/domain/cipher.ts b/src/models/domain/cipher.ts index 9dfbafdac0..7f9c18d1f0 100644 --- a/src/models/domain/cipher.ts +++ b/src/models/domain/cipher.ts @@ -1,9 +1,9 @@ import { CipherType } from '../../enums/cipherType'; -import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; - import { CipherData } from '../data/cipherData'; +import { CipherView } from '../view/cipherView'; + import { Attachment } from './attachment'; import { Card } from './card'; import { CipherString } from './cipherString'; @@ -89,23 +89,8 @@ export class Cipher extends Domain { } } - 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, - }; + async decrypt(): Promise { + const model = new CipherView(this); await this.decryptObj(model, { name: null, @@ -115,44 +100,15 @@ export class Cipher extends Domain { 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; diff --git a/src/models/domain/domain.ts b/src/models/domain/domain.ts index cdb220776b..7fe644740f 100644 --- a/src/models/domain/domain.ts +++ b/src/models/domain/domain.ts @@ -1,22 +1,25 @@ import { CipherString } from '../domain/cipherString'; +import { View } from '../view/view'; + export default abstract class Domain { - protected buildDomainModel(model: any, obj: any, map: any, alreadyEncrypted: boolean, notEncList: any[] = []) { + protected buildDomainModel(domain: D, dataObj: any, map: any, + alreadyEncrypted: boolean, notEncList: any[] = []) { for (const prop in map) { if (!map.hasOwnProperty(prop)) { continue; } - const objProp = obj[(map[prop] || prop)]; + const objProp = dataObj[(map[prop] || prop)]; if (alreadyEncrypted === true || notEncList.indexOf(prop) > -1) { - model[prop] = objProp ? objProp : null; + (domain as any)[prop] = objProp ? objProp : null; } else { - model[prop] = objProp ? new CipherString(objProp) : null; + (domain as any)[prop] = objProp ? new CipherString(objProp) : null; } } } - protected async decryptObj(model: any, map: any, orgId: string) { + protected async decryptObj(viewModel: T, map: any, orgId: string): Promise { const promises = []; const self: any = this; @@ -34,13 +37,13 @@ export default abstract class Domain { } return null; }).then((val: any) => { - model[theProp] = val; + (viewModel as any)[theProp] = val; }); promises.push(p); })(prop); } await Promise.all(promises); - return model; + return viewModel; } } diff --git a/src/models/domain/field.ts b/src/models/domain/field.ts index d118ad2463..023fefe5a9 100644 --- a/src/models/domain/field.ts +++ b/src/models/domain/field.ts @@ -5,6 +5,8 @@ import { FieldData } from '../data/fieldData'; import { CipherString } from './cipherString'; import Domain from './domain'; +import { FieldView } from '../view/fieldView'; + export class Field extends Domain { name: CipherString; vault: CipherString; @@ -23,12 +25,8 @@ export class Field extends Domain { }, alreadyEncrypted, []); } - decrypt(orgId: string): Promise { - const model = { - type: this.type, - }; - - return this.decryptObj(model, { + decrypt(orgId: string): Promise { + return this.decryptObj(new FieldView(this), { name: null, value: null, }, orgId); diff --git a/src/models/domain/identity.ts b/src/models/domain/identity.ts index 2b8323e113..ade03260ea 100644 --- a/src/models/domain/identity.ts +++ b/src/models/domain/identity.ts @@ -3,6 +3,8 @@ import { IdentityData } from '../data/identityData'; import { CipherString } from './cipherString'; import Domain from './domain'; +import { IdentityView } from '../view/identityView'; + export class Identity extends Domain { title: CipherString; firstName: CipherString; @@ -51,8 +53,8 @@ export class Identity extends Domain { }, alreadyEncrypted, []); } - decrypt(orgId: string): Promise { - return this.decryptObj({}, { + decrypt(orgId: string): Promise { + return this.decryptObj(new IdentityView(this), { title: null, firstName: null, middleName: null, diff --git a/src/models/domain/login.ts b/src/models/domain/login.ts index 5d0f50c378..1c98dc7031 100644 --- a/src/models/domain/login.ts +++ b/src/models/domain/login.ts @@ -1,5 +1,7 @@ import { LoginData } from '../data/loginData'; +import { LoginView } from '../view/loginView'; + import { CipherString } from './cipherString'; import Domain from './domain'; @@ -23,8 +25,8 @@ export class Login extends Domain { }, alreadyEncrypted, []); } - decrypt(orgId: string): Promise { - return this.decryptObj({}, { + decrypt(orgId: string): Promise { + return this.decryptObj(new LoginView(this), { uri: null, username: null, password: null, diff --git a/src/models/domain/secureNote.ts b/src/models/domain/secureNote.ts index 8feb7de3ad..79724a4193 100644 --- a/src/models/domain/secureNote.ts +++ b/src/models/domain/secureNote.ts @@ -4,6 +4,8 @@ import { SecureNoteData } from '../data/secureNoteData'; import Domain from './domain'; +import { SecureNoteView } from '../view/secureNoteView'; + export class SecureNote extends Domain { type: SecureNoteType; @@ -16,9 +18,7 @@ export class SecureNote extends Domain { this.type = obj.type; } - decrypt(orgId: string): any { - return { - type: this.type, - }; + decrypt(orgId: string): Promise { + return Promise.resolve(new SecureNoteView(this)); } } diff --git a/src/models/view/attachmentView.ts b/src/models/view/attachmentView.ts new file mode 100644 index 0000000000..49ea57326d --- /dev/null +++ b/src/models/view/attachmentView.ts @@ -0,0 +1,18 @@ +import { View } from './view'; + +import { Attachment } from '../domain/attachment'; + +export class AttachmentView implements View { + id: string; + url: string; + size: number; + sizeName: string; + fileName: string; + + constructor(a: Attachment) { + this.id = a.id; + this.url = a.url; + this.size = a.size; + this.sizeName = a.sizeName; + } +} diff --git a/src/models/view/cardView.ts b/src/models/view/cardView.ts new file mode 100644 index 0000000000..cb4b2380fe --- /dev/null +++ b/src/models/view/cardView.ts @@ -0,0 +1,16 @@ +import { View } from './view'; + +import { Card } from '../domain/card'; + +export class CardView implements View { + cardholderName: string; + brand: string; + number: string; + expMonth: string; + expYear: string; + code: string; + + constructor(c?: Card) { + // ctor + } +} diff --git a/src/models/view/cipherView.ts b/src/models/view/cipherView.ts new file mode 100644 index 0000000000..d111226913 --- /dev/null +++ b/src/models/view/cipherView.ts @@ -0,0 +1,80 @@ +import { CipherType } from '../../enums/cipherType'; + +import { Cipher } from '../domain/cipher'; + +import { AttachmentView } from './attachmentView'; +import { CardView } from './cardView'; +import { FieldView } from './fieldView'; +import { IdentityView } from './identityView'; +import { LoginView } from './loginView'; +import { SecureNoteView } from './secureNoteView'; +import { View } from './view'; + +export class CipherView implements View { + id: string; + organizationId: string; + folderId: string; + name: string; + notes: string; + type: CipherType; + favorite: boolean; + localData: any; + login: LoginView; + identity: IdentityView; + card: CardView; + secureNote: SecureNoteView; + attachments: AttachmentView[]; + fields: FieldView[]; + collectionIds: string[]; + + // tslint:disable-next-line + private _subTitle: string; + + constructor(c: Cipher) { + this.id = c.id; + this.organizationId = c.organizationId; + this.folderId = c.folderId; + this.favorite = c.favorite; + this.type = c.type; + this.localData = c.localData; + this.collectionIds = c.collectionIds; + } + + get subTitle(): string { + if (this._subTitle == null) { + switch (this.type) { + case CipherType.Login: + this._subTitle = this.login.username; + break; + case CipherType.SecureNote: + this._subTitle = null; + break; + case CipherType.Card: + this._subTitle = this.card.brand; + if (this.card.number != null && this.card.number.length >= 4) { + if (this._subTitle !== '') { + this._subTitle += ', '; + } + this._subTitle += ('*' + this.card.number.substr(this.card.number.length - 4)); + } + break; + case CipherType.Identity: + this._subTitle = ''; + if (this.identity.firstName != null) { + this._subTitle = this.identity.firstName; + } + if (this.identity.lastName != null) { + if (this._subTitle !== '') { + this._subTitle += ' '; + } + this._subTitle += this.identity.lastName; + } + break; + default: + break; + } + } + + return this._subTitle; + } +} diff --git a/src/models/view/fieldView.ts b/src/models/view/fieldView.ts new file mode 100644 index 0000000000..f25f86cc6e --- /dev/null +++ b/src/models/view/fieldView.ts @@ -0,0 +1,15 @@ +import { FieldType } from '../../enums/fieldType'; + +import { View } from './view'; + +import { Field } from '../domain/field'; + +export class FieldView implements View { + name: string; + vault: string; + type: FieldType; + + constructor(f: Field) { + this.type = f.type; + } +} diff --git a/src/models/view/identityView.ts b/src/models/view/identityView.ts new file mode 100644 index 0000000000..fb644c84a4 --- /dev/null +++ b/src/models/view/identityView.ts @@ -0,0 +1,28 @@ +import { View } from './view'; + +import { Identity } from '../domain/identity'; + +export class IdentityView implements View { + 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(i?: Identity) { + // ctor + } +} diff --git a/src/models/view/index.ts b/src/models/view/index.ts new file mode 100644 index 0000000000..0e07c256be --- /dev/null +++ b/src/models/view/index.ts @@ -0,0 +1,8 @@ +export { AttachmentView } from './attachmentView'; +export { CardView } from './cardView'; +export { CipherView } from './cipherView'; +export { FieldView } from './fieldView'; +export { IdentityView } from './identityView'; +export { LoginView } from './loginView'; +export { SecureNoteView } from './secureNoteView'; +export { View } from './view'; diff --git a/src/models/view/loginView.ts b/src/models/view/loginView.ts new file mode 100644 index 0000000000..c2e5270f05 --- /dev/null +++ b/src/models/view/loginView.ts @@ -0,0 +1,34 @@ +import { View } from './view'; + +import { Login } from '../domain/login'; + +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +export class LoginView implements View { + uri: string; + username: string; + password: string; + maskedPassword: string; + totp: string; + + // tslint:disable-next-line + private _domain: string; + + constructor(l?: Login) { + // ctor + } + + get domain(): string { + if (this._domain == null && this.uri != null) { + const containerService = (window as any).bitwardenContainerService; + if (containerService) { + const platformUtilsService: PlatformUtilsService = containerService.getPlatformUtilsService(); + this._domain = platformUtilsService.getDomain(this.uri); + } else { + throw new Error('window.bitwardenContainerService not initialized.'); + } + } + + return this._domain; + } +} diff --git a/src/models/view/secureNoteView.ts b/src/models/view/secureNoteView.ts new file mode 100644 index 0000000000..b4eae6d446 --- /dev/null +++ b/src/models/view/secureNoteView.ts @@ -0,0 +1,13 @@ +import { SecureNoteType } from '../../enums/secureNoteType'; + +import { View } from './view'; + +import { SecureNote } from '../domain/secureNote'; + +export class SecureNoteView implements View { + type: SecureNoteType; + + constructor(n: SecureNote) { + this.type = n.type; + } +} diff --git a/src/models/view/view.ts b/src/models/view/view.ts new file mode 100644 index 0000000000..c295888ef1 --- /dev/null +++ b/src/models/view/view.ts @@ -0,0 +1,2 @@ +export class View { +} diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 9f6e8fba3e..bec5c30dd6 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -4,6 +4,7 @@ import { CipherData } from '../models/data/cipherData'; import { Cipher } from '../models/domain/cipher'; import { CipherString } from '../models/domain/cipherString'; +import Domain from '../models/domain/domain'; import { Field } from '../models/domain/field'; import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; @@ -12,6 +13,13 @@ import { CipherRequest } from '../models/request/cipherRequest'; import { CipherResponse } from '../models/response/cipherResponse'; import { ErrorResponse } from '../models/response/errorResponse'; +import { CardView } from '../models/view/cardView'; +import { CipherView } from '../models/view/cipherView'; +import { FieldView } from '../models/view/fieldView'; +import { IdentityView } from '../models/view/identityView'; +import { LoginView } from '../models/view/loginView'; +import { View } from '../models/view/view'; + import { ConstantsService } from './constants.service'; import { ApiService } from '../abstractions/api.service'; @@ -68,7 +76,7 @@ export class CipherService implements CipherServiceAbstraction { return 0; } - decryptedCipherCache: any[]; + decryptedCipherCache: CipherView[]; constructor(private cryptoService: CryptoService, private userService: UserService, private settingsService: SettingsService, private apiService: ApiService, @@ -79,7 +87,7 @@ export class CipherService implements CipherServiceAbstraction { this.decryptedCipherCache = null; } - async encrypt(model: any): Promise { + async encrypt(model: CipherView): Promise { const cipher = new Cipher(); cipher.id = model.id; cipher.folderId = model.folderId; @@ -94,7 +102,7 @@ export class CipherService implements CipherServiceAbstraction { name: null, notes: null, }, key), - this.encryptCipherData(model, cipher, key), + this.encryptCipherData(cipher, model, key), this.encryptFields(model.fields, key).then((fields) => { cipher.fields = fields; }), @@ -103,7 +111,7 @@ export class CipherService implements CipherServiceAbstraction { return cipher; } - async encryptFields(fieldsModel: any[], key: SymmetricCryptoKey): Promise { + async encryptFields(fieldsModel: FieldView[], key: SymmetricCryptoKey): Promise { if (!fieldsModel || !fieldsModel.length) { return null; } @@ -121,7 +129,7 @@ export class CipherService implements CipherServiceAbstraction { return encFields; } - async encryptField(fieldModel: any, key: SymmetricCryptoKey): Promise { + async encryptField(fieldModel: FieldView, key: SymmetricCryptoKey): Promise { const field = new Field(); field.type = fieldModel.type; @@ -159,12 +167,12 @@ export class CipherService implements CipherServiceAbstraction { return response; } - async getAllDecrypted(): Promise { + async getAllDecrypted(): Promise { if (this.decryptedCipherCache != null) { return this.decryptedCipherCache; } - const decCiphers: any[] = []; + const decCiphers: CipherView[] = []; const key = await this.cryptoService.getKey(); if (key == null) { throw new Error('No key.'); @@ -173,9 +181,7 @@ export class CipherService implements CipherServiceAbstraction { const promises: any[] = []; const ciphers = await this.getAll(); ciphers.forEach((cipher) => { - promises.push(cipher.decrypt().then((c: any) => { - decCiphers.push(c); - })); + promises.push(cipher.decrypt().then((c) => decCiphers.push(c))); }); await Promise.all(promises); @@ -183,22 +189,21 @@ export class CipherService implements CipherServiceAbstraction { return this.decryptedCipherCache; } - async getAllDecryptedForGrouping(groupingId: string, folder: boolean = true): Promise { + async getAllDecryptedForGrouping(groupingId: string, folder: boolean = true): Promise { const ciphers = await this.getAllDecrypted(); - const ciphersToReturn: any[] = []; - ciphers.forEach((cipher) => { + return ciphers.filter((cipher) => { if (folder && cipher.folderId === groupingId) { - ciphersToReturn.push(cipher); + return true; } else if (!folder && cipher.collectionIds != null && cipher.collectionIds.indexOf(groupingId) > -1) { - ciphersToReturn.push(cipher); + return true; } - }); - return ciphersToReturn; + return false; + }); } - async getAllDecryptedForDomain(domain: string, includeOtherTypes?: any[]): Promise { + async getAllDecryptedForDomain(domain: string, includeOtherTypes?: any[]): Promise { if (domain == null && !includeOtherTypes) { return Promise.resolve([]); } @@ -222,21 +227,20 @@ export class CipherService implements CipherServiceAbstraction { const result = await Promise.all([eqDomainsPromise, this.getAllDecrypted()]); const matchingDomains = result[0]; const ciphers = result[1]; - const ciphersToReturn: any[] = []; - ciphers.forEach((cipher) => { + return ciphers.filter((cipher) => { if (domain && cipher.type === CipherType.Login && cipher.login.domain && matchingDomains.indexOf(cipher.login.domain) > -1) { - ciphersToReturn.push(cipher); + return true; } else if (includeOtherTypes && includeOtherTypes.indexOf(cipher.type) > -1) { - ciphersToReturn.push(cipher); + return true; } - }); - return ciphersToReturn; + return false; + }); } - async getLastUsedForDomain(domain: string): Promise { + async getLastUsedForDomain(domain: string): Promise { const ciphers = await this.getAllDecryptedForDomain(domain); if (ciphers.length === 0) { return null; @@ -437,7 +441,8 @@ export class CipherService implements CipherServiceAbstraction { // Helpers - private encryptObjProperty(model: any, obj: any, map: any, key: SymmetricCryptoKey): Promise { + private async encryptObjProperty(model: V, obj: D, + map: any, key: SymmetricCryptoKey): Promise { const promises = []; const self = this; @@ -449,39 +454,40 @@ export class CipherService implements CipherServiceAbstraction { // tslint:disable-next-line (function (theProp, theObj) { const p = Promise.resolve().then(() => { - const modelProp = model[(map[theProp] || theProp)]; + const modelProp = (model as any)[(map[theProp] || theProp)]; if (modelProp && modelProp !== '') { return self.cryptoService.encrypt(modelProp, key); } return null; }).then((val: CipherString) => { - theObj[theProp] = val; + (theObj as any)[theProp] = val; }); promises.push(p); })(prop, obj); } - return Promise.all(promises); + await Promise.all(promises); } - private encryptCipherData(cipher: Cipher, model: any, key: SymmetricCryptoKey): Promise { + private async encryptCipherData(cipher: Cipher, model: CipherView, key: SymmetricCryptoKey) { switch (cipher.type) { case CipherType.Login: - model.login = {}; - return this.encryptObjProperty(cipher.login, model.login, { + model.login = new LoginView(); + await this.encryptObjProperty(model.login, cipher.login, { uri: null, username: null, password: null, totp: null, }, key); + return; case CipherType.SecureNote: model.secureNote = { type: cipher.secureNote.type, }; - return Promise.resolve(); + return; case CipherType.Card: - model.card = {}; - return this.encryptObjProperty(cipher.card, model.card, { + model.card = new CardView(); + await this.encryptObjProperty(model.card, cipher.card, { cardholderName: null, brand: null, number: null, @@ -489,9 +495,10 @@ export class CipherService implements CipherServiceAbstraction { expYear: null, code: null, }, key); + return; case CipherType.Identity: - model.identity = {}; - return this.encryptObjProperty(cipher.identity, model.identity, { + model.identity = new IdentityView(); + await this.encryptObjProperty(model.identity, cipher.identity, { title: null, firstName: null, middleName: null, @@ -511,6 +518,7 @@ export class CipherService implements CipherServiceAbstraction { passportNumber: null, licenseNumber: null, }, key); + return; default: throw new Error('Unknown cipher type.'); } From bb187293067218df9dcbfa836fce9b7ccdd749e8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 24 Jan 2018 12:19:49 -0500 Subject: [PATCH 0063/1626] update abstraction --- src/abstractions/cipher.service.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index d639d99a01..330ac71c6c 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -4,19 +4,22 @@ import { Cipher } from '../models/domain/cipher'; import { Field } from '../models/domain/field'; import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; +import { CipherView } from '../models/view/cipherView'; +import { FieldView } from '../models/view/fieldView'; + export abstract class CipherService { - decryptedCipherCache: any[]; + decryptedCipherCache: CipherView[]; clearCache: () => void; - encrypt: (model: any) => Promise; - encryptFields: (fieldsModel: any[], key: SymmetricCryptoKey) => Promise; - encryptField: (fieldModel: any, key: SymmetricCryptoKey) => Promise; + encrypt: (model: CipherView) => Promise; + encryptFields: (fieldsModel: FieldView[], key: SymmetricCryptoKey) => Promise; + encryptField: (fieldModel: FieldView, key: SymmetricCryptoKey) => Promise; get: (id: string) => Promise; getAll: () => Promise; - getAllDecrypted: () => Promise; - getAllDecryptedForGrouping: (groupingId: string, folder?: boolean) => Promise; - getAllDecryptedForDomain: (domain: string, includeOtherTypes?: any[]) => Promise; - getLastUsedForDomain: (domain: string) => Promise; + getAllDecrypted: () => Promise; + getAllDecryptedForGrouping: (groupingId: string, folder?: boolean) => Promise; + getAllDecryptedForDomain: (domain: string, includeOtherTypes?: any[]) => Promise; + getLastUsedForDomain: (domain: string) => Promise; updateLastUsedDate: (id: string) => Promise; saveNeverDomain: (domain: string) => Promise; saveWithServer: (cipher: Cipher) => Promise; From 9fe0cfb2d8b801e2ca637a0df1a28aeaa40abb59 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 24 Jan 2018 13:27:32 -0500 Subject: [PATCH 0064/1626] fix encryptCipherData --- src/models/view/cipherView.ts | 6 +++++- src/services/cipher.service.ts | 15 +++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/models/view/cipherView.ts b/src/models/view/cipherView.ts index d111226913..50218527f8 100644 --- a/src/models/view/cipherView.ts +++ b/src/models/view/cipherView.ts @@ -30,7 +30,11 @@ export class CipherView implements View { // tslint:disable-next-line private _subTitle: string; - constructor(c: Cipher) { + constructor(c?: Cipher) { + if (!c) { + return; + } + this.id = c.id; this.organizationId = c.organizationId; this.folderId = c.folderId; diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index bec5c30dd6..784c3d8841 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -2,10 +2,14 @@ import { CipherType } from '../enums/cipherType'; import { CipherData } from '../models/data/cipherData'; +import { Card } from '../models/domain/card'; import { Cipher } from '../models/domain/cipher'; import { CipherString } from '../models/domain/cipherString'; import Domain from '../models/domain/domain'; import { Field } from '../models/domain/field'; +import { Identity } from '../models/domain/identity'; +import { Login } from '../models/domain/login'; +import { SecureNote } from '../models/domain/secureNote'; import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; import { CipherRequest } from '../models/request/cipherRequest'; @@ -472,7 +476,7 @@ export class CipherService implements CipherServiceAbstraction { private async encryptCipherData(cipher: Cipher, model: CipherView, key: SymmetricCryptoKey) { switch (cipher.type) { case CipherType.Login: - model.login = new LoginView(); + cipher.login = new Login(); await this.encryptObjProperty(model.login, cipher.login, { uri: null, username: null, @@ -481,12 +485,11 @@ export class CipherService implements CipherServiceAbstraction { }, key); return; case CipherType.SecureNote: - model.secureNote = { - type: cipher.secureNote.type, - }; + cipher.secureNote = new SecureNote(); + cipher.secureNote.type = model.secureNote.type; return; case CipherType.Card: - model.card = new CardView(); + cipher.card = new Card(); await this.encryptObjProperty(model.card, cipher.card, { cardholderName: null, brand: null, @@ -497,7 +500,7 @@ export class CipherService implements CipherServiceAbstraction { }, key); return; case CipherType.Identity: - model.identity = new IdentityView(); + cipher.identity = new Identity(); await this.encryptObjProperty(model.identity, cipher.identity, { title: null, firstName: null, From b4257b9ff3158c12e2e303a3187dea38b19b808d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 24 Jan 2018 16:58:34 -0500 Subject: [PATCH 0065/1626] subtitle properties --- src/models/view/cardView.ts | 39 +++++++++++++++++++++++-- src/models/view/cipherView.ts | 48 ++++++++----------------------- src/models/view/identityView.ts | 41 ++++++++++++++++++++++++-- src/models/view/loginView.ts | 41 +++++++++++++++++++++++--- src/models/view/secureNoteView.ts | 4 +++ 5 files changed, 129 insertions(+), 44 deletions(-) diff --git a/src/models/view/cardView.ts b/src/models/view/cardView.ts index cb4b2380fe..2e052fff55 100644 --- a/src/models/view/cardView.ts +++ b/src/models/view/cardView.ts @@ -4,13 +4,48 @@ import { Card } from '../domain/card'; export class CardView implements View { cardholderName: string; - brand: string; - number: string; expMonth: string; expYear: string; code: string; + // tslint:disable + private _brand: string; + private _number: string; + private _subTitle: string; + // tslint:enable + constructor(c?: Card) { // ctor } + + get brand(): string { + return this._brand; + } + set brand(value: string) { + this._brand = value; + this._subTitle = null; + } + + get number(): string { + return this._number; + } + set number(value: string) { + this._number = value; + this._subTitle = null; + } + + get subTitle(): string { + if (this._subTitle == null) { + this._subTitle = this.brand; + if (this.number != null && this.number.length >= 4) { + if (this._subTitle != null && this._subTitle !== '') { + this._subTitle += ', '; + } else { + this._subTitle = ''; + } + this._subTitle += ('*' + this.number.substr(this.number.length - 4)); + } + } + return this._subTitle; + } } diff --git a/src/models/view/cipherView.ts b/src/models/view/cipherView.ts index 50218527f8..e7ca481e7c 100644 --- a/src/models/view/cipherView.ts +++ b/src/models/view/cipherView.ts @@ -27,9 +27,6 @@ export class CipherView implements View { fields: FieldView[]; collectionIds: string[]; - // tslint:disable-next-line - private _subTitle: string; - constructor(c?: Cipher) { if (!c) { return; @@ -45,40 +42,19 @@ export class CipherView implements View { } get subTitle(): string { - if (this._subTitle == null) { - switch (this.type) { - case CipherType.Login: - this._subTitle = this.login.username; - break; - case CipherType.SecureNote: - this._subTitle = null; - break; - case CipherType.Card: - this._subTitle = this.card.brand; - if (this.card.number != null && this.card.number.length >= 4) { - if (this._subTitle !== '') { - this._subTitle += ', '; - } - this._subTitle += ('*' + this.card.number.substr(this.card.number.length - 4)); - } - break; - case CipherType.Identity: - this._subTitle = ''; - if (this.identity.firstName != null) { - this._subTitle = this.identity.firstName; - } - if (this.identity.lastName != null) { - if (this._subTitle !== '') { - this._subTitle += ' '; - } - this._subTitle += this.identity.lastName; - } - break; - default: - break; - } + switch (this.type) { + case CipherType.Login: + return this.login.subTitle; + case CipherType.SecureNote: + return this.secureNote.subTitle; + case CipherType.Card: + return this.card.subTitle; + case CipherType.Identity: + return this.identity.subTitle; + default: + break; } - return this._subTitle; + return null; } } diff --git a/src/models/view/identityView.ts b/src/models/view/identityView.ts index fb644c84a4..e1c3a2255f 100644 --- a/src/models/view/identityView.ts +++ b/src/models/view/identityView.ts @@ -4,9 +4,7 @@ import { Identity } from '../domain/identity'; export class IdentityView implements View { title: string; - firstName: string; middleName: string; - lastName: string; address1: string; address2: string; address3: string; @@ -22,7 +20,46 @@ export class IdentityView implements View { passportNumber: string; licenseNumber: string; + // tslint:disable + private _firstName: string; + private _lastName: string; + private _subTitle: string; + // tslint:enable + constructor(i?: Identity) { // ctor } + + get firstName(): string { + return this._firstName; + } + set firstName(value: string) { + this._firstName = value; + this._subTitle = null; + } + + get lastName(): string { + return this._lastName; + } + set lastName(value: string) { + this._lastName = value; + this._subTitle = null; + } + + get subTitle(): string { + if (this._subTitle == null && (this.firstName != null || this.lastName != null)) { + this._subTitle = ''; + if (this.firstName != null) { + this._subTitle = this.firstName; + } + if (this.lastName != null) { + if (this._subTitle !== '') { + this._subTitle += ' '; + } + this._subTitle += this.lastName; + } + } + + return this._subTitle; + } } diff --git a/src/models/view/loginView.ts b/src/models/view/loginView.ts index c2e5270f05..874bd5e494 100644 --- a/src/models/view/loginView.ts +++ b/src/models/view/loginView.ts @@ -5,19 +5,37 @@ import { Login } from '../domain/login'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; export class LoginView implements View { - uri: string; username: string; - password: string; - maskedPassword: string; totp: string; - // tslint:disable-next-line + // tslint:disable + private _uri: string; + private _username: string; + private _password: string; private _domain: string; + private _maskedPassword: string; + // tslint:enable constructor(l?: Login) { // ctor } + get uri(): string { + return this._uri; + } + set uri(value: string) { + this._uri = value; + this._domain = null; + } + + get password(): string { + return this._password; + } + set password(value: string) { + this._password = value; + this._maskedPassword = null; + } + get domain(): string { if (this._domain == null && this.uri != null) { const containerService = (window as any).bitwardenContainerService; @@ -31,4 +49,19 @@ export class LoginView implements View { return this._domain; } + + get maskedPassword(): string { + if (this._maskedPassword == null && this.password != null) { + this._maskedPassword = ''; + for (var i = 0; i < this.password.length; i++) { + this._maskedPassword += '•'; + } + } + + return this._maskedPassword; + } + + get subTitle(): string { + return this.username; + } } diff --git a/src/models/view/secureNoteView.ts b/src/models/view/secureNoteView.ts index b4eae6d446..4c131b884f 100644 --- a/src/models/view/secureNoteView.ts +++ b/src/models/view/secureNoteView.ts @@ -10,4 +10,8 @@ export class SecureNoteView implements View { constructor(n: SecureNote) { this.type = n.type; } + + get subTitle(): string { + return null; + } } From e5c1adedffff0197a736e068977bd1237bf6590e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 24 Jan 2018 23:27:04 -0500 Subject: [PATCH 0066/1626] lib updates for login view on desktop --- src/abstractions/token.service.ts | 38 +++++++++++++++---------------- src/abstractions/totp.service.ts | 6 ++--- src/models/view/cipherView.ts | 8 +++++++ src/models/view/loginView.ts | 14 +++++++++++- src/services/token.service.ts | 4 ++-- src/services/totp.service.ts | 4 ++-- 6 files changed, 47 insertions(+), 27 deletions(-) diff --git a/src/abstractions/token.service.ts b/src/abstractions/token.service.ts index 03f2434c9a..939b96f90f 100644 --- a/src/abstractions/token.service.ts +++ b/src/abstractions/token.service.ts @@ -1,23 +1,23 @@ -export interface TokenService { +export abstract class TokenService { token: string; decodedToken: any; refreshToken: string; - setTokens(accessToken: string, refreshToken: string): Promise; - setToken(token: string): Promise; - getToken(): Promise; - setRefreshToken(refreshToken: string): Promise; - getRefreshToken(): Promise; - setTwoFactorToken(token: string, email: string): Promise; - getTwoFactorToken(email: string): Promise; - clearTwoFactorToken(email: string): Promise; - clearToken(): Promise; - decodeToken(): any; - getTokenExpirationDate(): Date; - tokenSecondsRemaining(offsetSeconds?: number): number; - tokenNeedsRefresh(minutes?: number): boolean; - getUserId(): string; - getEmail(): string; - getName(): string; - getPremium(): boolean; - getIssuer(): string; + setTokens: (accessToken: string, refreshToken: string) => Promise; + setToken: (token: string) => Promise; + getToken: () => Promise; + setRefreshToken: (refreshToken: string) => Promise; + getRefreshToken: () => Promise; + setTwoFactorToken: (token: string, email: string) => Promise; + getTwoFactorToken: (email: string) => Promise; + clearTwoFactorToken: (email: string) => Promise; + clearToken: () => Promise; + decodeToken: () => any; + getTokenExpirationDate: () => Date; + tokenSecondsRemaining: (offsetSeconds?: number) => number; + tokenNeedsRefresh: (minutes?: number) => boolean; + getUserId: () => string; + getEmail: () => string; + getName: () => string; + getPremium: () => boolean; + getIssuer: () => string; } diff --git a/src/abstractions/totp.service.ts b/src/abstractions/totp.service.ts index 2e8fa79aa7..1155d3c2f7 100644 --- a/src/abstractions/totp.service.ts +++ b/src/abstractions/totp.service.ts @@ -1,4 +1,4 @@ -export interface TotpService { - getCode(keyb32: string): Promise; - isAutoCopyEnabled(): Promise; +export abstract class TotpService { + getCode: (keyb32: string) => Promise; + isAutoCopyEnabled: () => Promise; } diff --git a/src/models/view/cipherView.ts b/src/models/view/cipherView.ts index e7ca481e7c..b3f232bcbc 100644 --- a/src/models/view/cipherView.ts +++ b/src/models/view/cipherView.ts @@ -57,4 +57,12 @@ export class CipherView implements View { return null; } + + get hasAttachments(): boolean { + return this.attachments && this.attachments.length > 0; + } + + get hasFields(): boolean { + return this.fields && this.fields.length > 0; + } } diff --git a/src/models/view/loginView.ts b/src/models/view/loginView.ts index 874bd5e494..5bfa4318e6 100644 --- a/src/models/view/loginView.ts +++ b/src/models/view/loginView.ts @@ -53,7 +53,7 @@ export class LoginView implements View { get maskedPassword(): string { if (this._maskedPassword == null && this.password != null) { this._maskedPassword = ''; - for (var i = 0; i < this.password.length; i++) { + for (let i = 0; i < this.password.length; i++) { this._maskedPassword += '•'; } } @@ -64,4 +64,16 @@ export class LoginView implements View { get subTitle(): string { return this.username; } + + get domainOrUri(): string { + return this.domain != null ? this.domain : this.uri; + } + + get isWebsite(): boolean { + return this.uri != null && (this.uri.indexOf('http://') > -1 || this.uri.indexOf('https://') > -1); + } + + get canLaunch(): boolean { + return this.uri != null && this.uri.indexOf('://') > -1; + } } diff --git a/src/services/token.service.ts b/src/services/token.service.ts index 552767e5e0..94dbfa10fd 100644 --- a/src/services/token.service.ts +++ b/src/services/token.service.ts @@ -2,7 +2,7 @@ import { ConstantsService } from './constants.service'; import { UtilsService } from './utils.service'; import { StorageService } from '../abstractions/storage.service'; -import { TokenService as TokenServiceInterface } from '../abstractions/token.service'; +import { TokenService as TokenServiceAbstraction } from '../abstractions/token.service'; const Keys = { accessToken: 'accessToken', @@ -10,7 +10,7 @@ const Keys = { twoFactorTokenPrefix: 'twoFactorToken_', }; -export class TokenService implements TokenServiceInterface { +export class TokenService implements TokenServiceAbstraction { token: string; decodedToken: any; refreshToken: string; diff --git a/src/services/totp.service.ts b/src/services/totp.service.ts index ac6cc4cb78..e52c11e48c 100644 --- a/src/services/totp.service.ts +++ b/src/services/totp.service.ts @@ -1,7 +1,7 @@ import { ConstantsService } from './constants.service'; import { StorageService } from '../abstractions/storage.service'; -import { TotpService as TotpServiceInterface } from '../abstractions/totp.service'; +import { TotpService as TotpServiceAbstraction } from '../abstractions/totp.service'; const b32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; @@ -10,7 +10,7 @@ const TotpAlgorithm = { hash: { name: 'SHA-1' }, }; -export class TotpService implements TotpServiceInterface { +export class TotpService implements TotpServiceAbstraction { constructor(private storageService: StorageService) { } From 11755e409afabaceebeecc82c878eb226fb5b6d7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 25 Jan 2018 14:26:09 -0500 Subject: [PATCH 0067/1626] defining more abstractions --- src/abstractions/crypto.service.ts | 54 +++++++++++------------ src/abstractions/i18n.service.ts | 4 ++ src/abstractions/index.ts | 1 + src/abstractions/platformUtils.service.ts | 27 +++++++----- src/abstractions/utils.service.ts | 6 +-- src/models/view/cardView.ts | 14 ++++++ src/models/view/fieldView.ts | 25 ++++++++++- src/models/view/identityView.ts | 21 +++++++++ src/services/crypto.service.ts | 4 +- 9 files changed, 111 insertions(+), 45 deletions(-) create mode 100644 src/abstractions/i18n.service.ts diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 3a7e406f1a..b3fb5340b7 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -3,31 +3,31 @@ 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; - clearKey(): Promise; - clearKeyHash(): Promise; - clearEncKey(memoryOnly?: boolean): Promise; - clearPrivateKey(memoryOnly?: boolean): Promise; - clearOrgKeys(memoryOnly?: boolean): 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; +export abstract class 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; + clearKey: () => Promise; + clearKeyHash: () => Promise; + clearEncKey: (memoryOnly?: boolean) => Promise; + clearPrivateKey: (memoryOnly?: boolean) => Promise; + clearOrgKeys: (memoryOnly?: boolean) => 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/i18n.service.ts b/src/abstractions/i18n.service.ts new file mode 100644 index 0000000000..0a89e6eeba --- /dev/null +++ b/src/abstractions/i18n.service.ts @@ -0,0 +1,4 @@ +export abstract class I18nService { + t: (id: string) => string; + translate: (id: string) => string; +} diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index 69f1295104..154ef9238f 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -6,6 +6,7 @@ export { CollectionService } from './collection.service'; export { CryptoService } from './crypto.service'; export { EnvironmentService } from './environment.service'; export { FolderService } from './folder.service'; +export { I18nService } from './i18n.service'; export { LockService } from './lock.service'; export { MessagingService } from './messaging.service'; export { PasswordGenerationService } from './passwordGeneration.service'; diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index c1da1e8cc4..afcc30e5a2 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -1,15 +1,18 @@ import { DeviceType } from '../enums/deviceType'; -export interface PlatformUtilsService { - getDevice(): DeviceType; - getDeviceString(): string; - isFirefox(): boolean; - isChrome(): boolean; - isEdge(): boolean; - isOpera(): boolean; - isVivaldi(): boolean; - isSafari(): boolean; - analyticsId(): string; - getDomain(uriString: string): string; - isViewOpen(): boolean; +export abstract class PlatformUtilsService { + getDevice: () => DeviceType; + getDeviceString: () => string; + isFirefox: () => boolean; + isChrome: () => boolean; + isEdge: () => boolean; + isOpera: () => boolean; + isVivaldi: () => boolean; + isSafari: () => boolean; + analyticsId: () => string; + getDomain: (uriString: string) => string; + isViewOpen: () => boolean; + launchUri: (uri: string) => void; + saveFile: (win: Window, blobData: any, blobOptions: any, fileName: string) => void; + alertError: (title: string, message: string) => void; } diff --git a/src/abstractions/utils.service.ts b/src/abstractions/utils.service.ts index a251eedeaa..5ef1cce54d 100644 --- a/src/abstractions/utils.service.ts +++ b/src/abstractions/utils.service.ts @@ -1,4 +1,4 @@ -export interface UtilsService { - copyToClipboard(text: string, doc?: Document): void; - getHostname(uriString: string): string; +export abstract class UtilsService { + copyToClipboard: (text: string, doc?: Document) => void; + getHostname: (uriString: string) => string; } diff --git a/src/models/view/cardView.ts b/src/models/view/cardView.ts index 2e052fff55..b75fdf1fca 100644 --- a/src/models/view/cardView.ts +++ b/src/models/view/cardView.ts @@ -48,4 +48,18 @@ export class CardView implements View { } return this._subTitle; } + + get expiration(): string { + if (!this.expMonth && !this.expYear) { + return null; + } + + let exp = this.expMonth != null ? ('0' + this.expMonth).slice(-2) : '__'; + exp += (' / ' + (this.expYear != null ? this.formatYear(this.expYear) : '____')); + return exp; + } + + private formatYear(year: string): string { + return year.length === 2 ? '20' + year : year; + } } diff --git a/src/models/view/fieldView.ts b/src/models/view/fieldView.ts index f25f86cc6e..e3c544c8f3 100644 --- a/src/models/view/fieldView.ts +++ b/src/models/view/fieldView.ts @@ -6,10 +6,33 @@ import { Field } from '../domain/field'; export class FieldView implements View { name: string; - vault: string; type: FieldType; + // tslint:disable + private _value: string; + private _maskedValue: string; + // tslint:enable + constructor(f: Field) { this.type = f.type; } + + get value(): string { + return this._value; + } + set value(value: string) { + this._value = value; + this._maskedValue = null; + } + + get maskedValue(): string { + if (this._maskedValue == null && this.value != null) { + this._maskedValue = ''; + for (let i = 0; i < this.value.length; i++) { + this._maskedValue += '•'; + } + } + + return this._maskedValue; + } } diff --git a/src/models/view/identityView.ts b/src/models/view/identityView.ts index e1c3a2255f..ed8bf0e453 100644 --- a/src/models/view/identityView.ts +++ b/src/models/view/identityView.ts @@ -62,4 +62,25 @@ export class IdentityView implements View { return this._subTitle; } + + get fullName(): string { + if (this.title != null || this.firstName != null || this.middleName != null || this.lastName != null) { + let name = ''; + if (this.title != null) { + name += (this.title + ' '); + } + if (this.firstName != null) { + name += (this.firstName + ' '); + } + if (this.middleName != null) { + name += (this.middleName + ' '); + } + if (this.lastName != null) { + name += this.lastName; + } + return name.trim(); + } + + return null; + } } diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 84391897f8..e6ad642574 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -7,7 +7,7 @@ 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 { CryptoService as CryptoServiceAbstraction } from '../abstractions/crypto.service'; import { StorageService as StorageServiceInterface } from '../abstractions/storage.service'; import { ConstantsService } from './constants.service'; @@ -33,7 +33,7 @@ const AesAlgorithm = { const Crypto = window.crypto; const Subtle = Crypto.subtle; -export class CryptoService implements CryptoServiceInterface { +export class CryptoService implements CryptoServiceAbstraction { private key: SymmetricCryptoKey; private encKey: SymmetricCryptoKey; private legacyEtmKey: SymmetricCryptoKey; From 8d2a90e49646e42b39454b49baf2ea23d538549c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 25 Jan 2018 14:32:10 -0500 Subject: [PATCH 0068/1626] add options --- src/abstractions/platformUtils.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index afcc30e5a2..39857adbd9 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -12,7 +12,7 @@ export abstract class PlatformUtilsService { analyticsId: () => string; getDomain: (uriString: string) => string; isViewOpen: () => boolean; - launchUri: (uri: string) => void; + launchUri: (uri: string, options?: any) => void; saveFile: (win: Window, blobData: any, blobOptions: any, fileName: string) => void; alertError: (title: string, message: string) => void; } From 3869dcaf7b4933d113181284d25bcf8f215695e7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 25 Jan 2018 14:57:42 -0500 Subject: [PATCH 0069/1626] apply collection and folder views --- src/abstractions/collection.service.ts | 6 ++++-- src/abstractions/crypto.service.ts | 3 ++- src/abstractions/folder.service.ts | 8 +++++--- src/models/domain/collection.ts | 11 ++++------- src/models/domain/folder.ts | 10 ++++------ src/models/view/collectionView.ts | 18 ++++++++++++++++++ src/models/view/folderView.ts | 16 ++++++++++++++++ src/models/view/secureNoteView.ts | 6 +++++- src/services/collection.service.ts | 18 +++++++++--------- src/services/folder.service.ts | 14 +++++++------- 10 files changed, 74 insertions(+), 36 deletions(-) create mode 100644 src/models/view/collectionView.ts create mode 100644 src/models/view/folderView.ts diff --git a/src/abstractions/collection.service.ts b/src/abstractions/collection.service.ts index 0f7f25e7ab..6c51478555 100644 --- a/src/abstractions/collection.service.ts +++ b/src/abstractions/collection.service.ts @@ -2,13 +2,15 @@ import { CollectionData } from '../models/data/collectionData'; import { Collection } from '../models/domain/collection'; +import { CollectionView } from '../models/view/collectionView'; + export abstract class CollectionService { - decryptedCollectionCache: any[]; + decryptedCollectionCache: CollectionView[]; clearCache: () => void; get: (id: string) => Promise; getAll: () => Promise; - getAllDecrypted: () => Promise; + getAllDecrypted: () => Promise; upsert: (collection: CollectionData | CollectionData[]) => Promise; replace: (collections: { [id: string]: CollectionData; }) => Promise; clear: (userId: string) => Promise; diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index b3fb5340b7..dbf5f0da75 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -25,7 +25,8 @@ export abstract class CryptoService { 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; + 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; diff --git a/src/abstractions/folder.service.ts b/src/abstractions/folder.service.ts index 7b75f6aebc..381bc01317 100644 --- a/src/abstractions/folder.service.ts +++ b/src/abstractions/folder.service.ts @@ -2,14 +2,16 @@ import { FolderData } from '../models/data/folderData'; import { Folder } from '../models/domain/folder'; +import { FolderView } from '../models/view/folderView'; + export abstract class FolderService { - decryptedFolderCache: any[]; + decryptedFolderCache: FolderView[]; clearCache: () => void; - encrypt: (model: any) => Promise; + encrypt: (model: FolderView) => Promise; get: (id: string) => Promise; getAll: () => Promise; - getAllDecrypted: () => Promise; + getAllDecrypted: () => Promise; saveWithServer: (folder: Folder) => Promise; upsert: (folder: FolderData | FolderData[]) => Promise; replace: (folders: { [id: string]: FolderData; }) => Promise; diff --git a/src/models/domain/collection.ts b/src/models/domain/collection.ts index 4857ec2c44..45fd54cd8c 100644 --- a/src/models/domain/collection.ts +++ b/src/models/domain/collection.ts @@ -1,5 +1,7 @@ import { CollectionData } from '../data/collectionData'; +import { CollectionView } from '../view/collectionView'; + import { CipherString } from './cipherString'; import Domain from './domain'; @@ -21,13 +23,8 @@ export class Collection extends Domain { }, alreadyEncrypted, ['id', 'organizationId']); } - decrypt(): Promise { - const model = { - id: this.id, - organizationId: this.organizationId, - }; - - return this.decryptObj(model, { + decrypt(): Promise { + return this.decryptObj(new CollectionView(this), { name: null, }, this.organizationId); } diff --git a/src/models/domain/folder.ts b/src/models/domain/folder.ts index f25d1f8314..77b9fb310d 100644 --- a/src/models/domain/folder.ts +++ b/src/models/domain/folder.ts @@ -1,5 +1,7 @@ import { FolderData } from '../data/folderData'; +import { FolderView } from '../view/folderView'; + import { CipherString } from './cipherString'; import Domain from './domain'; @@ -19,12 +21,8 @@ export class Folder extends Domain { }, alreadyEncrypted, ['id']); } - decrypt(): Promise { - const model = { - id: this.id, - }; - - return this.decryptObj(model, { + decrypt(): Promise { + return this.decryptObj(new FolderView(this), { name: null, }, null); } diff --git a/src/models/view/collectionView.ts b/src/models/view/collectionView.ts new file mode 100644 index 0000000000..ab184a2c35 --- /dev/null +++ b/src/models/view/collectionView.ts @@ -0,0 +1,18 @@ +import { View } from './view'; + +import { Collection } from '../domain/collection'; + +export class CollectionView implements View { + id: string; + organizationId: string; + name: string; + + constructor(c?: Collection) { + if (!c) { + return; + } + + this.id = c.id; + this.organizationId = c.organizationId; + } +} diff --git a/src/models/view/folderView.ts b/src/models/view/folderView.ts new file mode 100644 index 0000000000..fc4bb219b3 --- /dev/null +++ b/src/models/view/folderView.ts @@ -0,0 +1,16 @@ +import { View } from './view'; + +import { Folder } from '../domain/folder'; + +export class FolderView implements View { + id: string; + name: string; + + constructor(f?: Folder) { + if (!f) { + return; + } + + this.id = f.id; + } +} diff --git a/src/models/view/secureNoteView.ts b/src/models/view/secureNoteView.ts index 4c131b884f..1047c41086 100644 --- a/src/models/view/secureNoteView.ts +++ b/src/models/view/secureNoteView.ts @@ -7,7 +7,11 @@ import { SecureNote } from '../domain/secureNote'; export class SecureNoteView implements View { type: SecureNoteType; - constructor(n: SecureNote) { + constructor(n?: SecureNote) { + if (!n) { + return; + } + this.type = n.type; } diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index f85dfcf8b1..5cdd393261 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -2,6 +2,8 @@ import { CollectionData } from '../models/data/collectionData'; import { Collection } from '../models/domain/collection'; +import { CollectionView } from '../models/view/collectionView'; + import { CollectionService as CollectionServiceAbstraction } from '../abstractions/collection.service'; import { CryptoService } from '../abstractions/crypto.service'; import { StorageService } from '../abstractions/storage.service'; @@ -12,7 +14,7 @@ const Keys = { }; export class CollectionService implements CollectionServiceAbstraction { - decryptedCollectionCache: any[]; + decryptedCollectionCache: CollectionView[]; constructor(private cryptoService: CryptoService, private userService: UserService, private storageService: StorageService) { @@ -46,7 +48,7 @@ export class CollectionService implements CollectionServiceAbstraction { return response; } - async getAllDecrypted(): Promise { + async getAllDecrypted(): Promise { if (this.decryptedCollectionCache != null) { return this.decryptedCollectionCache; } @@ -56,17 +58,15 @@ export class CollectionService implements CollectionServiceAbstraction { throw new Error('No key.'); } - const decFolders: any[] = []; + const decCollections: CollectionView[] = []; const promises: Array> = []; - const folders = await this.getAll(); - folders.forEach((folder) => { - promises.push(folder.decrypt().then((f: any) => { - decFolders.push(f); - })); + const collections = await this.getAll(); + collections.forEach((collection) => { + promises.push(collection.decrypt().then((c) => decCollections.push(c))); }); await Promise.all(promises); - this.decryptedCollectionCache = decFolders; + this.decryptedCollectionCache = decCollections; return this.decryptedCollectionCache; } diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index 0f8096849a..c185e20f14 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -6,6 +6,8 @@ import { FolderRequest } from '../models/request/folderRequest'; import { FolderResponse } from '../models/response/folderResponse'; +import { FolderView } from '../models/view/folderView'; + import { ApiService } from '../abstractions/api.service'; import { CryptoService } from '../abstractions/crypto.service'; import { FolderService as FolderServiceAbstraction } from '../abstractions/folder.service'; @@ -17,7 +19,7 @@ const Keys = { }; export class FolderService implements FolderServiceAbstraction { - decryptedFolderCache: any[]; + decryptedFolderCache: FolderView[]; constructor(private cryptoService: CryptoService, private userService: UserService, private noneFolder: () => string, private apiService: ApiService, @@ -28,7 +30,7 @@ export class FolderService implements FolderServiceAbstraction { this.decryptedFolderCache = null; } - async encrypt(model: any): Promise { + async encrypt(model: FolderView): Promise { const folder = new Folder(); folder.id = model.id; folder.name = await this.cryptoService.encrypt(model.name); @@ -59,12 +61,12 @@ export class FolderService implements FolderServiceAbstraction { return response; } - async getAllDecrypted(): Promise { + async getAllDecrypted(): Promise { if (this.decryptedFolderCache != null) { return this.decryptedFolderCache; } - const decFolders: any[] = [{ + const decFolders: FolderView[] = [{ id: null, name: this.noneFolder(), }]; @@ -77,9 +79,7 @@ export class FolderService implements FolderServiceAbstraction { const promises: Array> = []; const folders = await this.getAll(); folders.forEach((folder) => { - promises.push(folder.decrypt().then((f: any) => { - decFolders.push(f); - })); + promises.push(folder.decrypt().then((f) => decFolders.push(f))); }); await Promise.all(promises); From 47385a28184e7692551e0c212c0bd5bf7088fb2d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 25 Jan 2018 16:16:02 -0500 Subject: [PATCH 0070/1626] default to null --- src/models/view/cardView.ts | 4 ++-- src/models/view/identityView.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/models/view/cardView.ts b/src/models/view/cardView.ts index b75fdf1fca..26e3308e31 100644 --- a/src/models/view/cardView.ts +++ b/src/models/view/cardView.ts @@ -4,12 +4,12 @@ import { Card } from '../domain/card'; export class CardView implements View { cardholderName: string; - expMonth: string; + expMonth: string = null; expYear: string; code: string; // tslint:disable - private _brand: string; + private _brand: string = null; private _number: string; private _subTitle: string; // tslint:enable diff --git a/src/models/view/identityView.ts b/src/models/view/identityView.ts index ed8bf0e453..4ae5fa0cf9 100644 --- a/src/models/view/identityView.ts +++ b/src/models/view/identityView.ts @@ -3,7 +3,7 @@ import { View } from './view'; import { Identity } from '../domain/identity'; export class IdentityView implements View { - title: string; + title: string = null; middleName: string; address1: string; address2: string; From f8cbf8b247c3b7f1431705ab57fbbd0f17ba208e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 25 Jan 2018 22:59:53 -0500 Subject: [PATCH 0071/1626] fieldview ctor --- src/models/view/fieldView.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/models/view/fieldView.ts b/src/models/view/fieldView.ts index e3c544c8f3..b3f68a584f 100644 --- a/src/models/view/fieldView.ts +++ b/src/models/view/fieldView.ts @@ -13,7 +13,11 @@ export class FieldView implements View { private _maskedValue: string; // tslint:enable - constructor(f: Field) { + constructor(f?: Field) { + if (!f) { + return; + } + this.type = f.type; } From 6f9f1f9a2534a4fc6c50f6278bcf3cba7dd6f176 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 26 Jan 2018 10:35:44 -0500 Subject: [PATCH 0072/1626] extract analytics to jslib --- src/abstractions/platformUtils.service.ts | 1 + src/index.ts | 3 +- src/misc/analytics.ts | 96 +++++++++++++++++++++++ src/misc/index.ts | 1 + 4 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 src/misc/analytics.ts create mode 100644 src/misc/index.ts diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index 39857adbd9..7b1e98ebb2 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -15,4 +15,5 @@ export abstract class PlatformUtilsService { launchUri: (uri: string, options?: any) => void; saveFile: (win: Window, blobData: any, blobOptions: any, fileName: string) => void; alertError: (title: string, message: string) => void; + getApplicationVersion: () => string; } diff --git a/src/index.ts b/src/index.ts index da1f6af9e0..5d4c096cc3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,9 +2,10 @@ import * as Abstractions from './abstractions'; import * as Enums from './enums'; import * as Data from './models/data'; import * as Domain from './models/domain'; +import * as Misc from './misc'; import * as Request from './models/request'; import * as Response from './models/response'; import * as Services from './services'; import * as View from './models/view'; -export { Abstractions, Enums, Data, Domain, Request, Response, Services, View }; +export { Abstractions, Enums, Data, Domain, Misc, Request, Response, Services, View }; diff --git a/src/misc/analytics.ts b/src/misc/analytics.ts new file mode 100644 index 0000000000..98cfdebc5b --- /dev/null +++ b/src/misc/analytics.ts @@ -0,0 +1,96 @@ +import { AppIdService } from '../abstractions/appId.service'; +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; +import { StorageService } from '../abstractions/storage.service'; + +import { ConstantsService } from '../services/constants.service'; + +const GaObj = 'ga'; + +export class Analytics { + private gaTrackingId: string = null; + private isFirefox = false; + private appVersion: string; + + constructor(win: Window, private gaFilter?: () => boolean, + private platformUtilsService?: PlatformUtilsService, private storageService?: StorageService, + private appIdService?: AppIdService, private dependencyResolver?: () => any) { + if (dependencyResolver != null) { + const deps = dependencyResolver(); + if (platformUtilsService == null && deps.platformUtilsService) { + this.platformUtilsService = deps.platformUtilsService as PlatformUtilsService; + } + if (storageService == null && deps.storageService) { + this.storageService = deps.storageService as StorageService; + } + if (appIdService == null && deps.appIdService) { + this.appIdService = deps.appIdService as AppIdService; + } + } + + this.appVersion = this.platformUtilsService.getApplicationVersion(); + this.isFirefox = this.platformUtilsService.isFirefox(); + this.gaTrackingId = this.platformUtilsService.analyticsId(); + + (win as any).GoogleAnalyticsObject = GaObj; + (win as any)[GaObj] = async (action: string, param1: any, param2?: any) => { + await this.ga(action, param1, param2); + }; + } + + async ga(action: string, param1: any, param2?: any) { + if (this.gaFilter != null && this.gaFilter()) { + return; + } + + const disabled = await this.storageService.get(ConstantsService.disableGaKey); + // Default for Firefox is disabled. + if ((this.isFirefox && disabled == null) || disabled != null && disabled) { + return; + } + + if (action !== 'send' || !param1) { + return; + } + + const gaAnonAppId = await this.appIdService.getAnonymousAppId(); + const version = encodeURIComponent(this.appVersion); + let message = 'v=1&tid=' + this.gaTrackingId + '&cid=' + gaAnonAppId + '&cd1=' + version; + + if (param1 === 'pageview' && param2) { + message += this.gaTrackPageView(param2); + } else if (typeof param1 === 'object' && param1.hitType === 'pageview') { + message += this.gaTrackPageView(param1.page); + } else if (param1 === 'event' && param2) { + message += this.gaTrackEvent(param2); + } else if (typeof param1 === 'object' && param1.hitType === 'event') { + message += this.gaTrackEvent(param1); + } + + const request = new XMLHttpRequest(); + request.open('POST', 'https://www.google-analytics.com/collect', true); + request.send(message); + } + + private gaTrackEvent(options: any) { + return '&t=event&ec=' + (options.eventCategory ? encodeURIComponent(options.eventCategory) : 'Event') + + '&ea=' + encodeURIComponent(options.eventAction) + + (options.eventLabel ? '&el=' + encodeURIComponent(options.eventLabel) : '') + + (options.eventValue ? '&ev=' + encodeURIComponent(options.eventValue) : '') + + (options.page ? '&dp=' + this.cleanPagePath(options.page) : ''); + } + + private gaTrackPageView(pagePath: string) { + return '&t=pageview&dp=' + this.cleanPagePath(pagePath); + } + + private cleanPagePath(pagePath: string) { + const paramIndex = pagePath.indexOf('?'); + if (paramIndex > -1) { + pagePath = pagePath.substring(0, paramIndex); + } + if (pagePath.indexOf('!/') === 0 || pagePath.indexOf('#/') === 0) { + pagePath = pagePath.substring(1); + } + return encodeURIComponent(pagePath); + } +} diff --git a/src/misc/index.ts b/src/misc/index.ts new file mode 100644 index 0000000000..f26abc15fb --- /dev/null +++ b/src/misc/index.ts @@ -0,0 +1 @@ +export { Analytics } from './analytics'; From a611d6e77bf09589bab3447720e9d887bd9e0a53 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 26 Jan 2018 16:07:38 -0500 Subject: [PATCH 0073/1626] license and contrib --- CONTRIBUTING.md | 1 + LICENSE.txt | 674 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 675 insertions(+) create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..210e7f5120 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1 @@ +Code contributions are welcome! Please commit any pull requests against the `master` branch. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000000..30ace6a873 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file From 78dcbac7fa9f9573e6be1e2c3bead94edb64dfbe Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 26 Jan 2018 16:13:54 -0500 Subject: [PATCH 0074/1626] remove alerterror --- src/abstractions/platformUtils.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index 7b1e98ebb2..be8e3cb310 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -14,6 +14,5 @@ export abstract class PlatformUtilsService { isViewOpen: () => boolean; launchUri: (uri: string, options?: any) => void; saveFile: (win: Window, blobData: any, blobOptions: any, fileName: string) => void; - alertError: (title: string, message: string) => void; getApplicationVersion: () => string; } From ce4f683b966d3394963c4b5b256034e7f6859942 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 26 Jan 2018 22:16:35 -0500 Subject: [PATCH 0075/1626] added i18n sorting to folders, ciphers, collection --- src/abstractions/i18n.service.ts | 3 +++ src/services/cipher.service.ts | 11 ++++++++++- src/services/collection.service.ts | 11 ++++++++++- src/services/folder.service.ts | 16 +++++++++++++++- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/abstractions/i18n.service.ts b/src/abstractions/i18n.service.ts index 0a89e6eeba..372659d81d 100644 --- a/src/abstractions/i18n.service.ts +++ b/src/abstractions/i18n.service.ts @@ -1,4 +1,7 @@ export abstract class I18nService { + locale: string; + translationLocale: string; + collator: Intl.Collator; t: (id: string) => string; translate: (id: string) => string; } diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 784c3d8841..a25d02cf38 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -29,6 +29,7 @@ import { ConstantsService } from './constants.service'; import { ApiService } from '../abstractions/api.service'; import { CipherService as CipherServiceAbstraction } from '../abstractions/cipher.service'; import { CryptoService } from '../abstractions/crypto.service'; +import { I18nService } from '../abstractions/i18n.service'; import { SettingsService } from '../abstractions/settings.service'; import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; @@ -84,7 +85,7 @@ export class CipherService implements CipherServiceAbstraction { constructor(private cryptoService: CryptoService, private userService: UserService, private settingsService: SettingsService, private apiService: ApiService, - private storageService: StorageService) { + private storageService: StorageService, private i18nService: I18nService) { } clearCache(): void { @@ -189,6 +190,7 @@ export class CipherService implements CipherServiceAbstraction { }); await Promise.all(promises); + decCiphers.sort(this.getLocaleSortingFunction()) this.decryptedCipherCache = decCiphers; return this.decryptedCipherCache; } @@ -445,6 +447,13 @@ export class CipherService implements CipherServiceAbstraction { // Helpers + private getLocaleSortingFunction(): (a: CipherView, b: CipherView) => number { + return (a, b) => { + return this.i18nService.collator ? this.i18nService.collator.compare(a.name, b.name) : + a.name.localeCompare(b.name); + }; + } + private async encryptObjProperty(model: V, obj: D, map: any, key: SymmetricCryptoKey): Promise { const promises = []; diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index 5cdd393261..f860f5d9d3 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -6,6 +6,7 @@ import { CollectionView } from '../models/view/collectionView'; import { CollectionService as CollectionServiceAbstraction } from '../abstractions/collection.service'; import { CryptoService } from '../abstractions/crypto.service'; +import { I18nService } from '../abstractions/i18n.service'; import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; @@ -17,7 +18,7 @@ export class CollectionService implements CollectionServiceAbstraction { decryptedCollectionCache: CollectionView[]; constructor(private cryptoService: CryptoService, private userService: UserService, - private storageService: StorageService) { + private storageService: StorageService, private i18nService: I18nService) { } clearCache(): void { @@ -66,6 +67,7 @@ export class CollectionService implements CollectionServiceAbstraction { }); await Promise.all(promises); + decCollections.sort(this.getLocaleSortingFunction()); this.decryptedCollectionCache = decCollections; return this.decryptedCollectionCache; } @@ -122,4 +124,11 @@ export class CollectionService implements CollectionServiceAbstraction { await this.storageService.save(Keys.collectionsPrefix + userId, collections); this.decryptedCollectionCache = null; } + + private getLocaleSortingFunction(): (a: CollectionView, b: CollectionView) => number { + return (a, b) => { + return this.i18nService.collator ? this.i18nService.collator.compare(a.name, b.name) : + a.name.localeCompare(b.name); + }; + } } diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index c185e20f14..acc9b39c17 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -11,6 +11,7 @@ import { FolderView } from '../models/view/folderView'; import { ApiService } from '../abstractions/api.service'; import { CryptoService } from '../abstractions/crypto.service'; import { FolderService as FolderServiceAbstraction } from '../abstractions/folder.service'; +import { I18nService } from '../abstractions/i18n.service'; import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; @@ -23,7 +24,7 @@ export class FolderService implements FolderServiceAbstraction { constructor(private cryptoService: CryptoService, private userService: UserService, private noneFolder: () => string, private apiService: ApiService, - private storageService: StorageService) { + private storageService: StorageService, private i18nService: I18nService) { } clearCache(): void { @@ -83,6 +84,7 @@ export class FolderService implements FolderServiceAbstraction { }); await Promise.all(promises); + decFolders.sort(this.getLocaleSortingFunction()); this.decryptedFolderCache = decFolders; return this.decryptedFolderCache; } @@ -160,4 +162,16 @@ export class FolderService implements FolderServiceAbstraction { await this.apiService.deleteFolder(id); await this.delete(id); } + + private getLocaleSortingFunction(): (a: FolderView, b: FolderView) => number { + return (a, b) => { + if (a.id == null) { + // No folder is always last + return Number.MAX_SAFE_INTEGER; + } + + return this.i18nService.collator ? this.i18nService.collator.compare(a.name, b.name) : + a.name.localeCompare(b.name); + }; + } } From 3e43dd7aac7ddb187db610a5bf8af17186ac67f8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 26 Jan 2018 22:38:22 -0500 Subject: [PATCH 0076/1626] lint fix --- src/services/cipher.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index a25d02cf38..6818caabb5 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -190,7 +190,7 @@ export class CipherService implements CipherServiceAbstraction { }); await Promise.all(promises); - decCiphers.sort(this.getLocaleSortingFunction()) + decCiphers.sort(this.getLocaleSortingFunction()); this.decryptedCipherCache = decCiphers; return this.decryptedCipherCache; } From e160b97497d827eb9b7e8ef6f21f1e15001a619b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 26 Jan 2018 22:54:02 -0500 Subject: [PATCH 0077/1626] remove static sorting functions --- src/services/cipher.service.ts | 94 +++++++++++++++++----------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 6818caabb5..9ab02b910c 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -41,46 +41,6 @@ const Keys = { }; export class CipherService implements CipherServiceAbstraction { - 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: CipherView[]; constructor(private cryptoService: CryptoService, private userService: UserService, @@ -252,7 +212,7 @@ export class CipherService implements CipherServiceAbstraction { return null; } - const sortedCiphers = ciphers.sort(CipherService.sortCiphersByLastUsed); + const sortedCiphers = ciphers.sort(this.sortCiphersByLastUsed); return sortedCiphers[0]; } @@ -437,20 +397,60 @@ export class CipherService implements CipherServiceAbstraction { await this.deleteAttachment(id, attachmentId); } - sortCiphersByLastUsed(a: any, b: any): number { - return CipherService.sortCiphersByLastUsed(a, b); + sortCiphersByLastUsed(a: CipherView, b: CipherView): 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; } - sortCiphersByLastUsedThenName(a: any, b: any): number { - return CipherService.sortCiphersByLastUsedThenName(a, b); + sortCiphersByLastUsedThenName(a: CipherView, b: CipherView): number { + const result = this.sortCiphersByLastUsed(a, b); + if (result !== 0) { + return result; + } + + return this.getLocaleSortingFunction()(a, b); } // Helpers private getLocaleSortingFunction(): (a: CipherView, b: CipherView) => number { return (a, b) => { - return this.i18nService.collator ? this.i18nService.collator.compare(a.name, b.name) : - a.name.localeCompare(b.name); + let aName = a.name; + let bName = b.name; + + let result = this.i18nService.collator ? this.i18nService.collator.compare(aName, bName) : + aName.localeCompare(bName); + + if (result !== 0 || a.type !== CipherType.Login || b.type !== CipherType.Login) { + return result; + } + + if (a.login.username != null) { + aName += a.login.username; + } + + if (b.login.username != null) { + bName += b.login.username; + } + + return this.i18nService.collator ? this.i18nService.collator.compare(aName, bName) : + aName.localeCompare(bName); }; } From 15f254879fe60253d6c76cc73a35aea3c4225546 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 29 Jan 2018 17:59:57 -0500 Subject: [PATCH 0078/1626] abstract password generation service --- src/abstractions/passwordGeneration.service.ts | 16 +++++++--------- src/models/view/folderView.ts | 2 +- src/services/folder.service.ts | 7 +++---- src/services/passwordGeneration.service.ts | 8 ++++---- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/abstractions/passwordGeneration.service.ts b/src/abstractions/passwordGeneration.service.ts index 1531eb176b..a8967db281 100644 --- a/src/abstractions/passwordGeneration.service.ts +++ b/src/abstractions/passwordGeneration.service.ts @@ -1,12 +1,10 @@ import { PasswordHistory } from '../models/domain/passwordHistory'; -export interface PasswordGenerationService { - optionsCache: any; - history: PasswordHistory[]; - generatePassword(options: any): string; - getOptions(): any; - saveOptions(options: any): Promise; - getHistory(): Promise; - addHistory(password: string): Promise; - clear(): Promise; +export abstract class PasswordGenerationService { + generatePassword: (options: any) => string; + getOptions: () => any; + saveOptions: (options: any) => Promise; + getHistory: () => Promise; + addHistory: (password: string) => Promise; + clear: () => Promise; } diff --git a/src/models/view/folderView.ts b/src/models/view/folderView.ts index fc4bb219b3..ebe8ea157a 100644 --- a/src/models/view/folderView.ts +++ b/src/models/view/folderView.ts @@ -3,7 +3,7 @@ import { View } from './view'; import { Folder } from '../domain/folder'; export class FolderView implements View { - id: string; + id: string = null; name: string; constructor(f?: Folder) { diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index acc9b39c17..81c03a15f5 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -67,10 +67,9 @@ export class FolderService implements FolderServiceAbstraction { return this.decryptedFolderCache; } - const decFolders: FolderView[] = [{ - id: null, - name: this.noneFolder(), - }]; + const noneFolder = new FolderView(); + noneFolder.name = this.noneFolder(); + const decFolders: FolderView[] = [noneFolder]; const key = await this.cryptoService.getKey(); if (key == null) { diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 403bb104e2..33c3eea29a 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -5,7 +5,7 @@ import { UtilsService } from './utils.service'; import { CryptoService } from '../abstractions/crypto.service'; import { - PasswordGenerationService as PasswordGenerationServiceInterface, + PasswordGenerationService as PasswordGenerationServiceAbstraction, } from '../abstractions/passwordGeneration.service'; import { StorageService } from '../abstractions/storage.service'; @@ -29,7 +29,7 @@ const Keys = { const MaxPasswordsInHistory = 100; -export class PasswordGenerationService implements PasswordGenerationServiceInterface { +export class PasswordGenerationService implements PasswordGenerationServiceAbstraction { static generatePassword(options: any): string { // overload defaults with given options const o = Object.assign({}, DefaultOptions, options); @@ -147,8 +147,8 @@ export class PasswordGenerationService implements PasswordGenerationServiceInter return password; } - optionsCache: any; - history: PasswordHistory[] = []; + private optionsCache: any; + private history: PasswordHistory[] = []; constructor(private cryptoService: CryptoService, private storageService: StorageService) { } From 4694793785b1310ed8f4460733e866a9e15ac35b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 30 Jan 2018 13:13:12 -0500 Subject: [PATCH 0079/1626] reset domain to null if empty --- src/models/view/loginView.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/models/view/loginView.ts b/src/models/view/loginView.ts index 5bfa4318e6..c3554c9c38 100644 --- a/src/models/view/loginView.ts +++ b/src/models/view/loginView.ts @@ -42,6 +42,9 @@ export class LoginView implements View { if (containerService) { const platformUtilsService: PlatformUtilsService = containerService.getPlatformUtilsService(); this._domain = platformUtilsService.getDomain(this.uri); + if (this._domain === '') { + this._domain = null; + } } else { throw new Error('window.bitwardenContainerService not initialized.'); } @@ -70,7 +73,7 @@ export class LoginView implements View { } get isWebsite(): boolean { - return this.uri != null && (this.uri.indexOf('http://') > -1 || this.uri.indexOf('https://') > -1); + return this.uri != null && (this.uri.indexOf('http://') === 0 || this.uri.indexOf('https://') === 0); } get canLaunch(): boolean { From 5845291aafc64c0bf81537952237cb987467cb33 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 31 Jan 2018 14:27:11 -0500 Subject: [PATCH 0080/1626] abstract api service --- src/abstractions/api.service.ts | 35 +++++++++++++++++---------------- src/services/api.service.ts | 4 ++-- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 6059f3a4a5..8e0cf66498 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -12,26 +12,27 @@ import { FolderResponse } from '../models/response/folderResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { SyncResponse } from '../models/response/syncResponse'; -export interface ApiService { +export abstract class ApiService { urlsSet: boolean; baseUrl: string; identityBaseUrl: string; deviceType: string; logoutCallback: Function; - setUrls(urls: EnvironmentUrls): void; - postIdentityToken(request: TokenRequest): Promise; - refreshIdentityToken(): Promise; - postTwoFactorEmail(request: TwoFactorEmailRequest): Promise; - getAccountRevisionDate(): Promise; - postPasswordHint(request: PasswordHintRequest): Promise; - postRegister(request: RegisterRequest): Promise; - postFolder(request: FolderRequest): Promise; - putFolder(id: string, request: FolderRequest): Promise; - deleteFolder(id: string): Promise; - postCipher(request: CipherRequest): Promise; - putCipher(id: string, request: CipherRequest): Promise; - deleteCipher(id: string): Promise; - postCipherAttachment(id: string, data: FormData): Promise; - deleteCipherAttachment(id: string, attachmentId: string): Promise; - getSync(): Promise; + + setUrls: (urls: EnvironmentUrls) => void; + postIdentityToken: (request: TokenRequest) => Promise; + refreshIdentityToken: () => Promise; + postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise; + getAccountRevisionDate: () => Promise; + postPasswordHint: (request: PasswordHintRequest) => Promise; + postRegister: (request: RegisterRequest) => Promise; + postFolder: (request: FolderRequest) => Promise; + putFolder: (id: string, request: FolderRequest) => Promise; + deleteFolder: (id: string) => Promise; + postCipher: (request: CipherRequest) => Promise; + putCipher: (id: string, request: CipherRequest) => Promise; + deleteCipher: (id: string) => Promise; + postCipherAttachment: (id: string, data: FormData) => Promise; + deleteCipherAttachment: (id: string, attachmentId: string) => Promise; + getSync: () => Promise; } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index e9838ed095..5fd95daac4 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -1,6 +1,6 @@ import { ConstantsService } from './constants.service'; -import { ApiService as ApiServiceInterface } from '../abstractions/api.service'; +import { ApiService as ApiServiceAbstraction } from '../abstractions/api.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { TokenService } from '../abstractions/token.service'; @@ -19,7 +19,7 @@ import { FolderResponse } from '../models/response/folderResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { SyncResponse } from '../models/response/syncResponse'; -export class ApiService implements ApiServiceInterface { +export class ApiService implements ApiServiceAbstraction { urlsSet: boolean = false; baseUrl: string; identityBaseUrl: string; From 7c3bed1737e0e1193cba4e24921b75328b046f81 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 1 Feb 2018 22:55:49 -0500 Subject: [PATCH 0081/1626] updates for 2fa auth services --- src/abstractions/api.service.ts | 3 +- src/abstractions/auth.service.ts | 14 +- src/abstractions/platformUtils.service.ts | 1 + src/enums/index.ts | 1 + src/enums/twoFactorProviderType.ts | 8 + src/models/domain/authResult.ts | 6 + src/models/domain/index.ts | 1 + .../response/identityTwoFactorResponse.ts | 17 ++ src/models/response/index.ts | 1 + src/services/api.service.ts | 5 +- src/services/auth.service.ts | 172 +++++++++++++++--- src/services/cipher.service.ts | 2 +- 12 files changed, 195 insertions(+), 36 deletions(-) create mode 100644 src/enums/twoFactorProviderType.ts create mode 100644 src/models/domain/authResult.ts create mode 100644 src/models/response/identityTwoFactorResponse.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 8e0cf66498..4c7d621c14 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -10,6 +10,7 @@ import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; import { CipherResponse } from '../models/response/cipherResponse'; import { FolderResponse } from '../models/response/folderResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; +import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; import { SyncResponse } from '../models/response/syncResponse'; export abstract class ApiService { @@ -20,7 +21,7 @@ export abstract class ApiService { logoutCallback: Function; setUrls: (urls: EnvironmentUrls) => void; - postIdentityToken: (request: TokenRequest) => Promise; + postIdentityToken: (request: TokenRequest) => Promise; refreshIdentityToken: () => Promise; postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise; getAccountRevisionDate: () => Promise; diff --git a/src/abstractions/auth.service.ts b/src/abstractions/auth.service.ts index 45f7ca034d..08ca102729 100644 --- a/src/abstractions/auth.service.ts +++ b/src/abstractions/auth.service.ts @@ -1,5 +1,15 @@ +import { TwoFactorProviderType } from '../enums/twoFactorProviderType'; + +import { AuthResult } from '../models/domain/authResult'; + export abstract class AuthService { - logIn: (email: string, masterPassword: string, twoFactorProvider?: number, twoFactorToken?: string, - remember?: boolean) => Promise; + email: string; + masterPasswordHash: string; + twoFactorProviders: Map; + + logIn: (email: string, masterPassword: string) => Promise; + logInTwoFactor: (twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, + remember?: boolean) => Promise; logOut: (callback: Function) => void; + getDefaultTwoFactorProvider: (u2fSupported: boolean) => TwoFactorProviderType; } diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index be8e3cb310..d0c16bf2b0 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -15,4 +15,5 @@ export abstract class PlatformUtilsService { launchUri: (uri: string, options?: any) => void; saveFile: (win: Window, blobData: any, blobOptions: any, fileName: string) => void; getApplicationVersion: () => string; + supportsU2f: (win: Window) => boolean; } diff --git a/src/enums/index.ts b/src/enums/index.ts index d1fff1d889..efdf363c89 100644 --- a/src/enums/index.ts +++ b/src/enums/index.ts @@ -3,3 +3,4 @@ export { DeviceType } from './deviceType'; export { EncryptionType } from './encryptionType'; export { FieldType } from './fieldType'; export { SecureNoteType } from './secureNoteType'; +export { TwoFactorProviderType } from './twoFactorProviderType'; diff --git a/src/enums/twoFactorProviderType.ts b/src/enums/twoFactorProviderType.ts new file mode 100644 index 0000000000..9d767ae026 --- /dev/null +++ b/src/enums/twoFactorProviderType.ts @@ -0,0 +1,8 @@ +export enum TwoFactorProviderType { + Authenticator = 0, + Email = 1, + Duo = 2, + Yubikey = 3, + U2f = 4, + Remember = 5, +} diff --git a/src/models/domain/authResult.ts b/src/models/domain/authResult.ts new file mode 100644 index 0000000000..cb4bb57c65 --- /dev/null +++ b/src/models/domain/authResult.ts @@ -0,0 +1,6 @@ +import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; + +export class AuthResult { + twoFactor: boolean = false; + twoFactorProviders: Map = null; +} diff --git a/src/models/domain/index.ts b/src/models/domain/index.ts index 698585a3c9..7597a746d3 100644 --- a/src/models/domain/index.ts +++ b/src/models/domain/index.ts @@ -1,4 +1,5 @@ export { Attachment } from './attachment'; +export { AuthResult } from './authResult'; export { Card } from './card'; export { Cipher } from './cipher'; export { CipherString } from './cipherString'; diff --git a/src/models/response/identityTwoFactorResponse.ts b/src/models/response/identityTwoFactorResponse.ts new file mode 100644 index 0000000000..2adeeaa053 --- /dev/null +++ b/src/models/response/identityTwoFactorResponse.ts @@ -0,0 +1,17 @@ +import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; + +export class IdentityTwoFactorResponse { + twoFactorProviders: TwoFactorProviderType[]; + twoFactorProviders2 = new Map(); + + constructor(response: any) { + this.twoFactorProviders = response.TwoFactorProviders; + if (response.TwoFactorProviders2 != null) { + for (const prop in response.TwoFactorProviders2) { + if (response.TwoFactorProviders2.hasOwnProperty(prop)) { + this.twoFactorProviders2.set(parseInt(prop, null), response.TwoFactorProviders2[prop]); + } + } + } + } +} diff --git a/src/models/response/index.ts b/src/models/response/index.ts index 00f85d119d..98829b10ea 100644 --- a/src/models/response/index.ts +++ b/src/models/response/index.ts @@ -7,6 +7,7 @@ export { ErrorResponse } from './errorResponse'; export { FolderResponse } from './folderResponse'; export { GlobalDomainResponse } from './globalDomainResponse'; export { IdentityTokenResponse } from './identityTokenResponse'; +export { IdentityTwoFactorResponse } from './identityTwoFactorResponse'; export { KeysResponse } from './keysResponse'; export { ListResponse } from './listResponse'; export { ProfileOrganizationResponse } from './profileOrganizationResponse'; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 5fd95daac4..1e452af755 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -17,6 +17,7 @@ import { CipherResponse } from '../models/response/cipherResponse'; import { ErrorResponse } from '../models/response/errorResponse'; import { FolderResponse } from '../models/response/folderResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; +import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; import { SyncResponse } from '../models/response/syncResponse'; export class ApiService implements ApiServiceAbstraction { @@ -72,7 +73,7 @@ export class ApiService implements ApiServiceAbstraction { // Auth APIs - async postIdentityToken(request: TokenRequest): Promise { + async postIdentityToken(request: TokenRequest): Promise { const response = await fetch(new Request(this.identityBaseUrl + '/connect/token', { body: this.qsStringify(request.toIdentityToken()), cache: 'no-cache', @@ -96,7 +97,7 @@ export class ApiService implements ApiServiceAbstraction { } else if (response.status === 400 && responseJson.TwoFactorProviders2 && Object.keys(responseJson.TwoFactorProviders2).length) { await this.tokenService.clearTwoFactorToken(request.email); - return responseJson.TwoFactorProviders2; + return new IdentityTwoFactorResponse(responseJson); } } diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 2d2328c6a3..c12904b1b5 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,40 +1,148 @@ +import { TwoFactorProviderType } from '../enums/twoFactorProviderType'; + +import { AuthResult } from '../models/domain/authResult'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; + import { DeviceRequest } from '../models/request/deviceRequest'; import { TokenRequest } from '../models/request/tokenRequest'; +import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; +import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; + import { ConstantsService } from '../services/constants.service'; import { ApiService } from '../abstractions/api.service'; import { AppIdService } from '../abstractions/appId.service'; import { CryptoService } from '../abstractions/crypto.service'; +import { I18nService } from '../abstractions/i18n.service'; import { MessagingService } from '../abstractions/messaging.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { TokenService } from '../abstractions/token.service'; import { UserService } from '../abstractions/user.service'; +export const TwoFactorProviders = { + [TwoFactorProviderType.Authenticator]: { + name: null as string, + description: null as string, + active: true, + free: true, + displayOrder: 0, + priority: 1, + }, + [TwoFactorProviderType.Yubikey]: { + name: null as string, + description: null as string, + active: true, + free: false, + displayOrder: 1, + priority: 3, + }, + [TwoFactorProviderType.Duo]: { + name: 'Duo', + description: null as string, + active: true, + free: false, + displayOrder: 2, + priority: 2, + }, + [TwoFactorProviderType.U2f]: { + name: null as string, + description: null as string, + active: true, + free: false, + displayOrder: 3, + priority: 4, + }, + [TwoFactorProviderType.Email]: { + name: null as string, + description: null as string, + active: true, + free: false, + displayOrder: 4, + priority: 0, + }, +}; + export class AuthService { - constructor(public cryptoService: CryptoService, public apiService: ApiService, public userService: UserService, - public tokenService: TokenService, public appIdService: AppIdService, - public platformUtilsService: PlatformUtilsService, public constantsService: ConstantsService, - public messagingService: MessagingService) { + email: string; + masterPasswordHash: string; + twoFactorProviders: Map; + + private key: SymmetricCryptoKey; + + constructor(private cryptoService: CryptoService, private apiService: ApiService, private userService: UserService, + private tokenService: TokenService, private appIdService: AppIdService, private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, private constantsService: ConstantsService, + private messagingService: MessagingService) { } - async logIn(email: string, masterPassword: string, twoFactorProvider?: number, - twoFactorToken?: string, remember?: boolean) { + init() { + TwoFactorProviders[TwoFactorProviderType.Email].name = this.i18nService.t('emailTitle'); + TwoFactorProviders[TwoFactorProviderType.Email].description = this.i18nService.t('emailDesc'); + + TwoFactorProviders[TwoFactorProviderType.Authenticator].name = this.i18nService.t('authenticatorAppTitle'); + TwoFactorProviders[TwoFactorProviderType.Authenticator].description = + this.i18nService.t('authenticatorAppDesc'); + + TwoFactorProviders[TwoFactorProviderType.Duo].description = this.i18nService.t('duoDesc'); + + TwoFactorProviders[TwoFactorProviderType.U2f].name = this.i18nService.t('u2fTitle'); + TwoFactorProviders[TwoFactorProviderType.U2f].description = this.i18nService.t('u2fDesc'); + + TwoFactorProviders[TwoFactorProviderType.Yubikey].name = this.i18nService.t('yubiKeyTitle'); + TwoFactorProviders[TwoFactorProviderType.Yubikey].description = this.i18nService.t('yubiKeyDesc'); + } + + async logIn(email: string, masterPassword: string): Promise { email = email.toLowerCase(); - const key = this.cryptoService.makeKey(masterPassword, email); - const appId = await this.appIdService.getAppId(); - const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email); const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); + return await this.logInHelper(email, hashedPassword, key); + } + async logInTwoFactor(twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, + remember?: boolean): Promise { + return await this.logInHelper(this.email, this.masterPasswordHash, this.key, twoFactorProvider, + twoFactorToken, remember); + } + + logOut(callback: Function) { + callback(); + } + + getDefaultTwoFactorProvider(u2fSupported: boolean): TwoFactorProviderType { + if (this.twoFactorProviders == null) { + return null; + } + + let providerType: TwoFactorProviderType = null; + let providerPriority = -1; + this.twoFactorProviders.forEach((value, type) => { + const provider = (TwoFactorProviders as any)[type]; + if (provider != null && provider.active && provider.priority > providerPriority) { + if (type === TwoFactorProviderType.U2f && !u2fSupported) { + return; + } + + providerType = type; + providerPriority = provider.priority; + } + }); + + return providerType; + } + + private async logInHelper(email: string, hashedPassword: string, key: SymmetricCryptoKey, + twoFactorProvider?: TwoFactorProviderType, twoFactorToken?: string, remember?: boolean): Promise { + const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email); + const appId = await this.appIdService.getAppId(); const deviceRequest = new DeviceRequest(appId, this.platformUtilsService); let request: TokenRequest; - if (twoFactorToken != null && twoFactorProvider != null) { request = new TokenRequest(email, hashedPassword, twoFactorProvider, twoFactorToken, remember, deviceRequest); - } else if (storedTwoFactorToken) { + } else if (storedTwoFactorToken != null) { request = new TokenRequest(email, hashedPassword, this.constantsService.twoFactorProvider.remember, storedTwoFactorToken, false, deviceRequest); } else { @@ -42,37 +150,41 @@ export class AuthService { } const response = await this.apiService.postIdentityToken(request); - if (!response) { - return; - } - if (!response.accessToken) { + this.clearState(); + const result = new AuthResult(); + result.twoFactor = !(response as any).accessToken; + + if (result.twoFactor) { // two factor required - return { - twoFactor: true, - twoFactorProviders: response, - }; + const twoFactorResponse = response as IdentityTwoFactorResponse; + this.email = email; + this.masterPasswordHash = hashedPassword; + this.key = key; + this.twoFactorProviders = twoFactorResponse.twoFactorProviders2; + result.twoFactorProviders = twoFactorResponse.twoFactorProviders2; + return result; } - if (response.twoFactorToken) { - this.tokenService.setTwoFactorToken(response.twoFactorToken, email); + const tokenResponse = response as IdentityTokenResponse; + if (tokenResponse.twoFactorToken != null) { + this.tokenService.setTwoFactorToken(tokenResponse.twoFactorToken, email); } - await this.tokenService.setTokens(response.accessToken, response.refreshToken); + await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken); await this.cryptoService.setKey(key); await this.cryptoService.setKeyHash(hashedPassword); await this.userService.setUserIdAndEmail(this.tokenService.getUserId(), this.tokenService.getEmail()); - await this.cryptoService.setEncKey(response.key); - await this.cryptoService.setEncPrivateKey(response.privateKey); + await this.cryptoService.setEncKey(tokenResponse.key); + await this.cryptoService.setEncPrivateKey(tokenResponse.privateKey); this.messagingService.send('loggedIn'); - return { - twoFactor: false, - twoFactorProviders: null, - }; + return result; } - logOut(callback: Function) { - callback(); + private clearState(): void { + this.email = null; + this.masterPasswordHash = null; + this.twoFactorProviders = null; } } diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 9ab02b910c..fb8e532d62 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -434,7 +434,7 @@ export class CipherService implements CipherServiceAbstraction { let aName = a.name; let bName = b.name; - let result = this.i18nService.collator ? this.i18nService.collator.compare(aName, bName) : + const result = this.i18nService.collator ? this.i18nService.collator.compare(aName, bName) : aName.localeCompare(bName); if (result !== 0 || a.type !== CipherType.Login || b.type !== CipherType.Login) { From 17b9e9b03a8c6680a3ff177a7df1e295b298e01b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 2 Feb 2018 12:02:49 -0500 Subject: [PATCH 0082/1626] add placeholder support for i18n --- src/abstractions/i18n.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/abstractions/i18n.service.ts b/src/abstractions/i18n.service.ts index 372659d81d..f57783b474 100644 --- a/src/abstractions/i18n.service.ts +++ b/src/abstractions/i18n.service.ts @@ -2,6 +2,6 @@ export abstract class I18nService { locale: string; translationLocale: string; collator: Intl.Collator; - t: (id: string) => string; - translate: (id: string) => string; + t: (id: string, p1?: string, p2?: string, p3?: string) => string; + translate: (id: string, p1?: string, p2?: string, p3?: string) => string; } From 4a3705e6754bda70bfd5b0a92c950f686152c052 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 2 Feb 2018 12:03:05 -0500 Subject: [PATCH 0083/1626] remove unnecessary fields from 2fa providers --- src/services/auth.service.ts | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index c12904b1b5..70e9cae68a 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -22,43 +22,33 @@ import { UserService } from '../abstractions/user.service'; export const TwoFactorProviders = { [TwoFactorProviderType.Authenticator]: { + type: TwoFactorProviderType.Authenticator, name: null as string, description: null as string, - active: true, - free: true, - displayOrder: 0, priority: 1, }, [TwoFactorProviderType.Yubikey]: { + type: TwoFactorProviderType.Yubikey, name: null as string, description: null as string, - active: true, - free: false, - displayOrder: 1, priority: 3, }, [TwoFactorProviderType.Duo]: { + type: TwoFactorProviderType.Duo, name: 'Duo', description: null as string, - active: true, - free: false, - displayOrder: 2, priority: 2, }, [TwoFactorProviderType.U2f]: { + type: TwoFactorProviderType.U2f, name: null as string, description: null as string, - active: true, - free: false, - displayOrder: 3, priority: 4, }, [TwoFactorProviderType.Email]: { + type: TwoFactorProviderType.Email, name: null as string, description: null as string, - active: true, - free: false, - displayOrder: 4, priority: 0, }, }; @@ -119,7 +109,7 @@ export class AuthService { let providerPriority = -1; this.twoFactorProviders.forEach((value, type) => { const provider = (TwoFactorProviders as any)[type]; - if (provider != null && provider.active && provider.priority > providerPriority) { + if (provider != null && provider.priority > providerPriority) { if (type === TwoFactorProviderType.U2f && !u2fSupported) { return; } From 4c6a1892c0d0364a2b15642ae3d2475adb6b4202 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 2 Feb 2018 23:47:03 -0500 Subject: [PATCH 0084/1626] show dialog interface --- src/abstractions/platformUtils.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index d0c16bf2b0..893f99a9d4 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -16,4 +16,6 @@ export abstract class PlatformUtilsService { saveFile: (win: Window, blobData: any, blobOptions: any, fileName: string) => void; getApplicationVersion: () => string; supportsU2f: (win: Window) => boolean; + showDialog: (text: string, title?: string, confirmText?: string, cancelText?: string, + type?: string) => Promise; } From 78088acac72452f89fc770ec06e55113e4e8867c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 8 Feb 2018 10:04:47 -0500 Subject: [PATCH 0085/1626] update packages --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 57ee2d9e5c..8177c73edd 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "rimraf": "^2.6.2", "tslint": "^5.8.0", "typedoc": "^0.9.0", - "typescript": "^2.6.2" + "typescript": "^2.7.1" }, "dependencies": { "node-forge": "0.7.1", From 9cd74af2df5f946fa2203cc3154c328bec6b5f12 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 8 Feb 2018 10:49:00 -0500 Subject: [PATCH 0086/1626] sync service abstraction --- src/abstractions/sync.service.ts | 12 ++++++------ src/services/sync.service.ts | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/abstractions/sync.service.ts b/src/abstractions/sync.service.ts index a3287c39c9..ef4b4e8dc8 100644 --- a/src/abstractions/sync.service.ts +++ b/src/abstractions/sync.service.ts @@ -1,9 +1,9 @@ -export interface SyncService { +export abstract class SyncService { syncInProgress: boolean; - getLastSync(): Promise; - setLastSync(date: Date): Promise; - syncStarted(): void; - syncCompleted(successfully: boolean): void; - fullSync(forceSync: boolean): Promise; + getLastSync: () => Promise; + setLastSync: (date: Date) => Promise; + syncStarted: () => void; + syncCompleted: (successfully: boolean) => void; + fullSync: (forceSync: boolean) => Promise; } diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index 79e726c9aa..f233803c3e 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -6,7 +6,7 @@ import { FolderService } from '../abstractions/folder.service'; import { MessagingService } from '../abstractions/messaging.service'; import { SettingsService } from '../abstractions/settings.service'; import { StorageService } from '../abstractions/storage.service'; -import { SyncService as SyncServiceInterface } from '../abstractions/sync.service'; +import { SyncService as SyncServiceAbstraction } from '../abstractions/sync.service'; import { UserService } from '../abstractions/user.service'; import { CipherData } from '../models/data/cipherData'; @@ -23,7 +23,7 @@ const Keys = { lastSyncPrefix: 'lastSync_', }; -export class SyncService implements SyncServiceInterface { +export class SyncService implements SyncServiceAbstraction { syncInProgress: boolean = false; constructor(private userService: UserService, private apiService: ApiService, From 28ed4144dbc0718bf92441d624e7cc27d06e0f23 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 8 Feb 2018 12:24:37 -0500 Subject: [PATCH 0087/1626] abstract services --- src/abstractions/messaging.service.ts | 4 ++-- src/abstractions/settings.service.ts | 10 +++++----- src/abstractions/user.service.ts | 17 +++++++++-------- src/services/settings.service.ts | 4 ++-- src/services/user.service.ts | 4 ++-- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/abstractions/messaging.service.ts b/src/abstractions/messaging.service.ts index 6bafce93e4..a38b4c7f7b 100644 --- a/src/abstractions/messaging.service.ts +++ b/src/abstractions/messaging.service.ts @@ -1,3 +1,3 @@ -export interface MessagingService { - send(subscriber: string, arg?: any): void; +export abstract class MessagingService { + send: (subscriber: string, arg?: any) => void; } diff --git a/src/abstractions/settings.service.ts b/src/abstractions/settings.service.ts index 97d2bca975..6104c1d1be 100644 --- a/src/abstractions/settings.service.ts +++ b/src/abstractions/settings.service.ts @@ -1,6 +1,6 @@ -export interface SettingsService { - clearCache(): void; - getEquivalentDomains(): Promise; - setEquivalentDomains(equivalentDomains: string[][]): Promise; - clear(userId: string): Promise; +export abstract class SettingsService { + clearCache: () => void; + getEquivalentDomains: () => Promise; + setEquivalentDomains: (equivalentDomains: string[][]) => Promise; + clear: (userId: string) => Promise; } diff --git a/src/abstractions/user.service.ts b/src/abstractions/user.service.ts index 7afbd0b748..5621c3be50 100644 --- a/src/abstractions/user.service.ts +++ b/src/abstractions/user.service.ts @@ -1,12 +1,13 @@ -export interface UserService { +export abstract class UserService { userId: string; email: string; stamp: string; - setUserIdAndEmail(userId: string, email: string): Promise; - setSecurityStamp(stamp: string): Promise; - getUserId(): Promise; - getEmail(): Promise; - getSecurityStamp(): Promise; - clear(): Promise; - isAuthenticated(): Promise; + + setUserIdAndEmail: (userId: string, email: string) => Promise; + setSecurityStamp: (stamp: string) => Promise; + getUserId: () => Promise; + getEmail: () => Promise; + getSecurityStamp: () => Promise; + clear: () => Promise; + isAuthenticated: () => Promise; } diff --git a/src/services/settings.service.ts b/src/services/settings.service.ts index 87302a638c..506bdf01a0 100644 --- a/src/services/settings.service.ts +++ b/src/services/settings.service.ts @@ -1,4 +1,4 @@ -import { SettingsService as SettingsServiceInterface } from '../abstractions/settings.service'; +import { SettingsService as SettingsServiceAbstraction } from '../abstractions/settings.service'; import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; @@ -7,7 +7,7 @@ const Keys = { equivalentDomains: 'equivalentDomains', }; -export class SettingsService implements SettingsServiceInterface { +export class SettingsService implements SettingsServiceAbstraction { private settingsCache: any; constructor(private userService: UserService, private storageService: StorageService) { diff --git a/src/services/user.service.ts b/src/services/user.service.ts index aa2b19e179..1949cadb48 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -1,6 +1,6 @@ import { StorageService } from '../abstractions/storage.service'; import { TokenService } from '../abstractions/token.service'; -import { UserService as UserServiceInterface } from '../abstractions/user.service'; +import { UserService as UserServiceAbsrtaction } from '../abstractions/user.service'; const Keys = { userId: 'userId', @@ -8,7 +8,7 @@ const Keys = { stamp: 'securityStamp', }; -export class UserService implements UserServiceInterface { +export class UserService implements UserServiceAbsrtaction { userId: string; email: string; stamp: string; From 8d4799a0a2cf46129b2470cd793476a21f1fd0bd Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 9 Feb 2018 12:08:33 -0500 Subject: [PATCH 0088/1626] password gen fixes --- src/services/passwordGeneration.service.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 33c3eea29a..20d526a405 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -15,9 +15,9 @@ const DefaultOptions = { number: true, minNumber: 1, uppercase: true, - minUppercase: 1, + minUppercase: 0, lowercase: true, - minLowercase: 1, + minLowercase: 0, special: false, minSpecial: 1, }; @@ -35,16 +35,16 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr const o = Object.assign({}, DefaultOptions, options); // sanitize - if (o.uppercase && o.minUppercase < 0) { + if (o.uppercase && o.minUppercase <= 0) { o.minUppercase = 1; } - if (o.lowercase && o.minLowercase < 0) { + if (o.lowercase && o.minLowercase <= 0) { o.minLowercase = 1; } - if (o.number && o.minNumber < 0) { + if (o.number && o.minNumber <= 0) { o.minNumber = 1; } - if (o.special && o.minSpecial < 0) { + if (o.special && o.minSpecial <= 0) { o.minSpecial = 1; } From 6c3f0a538f10f050d0cf70ef89e0f2d900b1e694 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 10 Feb 2018 10:53:07 -0500 Subject: [PATCH 0089/1626] convert more service abstractions --- src/abstractions/lock.service.ts | 6 +++--- src/abstractions/storage.service.ts | 8 ++++---- src/services/lock.service.ts | 13 ++++++------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/abstractions/lock.service.ts b/src/abstractions/lock.service.ts index 2606f617ac..0b025b7039 100644 --- a/src/abstractions/lock.service.ts +++ b/src/abstractions/lock.service.ts @@ -1,4 +1,4 @@ -export interface LockService { - checkLock(): Promise; - lock(): Promise; +export abstract class LockService { + checkLock: () => Promise; + lock: () => Promise; } diff --git a/src/abstractions/storage.service.ts b/src/abstractions/storage.service.ts index 5d070d0a55..02fe9d9818 100644 --- a/src/abstractions/storage.service.ts +++ b/src/abstractions/storage.service.ts @@ -1,5 +1,5 @@ -export interface StorageService { - get(key: string): Promise; - save(key: string, obj: any): Promise; - remove(key: string): Promise; +export abstract class StorageService { + get: (key: string) => Promise; + save: (key: string, obj: any) => Promise; + remove: (key: string) => Promise; } diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts index e3b841a9a3..60d2a98be4 100644 --- a/src/services/lock.service.ts +++ b/src/services/lock.service.ts @@ -4,16 +4,16 @@ import { CipherService } from '../abstractions/cipher.service'; import { CollectionService } from '../abstractions/collection.service'; import { CryptoService } from '../abstractions/crypto.service'; import { FolderService } from '../abstractions/folder.service'; -import { LockService as LockServiceInterface } from '../abstractions/lock.service'; +import { LockService as LockServiceAbstraction } from '../abstractions/lock.service'; +import { MessagingService } from '../abstractions/messaging.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { StorageService } from '../abstractions/storage.service'; -export class LockService implements LockServiceInterface { +export class LockService implements LockServiceAbstraction { constructor(private cipherService: CipherService, private folderService: FolderService, private collectionService: CollectionService, private cryptoService: CryptoService, - private platformUtilsService: PlatformUtilsService, - private storageService: StorageService, - private setIcon: Function, private refreshBadgeAndMenu: Function) { + private platformUtilsService: PlatformUtilsService, private storageService: StorageService, + private messagingService: MessagingService) { this.checkLock(); setInterval(() => this.checkLock(), 10 * 1000); // check every 10 seconds } @@ -54,12 +54,11 @@ export class LockService implements LockServiceInterface { this.cryptoService.clearOrgKeys(true), this.cryptoService.clearPrivateKey(true), this.cryptoService.clearEncKey(true), - this.setIcon(), - this.refreshBadgeAndMenu(), ]); this.folderService.clearCache(); this.cipherService.clearCache(); this.collectionService.clearCache(); + this.messagingService.send('locked'); } } From 4960fa18c03934434481aba250d78e74232184d8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sun, 11 Feb 2018 00:38:17 -0500 Subject: [PATCH 0090/1626] setLockOption --- src/abstractions/lock.service.ts | 1 + src/services/lock.service.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/abstractions/lock.service.ts b/src/abstractions/lock.service.ts index 0b025b7039..448a0a05b0 100644 --- a/src/abstractions/lock.service.ts +++ b/src/abstractions/lock.service.ts @@ -1,4 +1,5 @@ export abstract class LockService { checkLock: () => Promise; lock: () => Promise; + setLockOption: (lockOption: number) => Promise; } diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts index 60d2a98be4..ba54fec1bf 100644 --- a/src/services/lock.service.ts +++ b/src/services/lock.service.ts @@ -61,4 +61,9 @@ export class LockService implements LockServiceAbstraction { this.collectionService.clearCache(); this.messagingService.send('locked'); } + + async setLockOption(lockOption: number): Promise { + await this.storageService.save(ConstantsService.lockOptionKey, lockOption); + await this.cryptoService.toggleKey(); + } } From 56d941a754931e67cc708bdd11bc57e46526e32a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 12 Feb 2018 15:06:18 -0500 Subject: [PATCH 0091/1626] state service --- src/abstractions/index.ts | 1 + src/abstractions/state.service.ts | 6 ++++++ src/services/index.ts | 1 + src/services/state.service.ts | 27 +++++++++++++++++++++++++++ 4 files changed, 35 insertions(+) create mode 100644 src/abstractions/state.service.ts create mode 100644 src/services/state.service.ts diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index 154ef9238f..0d5551e070 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -13,6 +13,7 @@ export { PasswordGenerationService } from './passwordGeneration.service'; export { PlatformUtilsService } from './platformUtils.service'; export { SettingsService } from './settings.service'; export { StorageService } from './storage.service'; +export { StateService } from './state.service'; export { SyncService } from './sync.service'; export { TokenService } from './token.service'; export { TotpService } from './totp.service'; diff --git a/src/abstractions/state.service.ts b/src/abstractions/state.service.ts new file mode 100644 index 0000000000..78658882cd --- /dev/null +++ b/src/abstractions/state.service.ts @@ -0,0 +1,6 @@ +export abstract class StateService { + get: (key: string) => Promise; + save: (key: string, obj: any) => Promise; + remove: (key: string) => Promise; + purge: () => Promise; +} diff --git a/src/services/index.ts b/src/services/index.ts index f66088a65b..c69cafca30 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -11,6 +11,7 @@ export { FolderService } from './folder.service'; export { LockService } from './lock.service'; export { PasswordGenerationService } from './passwordGeneration.service'; export { SettingsService } from './settings.service'; +export { StateService } from './state.service'; export { SyncService } from './sync.service'; export { TokenService } from './token.service'; export { TotpService } from './totp.service'; diff --git a/src/services/state.service.ts b/src/services/state.service.ts new file mode 100644 index 0000000000..03a09e30fc --- /dev/null +++ b/src/services/state.service.ts @@ -0,0 +1,27 @@ +import { StateService as StateServiceAbstraction } from '../abstractions/state.service'; + +export class StateService implements StateServiceAbstraction { + private state: any = {}; + + get(key: string): Promise { + if (this.state.hasOwnProperty(key)) { + return Promise.resolve(this.state[key]); + } + return Promise.resolve(null); + } + + save(key: string, obj: any): Promise { + this.state[key] = obj; + return Promise.resolve(); + } + + remove(key: string): Promise { + delete this.state[key]; + return Promise.resolve(); + } + + purge(): Promise { + this.state = {}; + return Promise.resolve(); + } +} From 61918fbc04041be17e4f8b668ec02e06c9f5a0f9 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 13 Feb 2018 17:23:45 -0500 Subject: [PATCH 0092/1626] isDev method --- src/abstractions/platformUtils.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index 893f99a9d4..bfd955ef2d 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -18,4 +18,5 @@ export abstract class PlatformUtilsService { supportsU2f: (win: Window) => boolean; showDialog: (text: string, title?: string, confirmText?: string, cancelText?: string, type?: string) => Promise; + isDev: () => boolean; } From 4412708ecbe90d5541220c170082066cdd5239e4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 15 Feb 2018 13:39:18 -0500 Subject: [PATCH 0093/1626] send loggedout message --- src/services/auth.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 70e9cae68a..a360dc1c1d 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -98,6 +98,7 @@ export class AuthService { logOut(callback: Function) { callback(); + this.messagingService.send('loggedOut'); } getDefaultTwoFactorProvider(u2fSupported: boolean): TwoFactorProviderType { From 7b3bbd92456159cdee0efae17796717ae3b8d219 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 15 Feb 2018 13:39:23 -0500 Subject: [PATCH 0094/1626] userid null check --- src/services/sync.service.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index f233803c3e..2a001e01a8 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -35,6 +35,10 @@ export class SyncService implements SyncServiceAbstraction { async getLastSync(): Promise { const userId = await this.userService.getUserId(); + if (userId == null) { + return null; + } + const lastSync = await this.storageService.get(Keys.lastSyncPrefix + userId); if (lastSync) { return new Date(lastSync); @@ -45,6 +49,10 @@ export class SyncService implements SyncServiceAbstraction { async setLastSync(date: Date): Promise { const userId = await this.userService.getUserId(); + if (userId == null) { + return; + } + await this.storageService.save(Keys.lastSyncPrefix + userId, date.toJSON()); } From ccf2fbd99643e7e47fc375f252d4df6192a438d0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 16 Feb 2018 14:00:16 -0500 Subject: [PATCH 0095/1626] copyToClipboard device utils --- src/abstractions/platformUtils.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index bfd955ef2d..3835bd062d 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -19,4 +19,5 @@ export abstract class PlatformUtilsService { showDialog: (text: string, title?: string, confirmText?: string, cancelText?: string, type?: string) => Promise; isDev: () => boolean; + copyToClipboard: (text: string, options?: any) => void; } From acb7b7b8e34df5f5b317d7b51c268305414fcf18 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 16 Feb 2018 15:50:01 -0500 Subject: [PATCH 0096/1626] installedVersionKey constant --- src/services/constants.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index 4275d67874..e0c695432c 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -11,6 +11,7 @@ export class ConstantsService { static readonly lockOptionKey: string = 'lockOption'; static readonly lastActiveKey: string = 'lastActive'; static readonly neverDomainsKey: string = 'neverDomains'; + static readonly installedVersionKey: string = 'installedVersion'; readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; readonly disableGaKey: string = ConstantsService.disableGaKey; @@ -22,6 +23,7 @@ export class ConstantsService { readonly lockOptionKey: string = ConstantsService.lockOptionKey; readonly lastActiveKey: string = ConstantsService.lastActiveKey; readonly neverDomainsKey: string = ConstantsService.neverDomainsKey; + readonly installedVersionKey: string = ConstantsService.installedVersionKey; // TODO: Convert these objects to enums readonly encType: any = { From 80cd30d7d8b1566d7ecedcf71d381b0e897280d0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 17 Feb 2018 22:51:28 -0500 Subject: [PATCH 0097/1626] dont init history --- src/services/passwordGeneration.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 20d526a405..09b728c050 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -148,7 +148,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr } private optionsCache: any; - private history: PasswordHistory[] = []; + private history: PasswordHistory[]; constructor(private cryptoService: CryptoService, private storageService: StorageService) { } @@ -203,11 +203,11 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return; } - currentHistory.push(new PasswordHistory(password, Date.now())); + currentHistory.unshift(new PasswordHistory(password, Date.now())); // Remove old items. if (currentHistory.length > MaxPasswordsInHistory) { - currentHistory.shift(); + currentHistory.pop(); } const newHistory = await this.encryptHistory(currentHistory); From 36315dae19914490e8810746b7935d0596d2a3ba Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 19 Feb 2018 12:04:35 -0500 Subject: [PATCH 0098/1626] collection from domain index --- src/services/collection.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index f860f5d9d3..3e7c60c089 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -1,6 +1,6 @@ import { CollectionData } from '../models/data/collectionData'; -import { Collection } from '../models/domain/collection'; +import { Collection } from '../models/domain'; import { CollectionView } from '../models/view/collectionView'; From 2d9f53fbed02b4b6e58f94e2532742a1cc718922 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 19 Feb 2018 12:33:32 -0500 Subject: [PATCH 0099/1626] reference barrels --- src/abstractions/api.service.ts | 28 +++++---- src/abstractions/auth.service.ts | 4 +- src/abstractions/cipher.service.ts | 16 +++-- src/abstractions/collection.service.ts | 6 +- src/abstractions/crypto.service.ts | 8 ++- src/abstractions/folder.service.ts | 6 +- .../passwordGeneration.service.ts | 2 +- src/abstractions/platformUtils.service.ts | 2 +- src/misc/analytics.ts | 8 ++- src/models/data/attachmentData.ts | 2 +- src/models/data/cipherData.ts | 4 +- src/models/data/collectionData.ts | 2 +- src/models/data/fieldData.ts | 2 +- src/models/data/folderData.ts | 2 +- src/models/data/secureNoteData.ts | 2 +- src/models/domain/attachment.ts | 4 +- src/models/domain/authResult.ts | 2 +- src/models/domain/card.ts | 4 +- src/models/domain/cipher.ts | 6 +- src/models/domain/cipherString.ts | 4 +- src/models/domain/collection.ts | 4 +- src/models/domain/domain.ts | 4 +- src/models/domain/field.ts | 6 +- src/models/domain/folder.ts | 4 +- src/models/domain/identity.ts | 4 +- src/models/domain/login.ts | 4 +- src/models/domain/secureNote.ts | 6 +- src/models/domain/symmetricCryptoKey.ts | 2 +- src/models/request/cipherRequest.ts | 2 +- src/models/request/deviceRequest.ts | 4 +- src/models/request/folderRequest.ts | 2 +- src/models/response/deviceResponse.ts | 2 +- .../response/identityTwoFactorResponse.ts | 2 +- src/models/view/attachmentView.ts | 2 +- src/models/view/cardView.ts | 2 +- src/models/view/cipherView.ts | 4 +- src/models/view/collectionView.ts | 2 +- src/models/view/fieldView.ts | 4 +- src/models/view/folderView.ts | 2 +- src/models/view/identityView.ts | 2 +- src/models/view/index.ts | 2 + src/models/view/loginView.ts | 4 +- src/models/view/secureNoteView.ts | 4 +- src/services/api.service.ts | 38 +++++++----- src/services/appId.service.ts | 6 +- src/services/auth.service.ts | 38 +++++++----- src/services/cipher.service.ts | 61 +++++++++++-------- src/services/collection.service.ts | 16 ++--- src/services/constants.service.ts | 2 +- src/services/container.service.ts | 6 +- src/services/crypto.service.ts | 23 ++++--- src/services/environment.service.ts | 10 +-- src/services/folder.service.ts | 24 ++++---- src/services/lock.service.ts | 18 +++--- src/services/passwordGeneration.service.ts | 12 ++-- src/services/settings.service.ts | 8 ++- src/services/state.service.ts | 2 +- src/services/sync.service.ts | 42 +++++++------ src/services/token.service.ts | 6 +- src/services/totp.service.ts | 6 +- src/services/user.service.ts | 8 ++- src/services/utils.service.ts | 2 +- 62 files changed, 293 insertions(+), 223 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 4c7d621c14..0f13f46c73 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -1,17 +1,21 @@ -import { EnvironmentUrls } from '../models/domain/environmentUrls'; +import { EnvironmentUrls } from '../models/domain'; -import { CipherRequest } from '../models/request/cipherRequest'; -import { FolderRequest } from '../models/request/folderRequest'; -import { PasswordHintRequest } from '../models/request/passwordHintRequest'; -import { RegisterRequest } from '../models/request/registerRequest'; -import { TokenRequest } from '../models/request/tokenRequest'; -import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; +import { + CipherRequest, + FolderRequest, + PasswordHintRequest, + RegisterRequest, + TokenRequest, + TwoFactorEmailRequest, +} from '../models/request'; -import { CipherResponse } from '../models/response/cipherResponse'; -import { FolderResponse } from '../models/response/folderResponse'; -import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; -import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; -import { SyncResponse } from '../models/response/syncResponse'; +import { + CipherResponse, + FolderResponse, + IdentityTokenResponse, + IdentityTwoFactorResponse, + SyncResponse, +} from '../models/response'; export abstract class ApiService { urlsSet: boolean; diff --git a/src/abstractions/auth.service.ts b/src/abstractions/auth.service.ts index 08ca102729..0faed28c2d 100644 --- a/src/abstractions/auth.service.ts +++ b/src/abstractions/auth.service.ts @@ -1,6 +1,6 @@ -import { TwoFactorProviderType } from '../enums/twoFactorProviderType'; +import { TwoFactorProviderType } from '../enums'; -import { AuthResult } from '../models/domain/authResult'; +import { AuthResult } from '../models/domain'; export abstract class AuthService { email: string; diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index 330ac71c6c..1df9c72b0f 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -1,11 +1,15 @@ -import { CipherData } from '../models/data/cipherData'; +import { CipherData } from '../models/data'; -import { Cipher } from '../models/domain/cipher'; -import { Field } from '../models/domain/field'; -import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; +import { + Cipher, + Field, + SymmetricCryptoKey, +} from '../models/domain'; -import { CipherView } from '../models/view/cipherView'; -import { FieldView } from '../models/view/fieldView'; +import { + CipherView, + FieldView, +} from '../models/view'; export abstract class CipherService { decryptedCipherCache: CipherView[]; diff --git a/src/abstractions/collection.service.ts b/src/abstractions/collection.service.ts index 6c51478555..1bcec5cdaf 100644 --- a/src/abstractions/collection.service.ts +++ b/src/abstractions/collection.service.ts @@ -1,8 +1,8 @@ -import { CollectionData } from '../models/data/collectionData'; +import { CollectionData } from '../models/data'; -import { Collection } from '../models/domain/collection'; +import { Collection } from '../models/domain'; -import { CollectionView } from '../models/view/collectionView'; +import { CollectionView } from '../models/view'; export abstract class CollectionService { decryptedCollectionCache: CollectionView[]; diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index dbf5f0da75..1d5de5e127 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -1,7 +1,9 @@ -import { CipherString } from '../models/domain/cipherString'; -import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; +import { + CipherString, + SymmetricCryptoKey, +} from '../models/domain'; -import { ProfileOrganizationResponse } from '../models/response/profileOrganizationResponse'; +import { ProfileOrganizationResponse } from '../models/response'; export abstract class CryptoService { setKey: (key: SymmetricCryptoKey) => Promise; diff --git a/src/abstractions/folder.service.ts b/src/abstractions/folder.service.ts index 381bc01317..a6615b2756 100644 --- a/src/abstractions/folder.service.ts +++ b/src/abstractions/folder.service.ts @@ -1,8 +1,8 @@ -import { FolderData } from '../models/data/folderData'; +import { FolderData } from '../models/data'; -import { Folder } from '../models/domain/folder'; +import { Folder } from '../models/domain'; -import { FolderView } from '../models/view/folderView'; +import { FolderView } from '../models/view'; export abstract class FolderService { decryptedFolderCache: FolderView[]; diff --git a/src/abstractions/passwordGeneration.service.ts b/src/abstractions/passwordGeneration.service.ts index a8967db281..36f532403b 100644 --- a/src/abstractions/passwordGeneration.service.ts +++ b/src/abstractions/passwordGeneration.service.ts @@ -1,4 +1,4 @@ -import { PasswordHistory } from '../models/domain/passwordHistory'; +import { PasswordHistory } from '../models/domain'; export abstract class PasswordGenerationService { generatePassword: (options: any) => string; diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index 3835bd062d..99d2795191 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -1,4 +1,4 @@ -import { DeviceType } from '../enums/deviceType'; +import { DeviceType } from '../enums'; export abstract class PlatformUtilsService { getDevice: () => DeviceType; diff --git a/src/misc/analytics.ts b/src/misc/analytics.ts index 98cfdebc5b..78962c66f6 100644 --- a/src/misc/analytics.ts +++ b/src/misc/analytics.ts @@ -1,6 +1,8 @@ -import { AppIdService } from '../abstractions/appId.service'; -import { PlatformUtilsService } from '../abstractions/platformUtils.service'; -import { StorageService } from '../abstractions/storage.service'; +import { + AppIdService, + PlatformUtilsService, + StorageService, +} from '../abstractions'; import { ConstantsService } from '../services/constants.service'; diff --git a/src/models/data/attachmentData.ts b/src/models/data/attachmentData.ts index 85aa452e1e..792eb07bd1 100644 --- a/src/models/data/attachmentData.ts +++ b/src/models/data/attachmentData.ts @@ -1,4 +1,4 @@ -import { AttachmentResponse } from '../response/attachmentResponse'; +import { AttachmentResponse } from '../response'; export class AttachmentData { id: string; diff --git a/src/models/data/cipherData.ts b/src/models/data/cipherData.ts index 0a4f83c520..40468f0a7a 100644 --- a/src/models/data/cipherData.ts +++ b/src/models/data/cipherData.ts @@ -1,4 +1,4 @@ -import { CipherType } from '../../enums/cipherType'; +import { CipherType } from '../../enums'; import { AttachmentData } from './attachmentData'; import { CardData } from './cardData'; @@ -7,7 +7,7 @@ import { IdentityData } from './identityData'; import { LoginData } from './loginData'; import { SecureNoteData } from './secureNoteData'; -import { CipherResponse } from '../response/cipherResponse'; +import { CipherResponse } from '../response'; export class CipherData { id: string; diff --git a/src/models/data/collectionData.ts b/src/models/data/collectionData.ts index 3b05b9af22..16b44871b3 100644 --- a/src/models/data/collectionData.ts +++ b/src/models/data/collectionData.ts @@ -1,4 +1,4 @@ -import { CollectionResponse } from '../response/collectionResponse'; +import { CollectionResponse } from '../response'; export class CollectionData { id: string; diff --git a/src/models/data/fieldData.ts b/src/models/data/fieldData.ts index cd0fb2c42b..3af1d04285 100644 --- a/src/models/data/fieldData.ts +++ b/src/models/data/fieldData.ts @@ -1,4 +1,4 @@ -import { FieldType } from '../../enums/fieldType'; +import { FieldType } from '../../enums'; export class FieldData { type: FieldType; diff --git a/src/models/data/folderData.ts b/src/models/data/folderData.ts index 509267c9d4..c0a4d0dd2f 100644 --- a/src/models/data/folderData.ts +++ b/src/models/data/folderData.ts @@ -1,4 +1,4 @@ -import { FolderResponse } from '../response/folderResponse'; +import { FolderResponse } from '../response'; export class FolderData { id: string; diff --git a/src/models/data/secureNoteData.ts b/src/models/data/secureNoteData.ts index 4b27125a13..0a3d64da9d 100644 --- a/src/models/data/secureNoteData.ts +++ b/src/models/data/secureNoteData.ts @@ -1,4 +1,4 @@ -import { SecureNoteType } from '../../enums/secureNoteType'; +import { SecureNoteType } from '../../enums'; export class SecureNoteData { type: SecureNoteType; diff --git a/src/models/domain/attachment.ts b/src/models/domain/attachment.ts index 7861066e0e..b2f82863c1 100644 --- a/src/models/domain/attachment.ts +++ b/src/models/domain/attachment.ts @@ -1,9 +1,9 @@ -import { AttachmentData } from '../data/attachmentData'; +import { AttachmentData } from '../data'; import { CipherString } from './cipherString'; import Domain from './domain'; -import { AttachmentView } from '../view/attachmentView'; +import { AttachmentView } from '../view'; export class Attachment extends Domain { id: string; diff --git a/src/models/domain/authResult.ts b/src/models/domain/authResult.ts index cb4bb57c65..e797781bbb 100644 --- a/src/models/domain/authResult.ts +++ b/src/models/domain/authResult.ts @@ -1,4 +1,4 @@ -import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; +import { TwoFactorProviderType } from '../../enums'; export class AuthResult { twoFactor: boolean = false; diff --git a/src/models/domain/card.ts b/src/models/domain/card.ts index 19a2ad9e50..a41597ecae 100644 --- a/src/models/domain/card.ts +++ b/src/models/domain/card.ts @@ -1,9 +1,9 @@ -import { CardData } from '../data/cardData'; +import { CardData } from '../data'; import { CipherString } from './cipherString'; import Domain from './domain'; -import { CardView } from '../view/cardView'; +import { CardView } from '../view'; export class Card extends Domain { cardholderName: CipherString; diff --git a/src/models/domain/cipher.ts b/src/models/domain/cipher.ts index 7f9c18d1f0..e591c38c59 100644 --- a/src/models/domain/cipher.ts +++ b/src/models/domain/cipher.ts @@ -1,8 +1,8 @@ -import { CipherType } from '../../enums/cipherType'; +import { CipherType } from '../../enums'; -import { CipherData } from '../data/cipherData'; +import { CipherData } from '../data'; -import { CipherView } from '../view/cipherView'; +import { CipherView } from '../view'; import { Attachment } from './attachment'; import { Card } from './card'; diff --git a/src/models/domain/cipherString.ts b/src/models/domain/cipherString.ts index d19d191065..b6f4bedc08 100644 --- a/src/models/domain/cipherString.ts +++ b/src/models/domain/cipherString.ts @@ -1,6 +1,6 @@ -import { EncryptionType } from '../../enums/encryptionType'; +import { EncryptionType } from '../../enums'; -import { CryptoService } from '../../abstractions/crypto.service'; +import { CryptoService } from '../../abstractions'; export class CipherString { encryptedString?: string; diff --git a/src/models/domain/collection.ts b/src/models/domain/collection.ts index 45fd54cd8c..9c3136e189 100644 --- a/src/models/domain/collection.ts +++ b/src/models/domain/collection.ts @@ -1,6 +1,6 @@ -import { CollectionData } from '../data/collectionData'; +import { CollectionData } from '../data'; -import { CollectionView } from '../view/collectionView'; +import { CollectionView } from '../view'; import { CipherString } from './cipherString'; import Domain from './domain'; diff --git a/src/models/domain/domain.ts b/src/models/domain/domain.ts index 7fe644740f..d75bec902e 100644 --- a/src/models/domain/domain.ts +++ b/src/models/domain/domain.ts @@ -1,6 +1,6 @@ -import { CipherString } from '../domain/cipherString'; +import { CipherString } from './cipherString'; -import { View } from '../view/view'; +import { View } from '../view'; export default abstract class Domain { protected buildDomainModel(domain: D, dataObj: any, map: any, diff --git a/src/models/domain/field.ts b/src/models/domain/field.ts index 023fefe5a9..a527edbcac 100644 --- a/src/models/domain/field.ts +++ b/src/models/domain/field.ts @@ -1,11 +1,11 @@ -import { FieldType } from '../../enums/fieldType'; +import { FieldType } from '../../enums'; -import { FieldData } from '../data/fieldData'; +import { FieldData } from '../data'; import { CipherString } from './cipherString'; import Domain from './domain'; -import { FieldView } from '../view/fieldView'; +import { FieldView } from '../view'; export class Field extends Domain { name: CipherString; diff --git a/src/models/domain/folder.ts b/src/models/domain/folder.ts index 77b9fb310d..80c5057533 100644 --- a/src/models/domain/folder.ts +++ b/src/models/domain/folder.ts @@ -1,6 +1,6 @@ -import { FolderData } from '../data/folderData'; +import { FolderData } from '../data'; -import { FolderView } from '../view/folderView'; +import { FolderView } from '../view'; import { CipherString } from './cipherString'; import Domain from './domain'; diff --git a/src/models/domain/identity.ts b/src/models/domain/identity.ts index ade03260ea..7ef205a376 100644 --- a/src/models/domain/identity.ts +++ b/src/models/domain/identity.ts @@ -1,9 +1,9 @@ -import { IdentityData } from '../data/identityData'; +import { IdentityData } from '../data'; import { CipherString } from './cipherString'; import Domain from './domain'; -import { IdentityView } from '../view/identityView'; +import { IdentityView } from '../view'; export class Identity extends Domain { title: CipherString; diff --git a/src/models/domain/login.ts b/src/models/domain/login.ts index 1c98dc7031..743b54ae3a 100644 --- a/src/models/domain/login.ts +++ b/src/models/domain/login.ts @@ -1,6 +1,6 @@ -import { LoginData } from '../data/loginData'; +import { LoginData } from '../data'; -import { LoginView } from '../view/loginView'; +import { LoginView } from '../view'; import { CipherString } from './cipherString'; import Domain from './domain'; diff --git a/src/models/domain/secureNote.ts b/src/models/domain/secureNote.ts index 79724a4193..33640e4794 100644 --- a/src/models/domain/secureNote.ts +++ b/src/models/domain/secureNote.ts @@ -1,10 +1,10 @@ -import { SecureNoteType } from '../../enums/secureNoteType'; +import { SecureNoteType } from '../../enums'; -import { SecureNoteData } from '../data/secureNoteData'; +import { SecureNoteData } from '../data'; import Domain from './domain'; -import { SecureNoteView } from '../view/secureNoteView'; +import { SecureNoteView } from '../view'; export class SecureNote extends Domain { type: SecureNoteType; diff --git a/src/models/domain/symmetricCryptoKey.ts b/src/models/domain/symmetricCryptoKey.ts index 4e3a5d6c2d..4b12f98b07 100644 --- a/src/models/domain/symmetricCryptoKey.ts +++ b/src/models/domain/symmetricCryptoKey.ts @@ -1,6 +1,6 @@ import * as forge from 'node-forge'; -import { EncryptionType } from '../../enums/encryptionType'; +import { EncryptionType } from '../../enums'; import { SymmetricCryptoKeyBuffers } from './symmetricCryptoKeyBuffers'; diff --git a/src/models/request/cipherRequest.ts b/src/models/request/cipherRequest.ts index 232883f323..ff2a79d972 100644 --- a/src/models/request/cipherRequest.ts +++ b/src/models/request/cipherRequest.ts @@ -1,4 +1,4 @@ -import { CipherType } from '../../enums/cipherType'; +import { CipherType } from '../../enums'; export class CipherRequest { type: CipherType; diff --git a/src/models/request/deviceRequest.ts b/src/models/request/deviceRequest.ts index 2aaa42cbe0..4b29b44383 100644 --- a/src/models/request/deviceRequest.ts +++ b/src/models/request/deviceRequest.ts @@ -1,6 +1,6 @@ -import { DeviceType } from '../../enums/deviceType'; +import { DeviceType } from '../../enums'; -import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { PlatformUtilsService } from '../../abstractions'; export class DeviceRequest { type: DeviceType; diff --git a/src/models/request/folderRequest.ts b/src/models/request/folderRequest.ts index 54ec76cac5..63d53819b1 100644 --- a/src/models/request/folderRequest.ts +++ b/src/models/request/folderRequest.ts @@ -1,4 +1,4 @@ -import { Folder } from '../domain/folder'; +import { Folder } from '../domain'; export class FolderRequest { name: string; diff --git a/src/models/response/deviceResponse.ts b/src/models/response/deviceResponse.ts index 23d0135b76..9fe3d37ece 100644 --- a/src/models/response/deviceResponse.ts +++ b/src/models/response/deviceResponse.ts @@ -1,4 +1,4 @@ -import { DeviceType } from '../../enums/deviceType'; +import { DeviceType } from '../../enums'; export class DeviceResponse { id: string; diff --git a/src/models/response/identityTwoFactorResponse.ts b/src/models/response/identityTwoFactorResponse.ts index 2adeeaa053..b01b83fb58 100644 --- a/src/models/response/identityTwoFactorResponse.ts +++ b/src/models/response/identityTwoFactorResponse.ts @@ -1,4 +1,4 @@ -import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; +import { TwoFactorProviderType } from '../../enums'; export class IdentityTwoFactorResponse { twoFactorProviders: TwoFactorProviderType[]; diff --git a/src/models/view/attachmentView.ts b/src/models/view/attachmentView.ts index 49ea57326d..3fc9c39ff9 100644 --- a/src/models/view/attachmentView.ts +++ b/src/models/view/attachmentView.ts @@ -1,6 +1,6 @@ import { View } from './view'; -import { Attachment } from '../domain/attachment'; +import { Attachment } from '../domain'; export class AttachmentView implements View { id: string; diff --git a/src/models/view/cardView.ts b/src/models/view/cardView.ts index 26e3308e31..7149395630 100644 --- a/src/models/view/cardView.ts +++ b/src/models/view/cardView.ts @@ -1,6 +1,6 @@ import { View } from './view'; -import { Card } from '../domain/card'; +import { Card } from '../domain'; export class CardView implements View { cardholderName: string; diff --git a/src/models/view/cipherView.ts b/src/models/view/cipherView.ts index b3f232bcbc..aeb7e43cd7 100644 --- a/src/models/view/cipherView.ts +++ b/src/models/view/cipherView.ts @@ -1,6 +1,6 @@ -import { CipherType } from '../../enums/cipherType'; +import { CipherType } from '../../enums'; -import { Cipher } from '../domain/cipher'; +import { Cipher } from '../domain'; import { AttachmentView } from './attachmentView'; import { CardView } from './cardView'; diff --git a/src/models/view/collectionView.ts b/src/models/view/collectionView.ts index ab184a2c35..6cdc8a1791 100644 --- a/src/models/view/collectionView.ts +++ b/src/models/view/collectionView.ts @@ -1,6 +1,6 @@ import { View } from './view'; -import { Collection } from '../domain/collection'; +import { Collection } from '../domain'; export class CollectionView implements View { id: string; diff --git a/src/models/view/fieldView.ts b/src/models/view/fieldView.ts index b3f68a584f..8e67ff6dc8 100644 --- a/src/models/view/fieldView.ts +++ b/src/models/view/fieldView.ts @@ -1,8 +1,8 @@ -import { FieldType } from '../../enums/fieldType'; +import { FieldType } from '../../enums'; import { View } from './view'; -import { Field } from '../domain/field'; +import { Field } from '../domain'; export class FieldView implements View { name: string; diff --git a/src/models/view/folderView.ts b/src/models/view/folderView.ts index ebe8ea157a..8f2cfe3e2c 100644 --- a/src/models/view/folderView.ts +++ b/src/models/view/folderView.ts @@ -1,6 +1,6 @@ import { View } from './view'; -import { Folder } from '../domain/folder'; +import { Folder } from '../domain'; export class FolderView implements View { id: string = null; diff --git a/src/models/view/identityView.ts b/src/models/view/identityView.ts index 4ae5fa0cf9..6843e1e3ac 100644 --- a/src/models/view/identityView.ts +++ b/src/models/view/identityView.ts @@ -1,6 +1,6 @@ import { View } from './view'; -import { Identity } from '../domain/identity'; +import { Identity } from '../domain'; export class IdentityView implements View { title: string = null; diff --git a/src/models/view/index.ts b/src/models/view/index.ts index 0e07c256be..eb1b943c82 100644 --- a/src/models/view/index.ts +++ b/src/models/view/index.ts @@ -1,7 +1,9 @@ export { AttachmentView } from './attachmentView'; export { CardView } from './cardView'; export { CipherView } from './cipherView'; +export { CollectionView } from './collectionView'; export { FieldView } from './fieldView'; +export { FolderView } from './folderView'; export { IdentityView } from './identityView'; export { LoginView } from './loginView'; export { SecureNoteView } from './secureNoteView'; diff --git a/src/models/view/loginView.ts b/src/models/view/loginView.ts index c3554c9c38..6c97772b76 100644 --- a/src/models/view/loginView.ts +++ b/src/models/view/loginView.ts @@ -1,8 +1,8 @@ import { View } from './view'; -import { Login } from '../domain/login'; +import { Login } from '../domain'; -import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { PlatformUtilsService } from '../../abstractions'; export class LoginView implements View { username: string; diff --git a/src/models/view/secureNoteView.ts b/src/models/view/secureNoteView.ts index 1047c41086..d6c2867dfa 100644 --- a/src/models/view/secureNoteView.ts +++ b/src/models/view/secureNoteView.ts @@ -1,8 +1,8 @@ -import { SecureNoteType } from '../../enums/secureNoteType'; +import { SecureNoteType } from '../../enums'; import { View } from './view'; -import { SecureNote } from '../domain/secureNote'; +import { SecureNote } from '../domain'; export class SecureNoteView implements View { type: SecureNoteType; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 1e452af755..395b411794 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -1,24 +1,30 @@ import { ConstantsService } from './constants.service'; -import { ApiService as ApiServiceAbstraction } from '../abstractions/api.service'; -import { PlatformUtilsService } from '../abstractions/platformUtils.service'; -import { TokenService } from '../abstractions/token.service'; +import { + ApiService as ApiServiceAbstraction, + PlatformUtilsService, + TokenService, +} from '../abstractions'; -import { EnvironmentUrls } from '../models/domain/environmentUrls'; +import { EnvironmentUrls } from '../models/domain'; -import { CipherRequest } from '../models/request/cipherRequest'; -import { FolderRequest } from '../models/request/folderRequest'; -import { PasswordHintRequest } from '../models/request/passwordHintRequest'; -import { RegisterRequest } from '../models/request/registerRequest'; -import { TokenRequest } from '../models/request/tokenRequest'; -import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; +import { + CipherRequest, + FolderRequest, + PasswordHintRequest, + RegisterRequest, + TokenRequest, + TwoFactorEmailRequest, +} from '../models/request'; -import { CipherResponse } from '../models/response/cipherResponse'; -import { ErrorResponse } from '../models/response/errorResponse'; -import { FolderResponse } from '../models/response/folderResponse'; -import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; -import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; -import { SyncResponse } from '../models/response/syncResponse'; +import { + CipherResponse, + ErrorResponse, + FolderResponse, + IdentityTokenResponse, + IdentityTwoFactorResponse, + SyncResponse, +} from '../models/response'; export class ApiService implements ApiServiceAbstraction { urlsSet: boolean = false; diff --git a/src/services/appId.service.ts b/src/services/appId.service.ts index 430a375571..50778bf8ea 100644 --- a/src/services/appId.service.ts +++ b/src/services/appId.service.ts @@ -1,7 +1,9 @@ import { UtilsService } from './utils.service'; -import { AppIdService as AppIdServiceInterface } from '../abstractions/appId.service'; -import { StorageService } from '../abstractions/storage.service'; +import { + AppIdService as AppIdServiceInterface, + StorageService, +} from '../abstractions'; export class AppIdService implements AppIdServiceInterface { constructor(private storageService: StorageService) { diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index a360dc1c1d..101a53e4f6 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,24 +1,32 @@ -import { TwoFactorProviderType } from '../enums/twoFactorProviderType'; +import { TwoFactorProviderType } from '../enums'; -import { AuthResult } from '../models/domain/authResult'; -import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; +import { + AuthResult, + SymmetricCryptoKey, +} from '../models/domain'; -import { DeviceRequest } from '../models/request/deviceRequest'; -import { TokenRequest } from '../models/request/tokenRequest'; +import { + DeviceRequest, + TokenRequest, +} from '../models/request'; -import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; -import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; +import { + IdentityTokenResponse, + IdentityTwoFactorResponse, +} from '../models/response'; import { ConstantsService } from '../services/constants.service'; -import { ApiService } from '../abstractions/api.service'; -import { AppIdService } from '../abstractions/appId.service'; -import { CryptoService } from '../abstractions/crypto.service'; -import { I18nService } from '../abstractions/i18n.service'; -import { MessagingService } from '../abstractions/messaging.service'; -import { PlatformUtilsService } from '../abstractions/platformUtils.service'; -import { TokenService } from '../abstractions/token.service'; -import { UserService } from '../abstractions/user.service'; +import { + ApiService, + AppIdService, + CryptoService, + I18nService, + MessagingService, + PlatformUtilsService, + TokenService, + UserService, +} from '../abstractions'; export const TwoFactorProviders = { [TwoFactorProviderType.Authenticator]: { diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index fb8e532d62..9818eab438 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -1,38 +1,47 @@ -import { CipherType } from '../enums/cipherType'; +import { CipherType } from '../enums'; -import { CipherData } from '../models/data/cipherData'; +import { CipherData } from '../models/data'; + +import { + Card, + Cipher, + CipherString, + Field, + Identity, + Login, + SecureNote, + SymmetricCryptoKey, +} from '../models/domain'; -import { Card } from '../models/domain/card'; -import { Cipher } from '../models/domain/cipher'; -import { CipherString } from '../models/domain/cipherString'; import Domain from '../models/domain/domain'; -import { Field } from '../models/domain/field'; -import { Identity } from '../models/domain/identity'; -import { Login } from '../models/domain/login'; -import { SecureNote } from '../models/domain/secureNote'; -import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; -import { CipherRequest } from '../models/request/cipherRequest'; +import { CipherRequest } from '../models/request'; -import { CipherResponse } from '../models/response/cipherResponse'; -import { ErrorResponse } from '../models/response/errorResponse'; +import { + CipherResponse, + ErrorResponse, +} from '../models/response'; -import { CardView } from '../models/view/cardView'; -import { CipherView } from '../models/view/cipherView'; -import { FieldView } from '../models/view/fieldView'; -import { IdentityView } from '../models/view/identityView'; -import { LoginView } from '../models/view/loginView'; -import { View } from '../models/view/view'; +import { + CardView, + CipherView, + FieldView, + IdentityView, + LoginView, + View, +} from '../models/view'; import { ConstantsService } from './constants.service'; -import { ApiService } from '../abstractions/api.service'; -import { CipherService as CipherServiceAbstraction } from '../abstractions/cipher.service'; -import { CryptoService } from '../abstractions/crypto.service'; -import { I18nService } from '../abstractions/i18n.service'; -import { SettingsService } from '../abstractions/settings.service'; -import { StorageService } from '../abstractions/storage.service'; -import { UserService } from '../abstractions/user.service'; +import { + ApiService, + CipherService as CipherServiceAbstraction, + CryptoService, + I18nService, + SettingsService, + StorageService, + UserService, +} from '../abstractions'; const Keys = { ciphersPrefix: 'ciphers_', diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index 3e7c60c089..58b54be274 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -1,14 +1,16 @@ -import { CollectionData } from '../models/data/collectionData'; +import { CollectionData } from '../models/data'; import { Collection } from '../models/domain'; -import { CollectionView } from '../models/view/collectionView'; +import { CollectionView } from '../models/view'; -import { CollectionService as CollectionServiceAbstraction } from '../abstractions/collection.service'; -import { CryptoService } from '../abstractions/crypto.service'; -import { I18nService } from '../abstractions/i18n.service'; -import { StorageService } from '../abstractions/storage.service'; -import { UserService } from '../abstractions/user.service'; +import { + CollectionService as CollectionServiceAbstraction, + CryptoService, + I18nService, + StorageService, + UserService, +} from '../abstractions'; const Keys = { collectionsPrefix: 'collections_', diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index e0c695432c..0210d9faed 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -1,4 +1,4 @@ -import { PlatformUtilsService } from '../abstractions/platformUtils.service'; +import { PlatformUtilsService } from '../abstractions'; export class ConstantsService { static readonly environmentUrlsKey: string = 'environmentUrls'; diff --git a/src/services/container.service.ts b/src/services/container.service.ts index fa9a6628ce..3659eacdfa 100644 --- a/src/services/container.service.ts +++ b/src/services/container.service.ts @@ -1,5 +1,7 @@ -import { CryptoService } from '../abstractions/crypto.service'; -import { PlatformUtilsService } from '../abstractions/platformUtils.service'; +import { + CryptoService, + PlatformUtilsService, +} from '../abstractions'; export class ContainerService { constructor(private cryptoService: CryptoService, diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index e6ad642574..a25f1786fe 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -1,14 +1,19 @@ import * as forge from 'node-forge'; -import { EncryptionType } from '../enums/encryptionType'; +import { EncryptionType } from '../enums'; -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 { + CipherString, + EncryptedObject, + SymmetricCryptoKey, +} from '../models/domain'; -import { CryptoService as CryptoServiceAbstraction } from '../abstractions/crypto.service'; -import { StorageService as StorageServiceInterface } from '../abstractions/storage.service'; +import { ProfileOrganizationResponse } from '../models/response'; + +import { + CryptoService as CryptoServiceAbstraction, + StorageService as StorageServiceAbstraction, +} from '../abstractions'; import { ConstantsService } from './constants.service'; import { UtilsService } from './utils.service'; @@ -41,8 +46,8 @@ export class CryptoService implements CryptoServiceAbstraction { private privateKey: ArrayBuffer; private orgKeys: Map; - constructor(private storageService: StorageServiceInterface, - private secureStorageService: StorageServiceInterface) { + constructor(private storageService: StorageServiceAbstraction, + private secureStorageService: StorageServiceAbstraction) { } async setKey(key: SymmetricCryptoKey): Promise { diff --git a/src/services/environment.service.ts b/src/services/environment.service.ts index e1544489f1..d9bab748a0 100644 --- a/src/services/environment.service.ts +++ b/src/services/environment.service.ts @@ -1,10 +1,12 @@ -import { EnvironmentUrls } from '../models/domain/environmentUrls'; +import { EnvironmentUrls } from '../models/domain'; import { ConstantsService } from './constants.service'; -import { ApiService } from '../abstractions/api.service'; -import { EnvironmentService as EnvironmentServiceAbstraction } from '../abstractions/environment.service'; -import { StorageService } from '../abstractions/storage.service'; +import { + ApiService, + EnvironmentService as EnvironmentServiceAbstraction, + StorageService, +} from '../abstractions'; export class EnvironmentService implements EnvironmentServiceAbstraction { baseUrl: string; diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index 81c03a15f5..a8f1769874 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -1,19 +1,21 @@ -import { FolderData } from '../models/data/folderData'; +import { FolderData } from '../models/data'; -import { Folder } from '../models/domain/folder'; +import { Folder } from '../models/domain'; -import { FolderRequest } from '../models/request/folderRequest'; +import { FolderRequest } from '../models/request'; -import { FolderResponse } from '../models/response/folderResponse'; +import { FolderResponse } from '../models/response'; -import { FolderView } from '../models/view/folderView'; +import { FolderView } from '../models/view'; -import { ApiService } from '../abstractions/api.service'; -import { CryptoService } from '../abstractions/crypto.service'; -import { FolderService as FolderServiceAbstraction } from '../abstractions/folder.service'; -import { I18nService } from '../abstractions/i18n.service'; -import { StorageService } from '../abstractions/storage.service'; -import { UserService } from '../abstractions/user.service'; +import { + ApiService, + CryptoService, + FolderService as FolderServiceAbstraction, + I18nService, + StorageService, + UserService, +} from '../abstractions'; const Keys = { foldersPrefix: 'folders_', diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts index ba54fec1bf..db24c60219 100644 --- a/src/services/lock.service.ts +++ b/src/services/lock.service.ts @@ -1,13 +1,15 @@ import { ConstantsService } from './constants.service'; -import { CipherService } from '../abstractions/cipher.service'; -import { CollectionService } from '../abstractions/collection.service'; -import { CryptoService } from '../abstractions/crypto.service'; -import { FolderService } from '../abstractions/folder.service'; -import { LockService as LockServiceAbstraction } from '../abstractions/lock.service'; -import { MessagingService } from '../abstractions/messaging.service'; -import { PlatformUtilsService } from '../abstractions/platformUtils.service'; -import { StorageService } from '../abstractions/storage.service'; +import { + CipherService, + CollectionService, + CryptoService, + FolderService, + LockService as LockServiceAbstraction, + MessagingService, + PlatformUtilsService, + StorageService, +} from '../abstractions'; export class LockService implements LockServiceAbstraction { constructor(private cipherService: CipherService, private folderService: FolderService, diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 09b728c050..40cc3705c7 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -1,13 +1,15 @@ -import { CipherString } from '../models/domain/cipherString'; -import { PasswordHistory } from '../models/domain/passwordHistory'; +import { + CipherString, + PasswordHistory, +} from '../models/domain'; import { UtilsService } from './utils.service'; -import { CryptoService } from '../abstractions/crypto.service'; import { + CryptoService, PasswordGenerationService as PasswordGenerationServiceAbstraction, -} from '../abstractions/passwordGeneration.service'; -import { StorageService } from '../abstractions/storage.service'; + StorageService, +} from '../abstractions'; const DefaultOptions = { length: 14, diff --git a/src/services/settings.service.ts b/src/services/settings.service.ts index 506bdf01a0..cd9ee2e66e 100644 --- a/src/services/settings.service.ts +++ b/src/services/settings.service.ts @@ -1,6 +1,8 @@ -import { SettingsService as SettingsServiceAbstraction } from '../abstractions/settings.service'; -import { StorageService } from '../abstractions/storage.service'; -import { UserService } from '../abstractions/user.service'; +import { + SettingsService as SettingsServiceAbstraction, + StorageService, + UserService, +} from '../abstractions'; const Keys = { settingsPrefix: 'settings_', diff --git a/src/services/state.service.ts b/src/services/state.service.ts index 03a09e30fc..d8b845c010 100644 --- a/src/services/state.service.ts +++ b/src/services/state.service.ts @@ -1,4 +1,4 @@ -import { StateService as StateServiceAbstraction } from '../abstractions/state.service'; +import { StateService as StateServiceAbstraction } from '../abstractions'; export class StateService implements StateServiceAbstraction { private state: any = {}; diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index 2a001e01a8..fdcd16f493 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -1,23 +1,29 @@ -import { ApiService } from '../abstractions/api.service'; -import { CipherService } from '../abstractions/cipher.service'; -import { CollectionService } from '../abstractions/collection.service'; -import { CryptoService } from '../abstractions/crypto.service'; -import { FolderService } from '../abstractions/folder.service'; -import { MessagingService } from '../abstractions/messaging.service'; -import { SettingsService } from '../abstractions/settings.service'; -import { StorageService } from '../abstractions/storage.service'; -import { SyncService as SyncServiceAbstraction } from '../abstractions/sync.service'; -import { UserService } from '../abstractions/user.service'; +import { + ApiService, + CipherService, + CollectionService, + CryptoService, + FolderService, + MessagingService, + SettingsService, + StorageService, + SyncService as SyncServiceAbstraction, + UserService, +} from '../abstractions'; -import { CipherData } from '../models/data/cipherData'; -import { CollectionData } from '../models/data/collectionData'; -import { FolderData } from '../models/data/folderData'; +import { + CipherData, + CollectionData, + FolderData, +} from '../models/data'; -import { CipherResponse } from '../models/response/cipherResponse'; -import { CollectionResponse } from '../models/response/collectionResponse'; -import { DomainsResponse } from '../models/response/domainsResponse'; -import { FolderResponse } from '../models/response/folderResponse'; -import { ProfileResponse } from '../models/response/profileResponse'; +import { + CipherResponse, + CollectionResponse, + DomainsResponse, + FolderResponse, + ProfileResponse, +} from '../models/response'; const Keys = { lastSyncPrefix: 'lastSync_', diff --git a/src/services/token.service.ts b/src/services/token.service.ts index 94dbfa10fd..694c2bf78c 100644 --- a/src/services/token.service.ts +++ b/src/services/token.service.ts @@ -1,8 +1,10 @@ import { ConstantsService } from './constants.service'; import { UtilsService } from './utils.service'; -import { StorageService } from '../abstractions/storage.service'; -import { TokenService as TokenServiceAbstraction } from '../abstractions/token.service'; +import { + StorageService, + TokenService as TokenServiceAbstraction, +} from '../abstractions'; const Keys = { accessToken: 'accessToken', diff --git a/src/services/totp.service.ts b/src/services/totp.service.ts index e52c11e48c..1bc57b4be8 100644 --- a/src/services/totp.service.ts +++ b/src/services/totp.service.ts @@ -1,7 +1,9 @@ import { ConstantsService } from './constants.service'; -import { StorageService } from '../abstractions/storage.service'; -import { TotpService as TotpServiceAbstraction } from '../abstractions/totp.service'; +import { + StorageService, + TotpService as TotpServiceAbstraction, +} from '../abstractions'; const b32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 1949cadb48..5fefef16ce 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -1,6 +1,8 @@ -import { StorageService } from '../abstractions/storage.service'; -import { TokenService } from '../abstractions/token.service'; -import { UserService as UserServiceAbsrtaction } from '../abstractions/user.service'; +import { + StorageService, + TokenService, + UserService as UserServiceAbsrtaction, +} from '../abstractions'; const Keys = { userId: 'userId', diff --git a/src/services/utils.service.ts b/src/services/utils.service.ts index 359a743a91..7a58df6a5a 100644 --- a/src/services/utils.service.ts +++ b/src/services/utils.service.ts @@ -1,4 +1,4 @@ -import { UtilsService as UtilsServiceAbstraction } from '../abstractions/utils.service'; +import { UtilsService as UtilsServiceAbstraction } from '../abstractions'; export class UtilsService implements UtilsServiceAbstraction { static copyToClipboard(text: string, doc?: Document): void { From 166ed44392dfa91a423c09e22e9af77bd036a96b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 19 Feb 2018 13:07:19 -0500 Subject: [PATCH 0100/1626] Revert "reference barrels" This reverts commit 2d9f53fbed02b4b6e58f94e2532742a1cc718922. --- src/abstractions/api.service.ts | 28 ++++----- src/abstractions/auth.service.ts | 4 +- src/abstractions/cipher.service.ts | 16 ++--- src/abstractions/collection.service.ts | 6 +- src/abstractions/crypto.service.ts | 8 +-- src/abstractions/folder.service.ts | 6 +- .../passwordGeneration.service.ts | 2 +- src/abstractions/platformUtils.service.ts | 2 +- src/misc/analytics.ts | 8 +-- src/models/data/attachmentData.ts | 2 +- src/models/data/cipherData.ts | 4 +- src/models/data/collectionData.ts | 2 +- src/models/data/fieldData.ts | 2 +- src/models/data/folderData.ts | 2 +- src/models/data/secureNoteData.ts | 2 +- src/models/domain/attachment.ts | 4 +- src/models/domain/authResult.ts | 2 +- src/models/domain/card.ts | 4 +- src/models/domain/cipher.ts | 6 +- src/models/domain/cipherString.ts | 4 +- src/models/domain/collection.ts | 4 +- src/models/domain/domain.ts | 4 +- src/models/domain/field.ts | 6 +- src/models/domain/folder.ts | 4 +- src/models/domain/identity.ts | 4 +- src/models/domain/login.ts | 4 +- src/models/domain/secureNote.ts | 6 +- src/models/domain/symmetricCryptoKey.ts | 2 +- src/models/request/cipherRequest.ts | 2 +- src/models/request/deviceRequest.ts | 4 +- src/models/request/folderRequest.ts | 2 +- src/models/response/deviceResponse.ts | 2 +- .../response/identityTwoFactorResponse.ts | 2 +- src/models/view/attachmentView.ts | 2 +- src/models/view/cardView.ts | 2 +- src/models/view/cipherView.ts | 4 +- src/models/view/collectionView.ts | 2 +- src/models/view/fieldView.ts | 4 +- src/models/view/folderView.ts | 2 +- src/models/view/identityView.ts | 2 +- src/models/view/index.ts | 2 - src/models/view/loginView.ts | 4 +- src/models/view/secureNoteView.ts | 4 +- src/services/api.service.ts | 38 +++++------- src/services/appId.service.ts | 6 +- src/services/auth.service.ts | 38 +++++------- src/services/cipher.service.ts | 61 ++++++++----------- src/services/collection.service.ts | 16 +++-- src/services/constants.service.ts | 2 +- src/services/container.service.ts | 6 +- src/services/crypto.service.ts | 23 +++---- src/services/environment.service.ts | 10 ++- src/services/folder.service.ts | 24 ++++---- src/services/lock.service.ts | 18 +++--- src/services/passwordGeneration.service.ts | 12 ++-- src/services/settings.service.ts | 8 +-- src/services/state.service.ts | 2 +- src/services/sync.service.ts | 42 ++++++------- src/services/token.service.ts | 6 +- src/services/totp.service.ts | 6 +- src/services/user.service.ts | 8 +-- src/services/utils.service.ts | 2 +- 62 files changed, 223 insertions(+), 293 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 0f13f46c73..4c7d621c14 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -1,21 +1,17 @@ -import { EnvironmentUrls } from '../models/domain'; +import { EnvironmentUrls } from '../models/domain/environmentUrls'; -import { - CipherRequest, - FolderRequest, - PasswordHintRequest, - RegisterRequest, - TokenRequest, - TwoFactorEmailRequest, -} from '../models/request'; +import { CipherRequest } from '../models/request/cipherRequest'; +import { FolderRequest } from '../models/request/folderRequest'; +import { PasswordHintRequest } from '../models/request/passwordHintRequest'; +import { RegisterRequest } from '../models/request/registerRequest'; +import { TokenRequest } from '../models/request/tokenRequest'; +import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; -import { - CipherResponse, - FolderResponse, - IdentityTokenResponse, - IdentityTwoFactorResponse, - SyncResponse, -} from '../models/response'; +import { CipherResponse } from '../models/response/cipherResponse'; +import { FolderResponse } from '../models/response/folderResponse'; +import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; +import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; +import { SyncResponse } from '../models/response/syncResponse'; export abstract class ApiService { urlsSet: boolean; diff --git a/src/abstractions/auth.service.ts b/src/abstractions/auth.service.ts index 0faed28c2d..08ca102729 100644 --- a/src/abstractions/auth.service.ts +++ b/src/abstractions/auth.service.ts @@ -1,6 +1,6 @@ -import { TwoFactorProviderType } from '../enums'; +import { TwoFactorProviderType } from '../enums/twoFactorProviderType'; -import { AuthResult } from '../models/domain'; +import { AuthResult } from '../models/domain/authResult'; export abstract class AuthService { email: string; diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index 1df9c72b0f..330ac71c6c 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -1,15 +1,11 @@ -import { CipherData } from '../models/data'; +import { CipherData } from '../models/data/cipherData'; -import { - Cipher, - Field, - SymmetricCryptoKey, -} from '../models/domain'; +import { Cipher } from '../models/domain/cipher'; +import { Field } from '../models/domain/field'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; -import { - CipherView, - FieldView, -} from '../models/view'; +import { CipherView } from '../models/view/cipherView'; +import { FieldView } from '../models/view/fieldView'; export abstract class CipherService { decryptedCipherCache: CipherView[]; diff --git a/src/abstractions/collection.service.ts b/src/abstractions/collection.service.ts index 1bcec5cdaf..6c51478555 100644 --- a/src/abstractions/collection.service.ts +++ b/src/abstractions/collection.service.ts @@ -1,8 +1,8 @@ -import { CollectionData } from '../models/data'; +import { CollectionData } from '../models/data/collectionData'; -import { Collection } from '../models/domain'; +import { Collection } from '../models/domain/collection'; -import { CollectionView } from '../models/view'; +import { CollectionView } from '../models/view/collectionView'; export abstract class CollectionService { decryptedCollectionCache: CollectionView[]; diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 1d5de5e127..dbf5f0da75 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -1,9 +1,7 @@ -import { - CipherString, - SymmetricCryptoKey, -} from '../models/domain'; +import { CipherString } from '../models/domain/cipherString'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; -import { ProfileOrganizationResponse } from '../models/response'; +import { ProfileOrganizationResponse } from '../models/response/profileOrganizationResponse'; export abstract class CryptoService { setKey: (key: SymmetricCryptoKey) => Promise; diff --git a/src/abstractions/folder.service.ts b/src/abstractions/folder.service.ts index a6615b2756..381bc01317 100644 --- a/src/abstractions/folder.service.ts +++ b/src/abstractions/folder.service.ts @@ -1,8 +1,8 @@ -import { FolderData } from '../models/data'; +import { FolderData } from '../models/data/folderData'; -import { Folder } from '../models/domain'; +import { Folder } from '../models/domain/folder'; -import { FolderView } from '../models/view'; +import { FolderView } from '../models/view/folderView'; export abstract class FolderService { decryptedFolderCache: FolderView[]; diff --git a/src/abstractions/passwordGeneration.service.ts b/src/abstractions/passwordGeneration.service.ts index 36f532403b..a8967db281 100644 --- a/src/abstractions/passwordGeneration.service.ts +++ b/src/abstractions/passwordGeneration.service.ts @@ -1,4 +1,4 @@ -import { PasswordHistory } from '../models/domain'; +import { PasswordHistory } from '../models/domain/passwordHistory'; export abstract class PasswordGenerationService { generatePassword: (options: any) => string; diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index 99d2795191..3835bd062d 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -1,4 +1,4 @@ -import { DeviceType } from '../enums'; +import { DeviceType } from '../enums/deviceType'; export abstract class PlatformUtilsService { getDevice: () => DeviceType; diff --git a/src/misc/analytics.ts b/src/misc/analytics.ts index 78962c66f6..98cfdebc5b 100644 --- a/src/misc/analytics.ts +++ b/src/misc/analytics.ts @@ -1,8 +1,6 @@ -import { - AppIdService, - PlatformUtilsService, - StorageService, -} from '../abstractions'; +import { AppIdService } from '../abstractions/appId.service'; +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; +import { StorageService } from '../abstractions/storage.service'; import { ConstantsService } from '../services/constants.service'; diff --git a/src/models/data/attachmentData.ts b/src/models/data/attachmentData.ts index 792eb07bd1..85aa452e1e 100644 --- a/src/models/data/attachmentData.ts +++ b/src/models/data/attachmentData.ts @@ -1,4 +1,4 @@ -import { AttachmentResponse } from '../response'; +import { AttachmentResponse } from '../response/attachmentResponse'; export class AttachmentData { id: string; diff --git a/src/models/data/cipherData.ts b/src/models/data/cipherData.ts index 40468f0a7a..0a4f83c520 100644 --- a/src/models/data/cipherData.ts +++ b/src/models/data/cipherData.ts @@ -1,4 +1,4 @@ -import { CipherType } from '../../enums'; +import { CipherType } from '../../enums/cipherType'; import { AttachmentData } from './attachmentData'; import { CardData } from './cardData'; @@ -7,7 +7,7 @@ import { IdentityData } from './identityData'; import { LoginData } from './loginData'; import { SecureNoteData } from './secureNoteData'; -import { CipherResponse } from '../response'; +import { CipherResponse } from '../response/cipherResponse'; export class CipherData { id: string; diff --git a/src/models/data/collectionData.ts b/src/models/data/collectionData.ts index 16b44871b3..3b05b9af22 100644 --- a/src/models/data/collectionData.ts +++ b/src/models/data/collectionData.ts @@ -1,4 +1,4 @@ -import { CollectionResponse } from '../response'; +import { CollectionResponse } from '../response/collectionResponse'; export class CollectionData { id: string; diff --git a/src/models/data/fieldData.ts b/src/models/data/fieldData.ts index 3af1d04285..cd0fb2c42b 100644 --- a/src/models/data/fieldData.ts +++ b/src/models/data/fieldData.ts @@ -1,4 +1,4 @@ -import { FieldType } from '../../enums'; +import { FieldType } from '../../enums/fieldType'; export class FieldData { type: FieldType; diff --git a/src/models/data/folderData.ts b/src/models/data/folderData.ts index c0a4d0dd2f..509267c9d4 100644 --- a/src/models/data/folderData.ts +++ b/src/models/data/folderData.ts @@ -1,4 +1,4 @@ -import { FolderResponse } from '../response'; +import { FolderResponse } from '../response/folderResponse'; export class FolderData { id: string; diff --git a/src/models/data/secureNoteData.ts b/src/models/data/secureNoteData.ts index 0a3d64da9d..4b27125a13 100644 --- a/src/models/data/secureNoteData.ts +++ b/src/models/data/secureNoteData.ts @@ -1,4 +1,4 @@ -import { SecureNoteType } from '../../enums'; +import { SecureNoteType } from '../../enums/secureNoteType'; export class SecureNoteData { type: SecureNoteType; diff --git a/src/models/domain/attachment.ts b/src/models/domain/attachment.ts index b2f82863c1..7861066e0e 100644 --- a/src/models/domain/attachment.ts +++ b/src/models/domain/attachment.ts @@ -1,9 +1,9 @@ -import { AttachmentData } from '../data'; +import { AttachmentData } from '../data/attachmentData'; import { CipherString } from './cipherString'; import Domain from './domain'; -import { AttachmentView } from '../view'; +import { AttachmentView } from '../view/attachmentView'; export class Attachment extends Domain { id: string; diff --git a/src/models/domain/authResult.ts b/src/models/domain/authResult.ts index e797781bbb..cb4bb57c65 100644 --- a/src/models/domain/authResult.ts +++ b/src/models/domain/authResult.ts @@ -1,4 +1,4 @@ -import { TwoFactorProviderType } from '../../enums'; +import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; export class AuthResult { twoFactor: boolean = false; diff --git a/src/models/domain/card.ts b/src/models/domain/card.ts index a41597ecae..19a2ad9e50 100644 --- a/src/models/domain/card.ts +++ b/src/models/domain/card.ts @@ -1,9 +1,9 @@ -import { CardData } from '../data'; +import { CardData } from '../data/cardData'; import { CipherString } from './cipherString'; import Domain from './domain'; -import { CardView } from '../view'; +import { CardView } from '../view/cardView'; export class Card extends Domain { cardholderName: CipherString; diff --git a/src/models/domain/cipher.ts b/src/models/domain/cipher.ts index e591c38c59..7f9c18d1f0 100644 --- a/src/models/domain/cipher.ts +++ b/src/models/domain/cipher.ts @@ -1,8 +1,8 @@ -import { CipherType } from '../../enums'; +import { CipherType } from '../../enums/cipherType'; -import { CipherData } from '../data'; +import { CipherData } from '../data/cipherData'; -import { CipherView } from '../view'; +import { CipherView } from '../view/cipherView'; import { Attachment } from './attachment'; import { Card } from './card'; diff --git a/src/models/domain/cipherString.ts b/src/models/domain/cipherString.ts index b6f4bedc08..d19d191065 100644 --- a/src/models/domain/cipherString.ts +++ b/src/models/domain/cipherString.ts @@ -1,6 +1,6 @@ -import { EncryptionType } from '../../enums'; +import { EncryptionType } from '../../enums/encryptionType'; -import { CryptoService } from '../../abstractions'; +import { CryptoService } from '../../abstractions/crypto.service'; export class CipherString { encryptedString?: string; diff --git a/src/models/domain/collection.ts b/src/models/domain/collection.ts index 9c3136e189..45fd54cd8c 100644 --- a/src/models/domain/collection.ts +++ b/src/models/domain/collection.ts @@ -1,6 +1,6 @@ -import { CollectionData } from '../data'; +import { CollectionData } from '../data/collectionData'; -import { CollectionView } from '../view'; +import { CollectionView } from '../view/collectionView'; import { CipherString } from './cipherString'; import Domain from './domain'; diff --git a/src/models/domain/domain.ts b/src/models/domain/domain.ts index d75bec902e..7fe644740f 100644 --- a/src/models/domain/domain.ts +++ b/src/models/domain/domain.ts @@ -1,6 +1,6 @@ -import { CipherString } from './cipherString'; +import { CipherString } from '../domain/cipherString'; -import { View } from '../view'; +import { View } from '../view/view'; export default abstract class Domain { protected buildDomainModel(domain: D, dataObj: any, map: any, diff --git a/src/models/domain/field.ts b/src/models/domain/field.ts index a527edbcac..023fefe5a9 100644 --- a/src/models/domain/field.ts +++ b/src/models/domain/field.ts @@ -1,11 +1,11 @@ -import { FieldType } from '../../enums'; +import { FieldType } from '../../enums/fieldType'; -import { FieldData } from '../data'; +import { FieldData } from '../data/fieldData'; import { CipherString } from './cipherString'; import Domain from './domain'; -import { FieldView } from '../view'; +import { FieldView } from '../view/fieldView'; export class Field extends Domain { name: CipherString; diff --git a/src/models/domain/folder.ts b/src/models/domain/folder.ts index 80c5057533..77b9fb310d 100644 --- a/src/models/domain/folder.ts +++ b/src/models/domain/folder.ts @@ -1,6 +1,6 @@ -import { FolderData } from '../data'; +import { FolderData } from '../data/folderData'; -import { FolderView } from '../view'; +import { FolderView } from '../view/folderView'; import { CipherString } from './cipherString'; import Domain from './domain'; diff --git a/src/models/domain/identity.ts b/src/models/domain/identity.ts index 7ef205a376..ade03260ea 100644 --- a/src/models/domain/identity.ts +++ b/src/models/domain/identity.ts @@ -1,9 +1,9 @@ -import { IdentityData } from '../data'; +import { IdentityData } from '../data/identityData'; import { CipherString } from './cipherString'; import Domain from './domain'; -import { IdentityView } from '../view'; +import { IdentityView } from '../view/identityView'; export class Identity extends Domain { title: CipherString; diff --git a/src/models/domain/login.ts b/src/models/domain/login.ts index 743b54ae3a..1c98dc7031 100644 --- a/src/models/domain/login.ts +++ b/src/models/domain/login.ts @@ -1,6 +1,6 @@ -import { LoginData } from '../data'; +import { LoginData } from '../data/loginData'; -import { LoginView } from '../view'; +import { LoginView } from '../view/loginView'; import { CipherString } from './cipherString'; import Domain from './domain'; diff --git a/src/models/domain/secureNote.ts b/src/models/domain/secureNote.ts index 33640e4794..79724a4193 100644 --- a/src/models/domain/secureNote.ts +++ b/src/models/domain/secureNote.ts @@ -1,10 +1,10 @@ -import { SecureNoteType } from '../../enums'; +import { SecureNoteType } from '../../enums/secureNoteType'; -import { SecureNoteData } from '../data'; +import { SecureNoteData } from '../data/secureNoteData'; import Domain from './domain'; -import { SecureNoteView } from '../view'; +import { SecureNoteView } from '../view/secureNoteView'; export class SecureNote extends Domain { type: SecureNoteType; diff --git a/src/models/domain/symmetricCryptoKey.ts b/src/models/domain/symmetricCryptoKey.ts index 4b12f98b07..4e3a5d6c2d 100644 --- a/src/models/domain/symmetricCryptoKey.ts +++ b/src/models/domain/symmetricCryptoKey.ts @@ -1,6 +1,6 @@ import * as forge from 'node-forge'; -import { EncryptionType } from '../../enums'; +import { EncryptionType } from '../../enums/encryptionType'; import { SymmetricCryptoKeyBuffers } from './symmetricCryptoKeyBuffers'; diff --git a/src/models/request/cipherRequest.ts b/src/models/request/cipherRequest.ts index ff2a79d972..232883f323 100644 --- a/src/models/request/cipherRequest.ts +++ b/src/models/request/cipherRequest.ts @@ -1,4 +1,4 @@ -import { CipherType } from '../../enums'; +import { CipherType } from '../../enums/cipherType'; export class CipherRequest { type: CipherType; diff --git a/src/models/request/deviceRequest.ts b/src/models/request/deviceRequest.ts index 4b29b44383..2aaa42cbe0 100644 --- a/src/models/request/deviceRequest.ts +++ b/src/models/request/deviceRequest.ts @@ -1,6 +1,6 @@ -import { DeviceType } from '../../enums'; +import { DeviceType } from '../../enums/deviceType'; -import { PlatformUtilsService } from '../../abstractions'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; export class DeviceRequest { type: DeviceType; diff --git a/src/models/request/folderRequest.ts b/src/models/request/folderRequest.ts index 63d53819b1..54ec76cac5 100644 --- a/src/models/request/folderRequest.ts +++ b/src/models/request/folderRequest.ts @@ -1,4 +1,4 @@ -import { Folder } from '../domain'; +import { Folder } from '../domain/folder'; export class FolderRequest { name: string; diff --git a/src/models/response/deviceResponse.ts b/src/models/response/deviceResponse.ts index 9fe3d37ece..23d0135b76 100644 --- a/src/models/response/deviceResponse.ts +++ b/src/models/response/deviceResponse.ts @@ -1,4 +1,4 @@ -import { DeviceType } from '../../enums'; +import { DeviceType } from '../../enums/deviceType'; export class DeviceResponse { id: string; diff --git a/src/models/response/identityTwoFactorResponse.ts b/src/models/response/identityTwoFactorResponse.ts index b01b83fb58..2adeeaa053 100644 --- a/src/models/response/identityTwoFactorResponse.ts +++ b/src/models/response/identityTwoFactorResponse.ts @@ -1,4 +1,4 @@ -import { TwoFactorProviderType } from '../../enums'; +import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; export class IdentityTwoFactorResponse { twoFactorProviders: TwoFactorProviderType[]; diff --git a/src/models/view/attachmentView.ts b/src/models/view/attachmentView.ts index 3fc9c39ff9..49ea57326d 100644 --- a/src/models/view/attachmentView.ts +++ b/src/models/view/attachmentView.ts @@ -1,6 +1,6 @@ import { View } from './view'; -import { Attachment } from '../domain'; +import { Attachment } from '../domain/attachment'; export class AttachmentView implements View { id: string; diff --git a/src/models/view/cardView.ts b/src/models/view/cardView.ts index 7149395630..26e3308e31 100644 --- a/src/models/view/cardView.ts +++ b/src/models/view/cardView.ts @@ -1,6 +1,6 @@ import { View } from './view'; -import { Card } from '../domain'; +import { Card } from '../domain/card'; export class CardView implements View { cardholderName: string; diff --git a/src/models/view/cipherView.ts b/src/models/view/cipherView.ts index aeb7e43cd7..b3f232bcbc 100644 --- a/src/models/view/cipherView.ts +++ b/src/models/view/cipherView.ts @@ -1,6 +1,6 @@ -import { CipherType } from '../../enums'; +import { CipherType } from '../../enums/cipherType'; -import { Cipher } from '../domain'; +import { Cipher } from '../domain/cipher'; import { AttachmentView } from './attachmentView'; import { CardView } from './cardView'; diff --git a/src/models/view/collectionView.ts b/src/models/view/collectionView.ts index 6cdc8a1791..ab184a2c35 100644 --- a/src/models/view/collectionView.ts +++ b/src/models/view/collectionView.ts @@ -1,6 +1,6 @@ import { View } from './view'; -import { Collection } from '../domain'; +import { Collection } from '../domain/collection'; export class CollectionView implements View { id: string; diff --git a/src/models/view/fieldView.ts b/src/models/view/fieldView.ts index 8e67ff6dc8..b3f68a584f 100644 --- a/src/models/view/fieldView.ts +++ b/src/models/view/fieldView.ts @@ -1,8 +1,8 @@ -import { FieldType } from '../../enums'; +import { FieldType } from '../../enums/fieldType'; import { View } from './view'; -import { Field } from '../domain'; +import { Field } from '../domain/field'; export class FieldView implements View { name: string; diff --git a/src/models/view/folderView.ts b/src/models/view/folderView.ts index 8f2cfe3e2c..ebe8ea157a 100644 --- a/src/models/view/folderView.ts +++ b/src/models/view/folderView.ts @@ -1,6 +1,6 @@ import { View } from './view'; -import { Folder } from '../domain'; +import { Folder } from '../domain/folder'; export class FolderView implements View { id: string = null; diff --git a/src/models/view/identityView.ts b/src/models/view/identityView.ts index 6843e1e3ac..4ae5fa0cf9 100644 --- a/src/models/view/identityView.ts +++ b/src/models/view/identityView.ts @@ -1,6 +1,6 @@ import { View } from './view'; -import { Identity } from '../domain'; +import { Identity } from '../domain/identity'; export class IdentityView implements View { title: string = null; diff --git a/src/models/view/index.ts b/src/models/view/index.ts index eb1b943c82..0e07c256be 100644 --- a/src/models/view/index.ts +++ b/src/models/view/index.ts @@ -1,9 +1,7 @@ export { AttachmentView } from './attachmentView'; export { CardView } from './cardView'; export { CipherView } from './cipherView'; -export { CollectionView } from './collectionView'; export { FieldView } from './fieldView'; -export { FolderView } from './folderView'; export { IdentityView } from './identityView'; export { LoginView } from './loginView'; export { SecureNoteView } from './secureNoteView'; diff --git a/src/models/view/loginView.ts b/src/models/view/loginView.ts index 6c97772b76..c3554c9c38 100644 --- a/src/models/view/loginView.ts +++ b/src/models/view/loginView.ts @@ -1,8 +1,8 @@ import { View } from './view'; -import { Login } from '../domain'; +import { Login } from '../domain/login'; -import { PlatformUtilsService } from '../../abstractions'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; export class LoginView implements View { username: string; diff --git a/src/models/view/secureNoteView.ts b/src/models/view/secureNoteView.ts index d6c2867dfa..1047c41086 100644 --- a/src/models/view/secureNoteView.ts +++ b/src/models/view/secureNoteView.ts @@ -1,8 +1,8 @@ -import { SecureNoteType } from '../../enums'; +import { SecureNoteType } from '../../enums/secureNoteType'; import { View } from './view'; -import { SecureNote } from '../domain'; +import { SecureNote } from '../domain/secureNote'; export class SecureNoteView implements View { type: SecureNoteType; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 395b411794..1e452af755 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -1,30 +1,24 @@ import { ConstantsService } from './constants.service'; -import { - ApiService as ApiServiceAbstraction, - PlatformUtilsService, - TokenService, -} from '../abstractions'; +import { ApiService as ApiServiceAbstraction } from '../abstractions/api.service'; +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; +import { TokenService } from '../abstractions/token.service'; -import { EnvironmentUrls } from '../models/domain'; +import { EnvironmentUrls } from '../models/domain/environmentUrls'; -import { - CipherRequest, - FolderRequest, - PasswordHintRequest, - RegisterRequest, - TokenRequest, - TwoFactorEmailRequest, -} from '../models/request'; +import { CipherRequest } from '../models/request/cipherRequest'; +import { FolderRequest } from '../models/request/folderRequest'; +import { PasswordHintRequest } from '../models/request/passwordHintRequest'; +import { RegisterRequest } from '../models/request/registerRequest'; +import { TokenRequest } from '../models/request/tokenRequest'; +import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; -import { - CipherResponse, - ErrorResponse, - FolderResponse, - IdentityTokenResponse, - IdentityTwoFactorResponse, - SyncResponse, -} from '../models/response'; +import { CipherResponse } from '../models/response/cipherResponse'; +import { ErrorResponse } from '../models/response/errorResponse'; +import { FolderResponse } from '../models/response/folderResponse'; +import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; +import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; +import { SyncResponse } from '../models/response/syncResponse'; export class ApiService implements ApiServiceAbstraction { urlsSet: boolean = false; diff --git a/src/services/appId.service.ts b/src/services/appId.service.ts index 50778bf8ea..430a375571 100644 --- a/src/services/appId.service.ts +++ b/src/services/appId.service.ts @@ -1,9 +1,7 @@ import { UtilsService } from './utils.service'; -import { - AppIdService as AppIdServiceInterface, - StorageService, -} from '../abstractions'; +import { AppIdService as AppIdServiceInterface } from '../abstractions/appId.service'; +import { StorageService } from '../abstractions/storage.service'; export class AppIdService implements AppIdServiceInterface { constructor(private storageService: StorageService) { diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 101a53e4f6..a360dc1c1d 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,32 +1,24 @@ -import { TwoFactorProviderType } from '../enums'; +import { TwoFactorProviderType } from '../enums/twoFactorProviderType'; -import { - AuthResult, - SymmetricCryptoKey, -} from '../models/domain'; +import { AuthResult } from '../models/domain/authResult'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; -import { - DeviceRequest, - TokenRequest, -} from '../models/request'; +import { DeviceRequest } from '../models/request/deviceRequest'; +import { TokenRequest } from '../models/request/tokenRequest'; -import { - IdentityTokenResponse, - IdentityTwoFactorResponse, -} from '../models/response'; +import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; +import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; import { ConstantsService } from '../services/constants.service'; -import { - ApiService, - AppIdService, - CryptoService, - I18nService, - MessagingService, - PlatformUtilsService, - TokenService, - UserService, -} from '../abstractions'; +import { ApiService } from '../abstractions/api.service'; +import { AppIdService } from '../abstractions/appId.service'; +import { CryptoService } from '../abstractions/crypto.service'; +import { I18nService } from '../abstractions/i18n.service'; +import { MessagingService } from '../abstractions/messaging.service'; +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; +import { TokenService } from '../abstractions/token.service'; +import { UserService } from '../abstractions/user.service'; export const TwoFactorProviders = { [TwoFactorProviderType.Authenticator]: { diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 9818eab438..fb8e532d62 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -1,47 +1,38 @@ -import { CipherType } from '../enums'; +import { CipherType } from '../enums/cipherType'; -import { CipherData } from '../models/data'; - -import { - Card, - Cipher, - CipherString, - Field, - Identity, - Login, - SecureNote, - SymmetricCryptoKey, -} from '../models/domain'; +import { CipherData } from '../models/data/cipherData'; +import { Card } from '../models/domain/card'; +import { Cipher } from '../models/domain/cipher'; +import { CipherString } from '../models/domain/cipherString'; import Domain from '../models/domain/domain'; +import { Field } from '../models/domain/field'; +import { Identity } from '../models/domain/identity'; +import { Login } from '../models/domain/login'; +import { SecureNote } from '../models/domain/secureNote'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; -import { CipherRequest } from '../models/request'; +import { CipherRequest } from '../models/request/cipherRequest'; -import { - CipherResponse, - ErrorResponse, -} from '../models/response'; +import { CipherResponse } from '../models/response/cipherResponse'; +import { ErrorResponse } from '../models/response/errorResponse'; -import { - CardView, - CipherView, - FieldView, - IdentityView, - LoginView, - View, -} from '../models/view'; +import { CardView } from '../models/view/cardView'; +import { CipherView } from '../models/view/cipherView'; +import { FieldView } from '../models/view/fieldView'; +import { IdentityView } from '../models/view/identityView'; +import { LoginView } from '../models/view/loginView'; +import { View } from '../models/view/view'; import { ConstantsService } from './constants.service'; -import { - ApiService, - CipherService as CipherServiceAbstraction, - CryptoService, - I18nService, - SettingsService, - StorageService, - UserService, -} from '../abstractions'; +import { ApiService } from '../abstractions/api.service'; +import { CipherService as CipherServiceAbstraction } from '../abstractions/cipher.service'; +import { CryptoService } from '../abstractions/crypto.service'; +import { I18nService } from '../abstractions/i18n.service'; +import { SettingsService } from '../abstractions/settings.service'; +import { StorageService } from '../abstractions/storage.service'; +import { UserService } from '../abstractions/user.service'; const Keys = { ciphersPrefix: 'ciphers_', diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index 58b54be274..3e7c60c089 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -1,16 +1,14 @@ -import { CollectionData } from '../models/data'; +import { CollectionData } from '../models/data/collectionData'; import { Collection } from '../models/domain'; -import { CollectionView } from '../models/view'; +import { CollectionView } from '../models/view/collectionView'; -import { - CollectionService as CollectionServiceAbstraction, - CryptoService, - I18nService, - StorageService, - UserService, -} from '../abstractions'; +import { CollectionService as CollectionServiceAbstraction } from '../abstractions/collection.service'; +import { CryptoService } from '../abstractions/crypto.service'; +import { I18nService } from '../abstractions/i18n.service'; +import { StorageService } from '../abstractions/storage.service'; +import { UserService } from '../abstractions/user.service'; const Keys = { collectionsPrefix: 'collections_', diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index 0210d9faed..e0c695432c 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -1,4 +1,4 @@ -import { PlatformUtilsService } from '../abstractions'; +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; export class ConstantsService { static readonly environmentUrlsKey: string = 'environmentUrls'; diff --git a/src/services/container.service.ts b/src/services/container.service.ts index 3659eacdfa..fa9a6628ce 100644 --- a/src/services/container.service.ts +++ b/src/services/container.service.ts @@ -1,7 +1,5 @@ -import { - CryptoService, - PlatformUtilsService, -} from '../abstractions'; +import { CryptoService } from '../abstractions/crypto.service'; +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; export class ContainerService { constructor(private cryptoService: CryptoService, diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index a25f1786fe..e6ad642574 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -1,19 +1,14 @@ import * as forge from 'node-forge'; -import { EncryptionType } from '../enums'; +import { EncryptionType } from '../enums/encryptionType'; -import { - CipherString, - EncryptedObject, - SymmetricCryptoKey, -} from '../models/domain'; +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 { ProfileOrganizationResponse } from '../models/response'; - -import { - CryptoService as CryptoServiceAbstraction, - StorageService as StorageServiceAbstraction, -} from '../abstractions'; +import { CryptoService as CryptoServiceAbstraction } from '../abstractions/crypto.service'; +import { StorageService as StorageServiceInterface } from '../abstractions/storage.service'; import { ConstantsService } from './constants.service'; import { UtilsService } from './utils.service'; @@ -46,8 +41,8 @@ export class CryptoService implements CryptoServiceAbstraction { private privateKey: ArrayBuffer; private orgKeys: Map; - constructor(private storageService: StorageServiceAbstraction, - private secureStorageService: StorageServiceAbstraction) { + constructor(private storageService: StorageServiceInterface, + private secureStorageService: StorageServiceInterface) { } async setKey(key: SymmetricCryptoKey): Promise { diff --git a/src/services/environment.service.ts b/src/services/environment.service.ts index d9bab748a0..e1544489f1 100644 --- a/src/services/environment.service.ts +++ b/src/services/environment.service.ts @@ -1,12 +1,10 @@ -import { EnvironmentUrls } from '../models/domain'; +import { EnvironmentUrls } from '../models/domain/environmentUrls'; import { ConstantsService } from './constants.service'; -import { - ApiService, - EnvironmentService as EnvironmentServiceAbstraction, - StorageService, -} from '../abstractions'; +import { ApiService } from '../abstractions/api.service'; +import { EnvironmentService as EnvironmentServiceAbstraction } from '../abstractions/environment.service'; +import { StorageService } from '../abstractions/storage.service'; export class EnvironmentService implements EnvironmentServiceAbstraction { baseUrl: string; diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index a8f1769874..81c03a15f5 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -1,21 +1,19 @@ -import { FolderData } from '../models/data'; +import { FolderData } from '../models/data/folderData'; -import { Folder } from '../models/domain'; +import { Folder } from '../models/domain/folder'; -import { FolderRequest } from '../models/request'; +import { FolderRequest } from '../models/request/folderRequest'; -import { FolderResponse } from '../models/response'; +import { FolderResponse } from '../models/response/folderResponse'; -import { FolderView } from '../models/view'; +import { FolderView } from '../models/view/folderView'; -import { - ApiService, - CryptoService, - FolderService as FolderServiceAbstraction, - I18nService, - StorageService, - UserService, -} from '../abstractions'; +import { ApiService } from '../abstractions/api.service'; +import { CryptoService } from '../abstractions/crypto.service'; +import { FolderService as FolderServiceAbstraction } from '../abstractions/folder.service'; +import { I18nService } from '../abstractions/i18n.service'; +import { StorageService } from '../abstractions/storage.service'; +import { UserService } from '../abstractions/user.service'; const Keys = { foldersPrefix: 'folders_', diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts index db24c60219..ba54fec1bf 100644 --- a/src/services/lock.service.ts +++ b/src/services/lock.service.ts @@ -1,15 +1,13 @@ import { ConstantsService } from './constants.service'; -import { - CipherService, - CollectionService, - CryptoService, - FolderService, - LockService as LockServiceAbstraction, - MessagingService, - PlatformUtilsService, - StorageService, -} from '../abstractions'; +import { CipherService } from '../abstractions/cipher.service'; +import { CollectionService } from '../abstractions/collection.service'; +import { CryptoService } from '../abstractions/crypto.service'; +import { FolderService } from '../abstractions/folder.service'; +import { LockService as LockServiceAbstraction } from '../abstractions/lock.service'; +import { MessagingService } from '../abstractions/messaging.service'; +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; +import { StorageService } from '../abstractions/storage.service'; export class LockService implements LockServiceAbstraction { constructor(private cipherService: CipherService, private folderService: FolderService, diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 40cc3705c7..09b728c050 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -1,15 +1,13 @@ -import { - CipherString, - PasswordHistory, -} from '../models/domain'; +import { CipherString } from '../models/domain/cipherString'; +import { PasswordHistory } from '../models/domain/passwordHistory'; import { UtilsService } from './utils.service'; +import { CryptoService } from '../abstractions/crypto.service'; import { - CryptoService, PasswordGenerationService as PasswordGenerationServiceAbstraction, - StorageService, -} from '../abstractions'; +} from '../abstractions/passwordGeneration.service'; +import { StorageService } from '../abstractions/storage.service'; const DefaultOptions = { length: 14, diff --git a/src/services/settings.service.ts b/src/services/settings.service.ts index cd9ee2e66e..506bdf01a0 100644 --- a/src/services/settings.service.ts +++ b/src/services/settings.service.ts @@ -1,8 +1,6 @@ -import { - SettingsService as SettingsServiceAbstraction, - StorageService, - UserService, -} from '../abstractions'; +import { SettingsService as SettingsServiceAbstraction } from '../abstractions/settings.service'; +import { StorageService } from '../abstractions/storage.service'; +import { UserService } from '../abstractions/user.service'; const Keys = { settingsPrefix: 'settings_', diff --git a/src/services/state.service.ts b/src/services/state.service.ts index d8b845c010..03a09e30fc 100644 --- a/src/services/state.service.ts +++ b/src/services/state.service.ts @@ -1,4 +1,4 @@ -import { StateService as StateServiceAbstraction } from '../abstractions'; +import { StateService as StateServiceAbstraction } from '../abstractions/state.service'; export class StateService implements StateServiceAbstraction { private state: any = {}; diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index fdcd16f493..2a001e01a8 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -1,29 +1,23 @@ -import { - ApiService, - CipherService, - CollectionService, - CryptoService, - FolderService, - MessagingService, - SettingsService, - StorageService, - SyncService as SyncServiceAbstraction, - UserService, -} from '../abstractions'; +import { ApiService } from '../abstractions/api.service'; +import { CipherService } from '../abstractions/cipher.service'; +import { CollectionService } from '../abstractions/collection.service'; +import { CryptoService } from '../abstractions/crypto.service'; +import { FolderService } from '../abstractions/folder.service'; +import { MessagingService } from '../abstractions/messaging.service'; +import { SettingsService } from '../abstractions/settings.service'; +import { StorageService } from '../abstractions/storage.service'; +import { SyncService as SyncServiceAbstraction } from '../abstractions/sync.service'; +import { UserService } from '../abstractions/user.service'; -import { - CipherData, - CollectionData, - FolderData, -} from '../models/data'; +import { CipherData } from '../models/data/cipherData'; +import { CollectionData } from '../models/data/collectionData'; +import { FolderData } from '../models/data/folderData'; -import { - CipherResponse, - CollectionResponse, - DomainsResponse, - FolderResponse, - ProfileResponse, -} from '../models/response'; +import { CipherResponse } from '../models/response/cipherResponse'; +import { CollectionResponse } from '../models/response/collectionResponse'; +import { DomainsResponse } from '../models/response/domainsResponse'; +import { FolderResponse } from '../models/response/folderResponse'; +import { ProfileResponse } from '../models/response/profileResponse'; const Keys = { lastSyncPrefix: 'lastSync_', diff --git a/src/services/token.service.ts b/src/services/token.service.ts index 694c2bf78c..94dbfa10fd 100644 --- a/src/services/token.service.ts +++ b/src/services/token.service.ts @@ -1,10 +1,8 @@ import { ConstantsService } from './constants.service'; import { UtilsService } from './utils.service'; -import { - StorageService, - TokenService as TokenServiceAbstraction, -} from '../abstractions'; +import { StorageService } from '../abstractions/storage.service'; +import { TokenService as TokenServiceAbstraction } from '../abstractions/token.service'; const Keys = { accessToken: 'accessToken', diff --git a/src/services/totp.service.ts b/src/services/totp.service.ts index 1bc57b4be8..e52c11e48c 100644 --- a/src/services/totp.service.ts +++ b/src/services/totp.service.ts @@ -1,9 +1,7 @@ import { ConstantsService } from './constants.service'; -import { - StorageService, - TotpService as TotpServiceAbstraction, -} from '../abstractions'; +import { StorageService } from '../abstractions/storage.service'; +import { TotpService as TotpServiceAbstraction } from '../abstractions/totp.service'; const b32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 5fefef16ce..1949cadb48 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -1,8 +1,6 @@ -import { - StorageService, - TokenService, - UserService as UserServiceAbsrtaction, -} from '../abstractions'; +import { StorageService } from '../abstractions/storage.service'; +import { TokenService } from '../abstractions/token.service'; +import { UserService as UserServiceAbsrtaction } from '../abstractions/user.service'; const Keys = { userId: 'userId', diff --git a/src/services/utils.service.ts b/src/services/utils.service.ts index 7a58df6a5a..359a743a91 100644 --- a/src/services/utils.service.ts +++ b/src/services/utils.service.ts @@ -1,4 +1,4 @@ -import { UtilsService as UtilsServiceAbstraction } from '../abstractions'; +import { UtilsService as UtilsServiceAbstraction } from '../abstractions/utils.service'; export class UtilsService implements UtilsServiceAbstraction { static copyToClipboard(text: string, doc?: Document): void { From 5b63336347ad9597ed65d907b1bdb804d8b4f39a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 19 Feb 2018 13:07:37 -0500 Subject: [PATCH 0101/1626] Revert "collection from domain index" This reverts commit 36315dae19914490e8810746b7935d0596d2a3ba. --- src/services/collection.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index 3e7c60c089..f860f5d9d3 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -1,6 +1,6 @@ import { CollectionData } from '../models/data/collectionData'; -import { Collection } from '../models/domain'; +import { Collection } from '../models/domain/collection'; import { CollectionView } from '../models/view/collectionView'; From e8814e8864b1183ecf1908ca7606931c489428c5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 19 Feb 2018 20:46:42 -0500 Subject: [PATCH 0102/1626] commit package-lock.json --- .gitignore | 1 - package-lock.json | 834 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 834 insertions(+), 1 deletion(-) create mode 100644 package-lock.json diff --git a/.gitignore b/.gitignore index db50295cf8..584bc1013b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,4 @@ node_modules npm-debug.log *.crx *.pem -package-lock.json dist diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000..10afb26822 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,834 @@ +{ + "name": "@bitwarden/jslib", + "version": "0.0.20", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/fs-extra": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-4.0.0.tgz", + "integrity": "sha512-PlKJw6ujJXLJjbvB3T0UCbY3jibKM6/Ya5cc9j1q+mYDeK3aR4Dp+20ZwxSuvJr9mIoPxp7+IL4aMOEvsscRTA==", + "dev": true, + "requires": { + "@types/node": "8.5.7" + } + }, + "@types/handlebars": { + "version": "4.0.31", + "resolved": "https://registry.npmjs.org/@types/handlebars/-/handlebars-4.0.31.tgz", + "integrity": "sha1-p/umb6/kJxOu6I7sqNuRGS7+bnI=", + "dev": true + }, + "@types/highlight.js": { + "version": "9.1.8", + "resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.1.8.tgz", + "integrity": "sha1-0ifxi8uPPxh+FpZfJESFmgRol1g=", + "dev": true + }, + "@types/lodash": { + "version": "4.14.74", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.74.tgz", + "integrity": "sha512-BZknw3E/z3JmCLqQVANcR17okqVTPZdlxvcIz0fJiJVLUCbSH1hK3zs9r634PVSmrzAxN+n/fxlVRiYoArdOIQ==", + "dev": true + }, + "@types/marked": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.3.0.tgz", + "integrity": "sha512-CSf9YWJdX1DkTNu9zcNtdCcn6hkRtB5ILjbhRId4ZOQqx30fXmdecuaXhugQL6eyrhuXtaHJ7PHI+Vm7k9ZJjg==", + "dev": true + }, + "@types/minimatch": { + "version": "2.0.29", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-2.0.29.tgz", + "integrity": "sha1-UALhT3Xi1x5WQoHfBDHIwbSio2o=", + "dev": true + }, + "@types/node": { + "version": "8.5.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.5.7.tgz", + "integrity": "sha512-+1ZfzGIq8Y3EV7hPF7bs3i+Gi2mqYOiEGGRxGYPrn+hTYLMmzg+/5TkMkCHiRtLB38XSNvr/43aQ9+cUq4BbBg==", + "dev": true + }, + "@types/node-forge": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.7.1.tgz", + "integrity": "sha1-XoS4q/QthOxenQg+jHVzsxVcYzQ=" + }, + "@types/shelljs": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.7.0.tgz", + "integrity": "sha1-IpwVfGvB5n1rmQ5sXhjb0v9Yz/A=", + "dev": true, + "requires": { + "@types/node": "8.5.7" + } + }, + "@types/webcrypto": { + "version": "0.0.28", + "resolved": "https://registry.npmjs.org/@types/webcrypto/-/webcrypto-0.0.28.tgz", + "integrity": "sha1-sGFgOQzQAPsgH2LrAqw0Hr9+YP4=" + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "balanced-match": { + "version": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "dev": true, + "requires": { + "balanced-match": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "concat-map": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + } + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true, + "optional": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "optional": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true, + "optional": true + } + } + }, + "concat-map": { + "version": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true, + "optional": true + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + }, + "fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "4.0.0", + "universalify": "0.1.1" + } + }, + "fs.realpath": { + "version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "dev": true, + "requires": { + "fs.realpath": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "inflight": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "path-is-absolute": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "handlebars": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "dev": true, + "requires": { + "async": "1.5.2", + "optimist": "0.6.1", + "source-map": "0.4.4", + "uglify-js": "2.8.29" + } + }, + "highlight.js": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz", + "integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4=", + "dev": true + }, + "inflight": { + "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "wrappy": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + } + }, + "inherits": { + "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "js-yaml": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", + "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "dev": true, + "requires": { + "argparse": "1.0.10", + "esprima": "4.0.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true, + "optional": true + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "marked": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.9.tgz", + "integrity": "sha512-nW5u0dxpXxHfkHzzrveY45gCbi+R4PaO4WRZYqZNl+vB0hVGeqlFn0aOg1c8AKL63TrNFn9Bm2UP4AdiZ9TPLw==", + "dev": true + }, + "minimatch": { + "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true, + "requires": { + "brace-expansion": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz" + } + }, + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + }, + "node-forge": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz", + "integrity": "sha1-naYR6giYL0uUIGs760zJZl8gwwA=" + }, + "once": { + "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "0.0.10", + "wordwrap": "0.0.3" + } + }, + "path-is-absolute": { + "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "progress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "dev": true + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz" + } + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "resolve": { + "version": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", + "integrity": "sha1-HwmsznlsmnYlefMbLBzEw83fnzY=", + "dev": true, + "requires": { + "path-parse": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz" + } + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz" + } + }, + "shelljs": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", + "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", + "dev": true, + "requires": { + "glob": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "interpret": "1.1.0", + "rechoir": "0.6.2" + } + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "tslint": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.9.1.tgz", + "integrity": "sha1-ElX4ej/1frCw4fDmEKi0dIBGya4=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "builtin-modules": "1.1.1", + "chalk": "2.3.1", + "commander": "2.14.1", + "diff": "3.4.0", + "glob": "7.1.2", + "js-yaml": "3.10.0", + "minimatch": "3.0.4", + "resolve": "1.5.0", + "semver": "5.5.0", + "tslib": "1.9.0", + "tsutils": "2.21.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "chalk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "5.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "supports-color": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", + "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "diff": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.4.0.tgz", + "integrity": "sha512-QpVuMTEoJMF7cKzi6bvWhRulU1fZqZnvyVQgNhPaxxuTYwyjn/j1v9falseQ/uXWwPnO56RBfwtg4h/EQXmucA==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "resolve": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", + "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==", + "dev": true + }, + "tsutils": { + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.21.1.tgz", + "integrity": "sha512-heMkdeQ9iUc90ynfiNo5Y+GXrEEGy86KMvnSTfHO+Q40AuNQ1lZGXcv58fuU9XTUxI0V7YIN9xPN+CO9b1Gn3w==", + "dev": true, + "requires": { + "tslib": "1.9.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } + }, + "typedoc": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.9.0.tgz", + "integrity": "sha512-numP0CtcUK4I1Vssw6E1N/FjyJWpWqhLT4Zb7Gw3i7ca3ElnYh6z41Y/tcUhMsMYn6L8b67E/Fu4XYYKkNaLbA==", + "dev": true, + "requires": { + "@types/fs-extra": "4.0.0", + "@types/handlebars": "4.0.31", + "@types/highlight.js": "9.1.8", + "@types/lodash": "4.14.74", + "@types/marked": "0.3.0", + "@types/minimatch": "2.0.29", + "@types/shelljs": "0.7.0", + "fs-extra": "4.0.3", + "handlebars": "4.0.11", + "highlight.js": "9.12.0", + "lodash": "4.17.4", + "marked": "0.3.9", + "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "progress": "2.0.0", + "shelljs": "0.7.8", + "typedoc-default-themes": "0.5.0", + "typescript": "2.4.1" + }, + "dependencies": { + "typescript": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.4.1.tgz", + "integrity": "sha1-w8yxbdqgsjFN4DHn5v7onlujRrw=", + "dev": true + } + } + }, + "typedoc-default-themes": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.5.0.tgz", + "integrity": "sha1-bcJDPnjti+qOiHo6zeLzF4W9Yic=", + "dev": true + }, + "typescript": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.1.tgz", + "integrity": "sha512-bqB1yS6o9TNA9ZC/MJxM0FZzPnZdtHj0xWK/IZ5khzVqdpGul/R/EIiHRgFXlwTD7PSIaYVnGKq1QgMCu2mnqw==", + "dev": true + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "optional": true, + "requires": { + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "optional": true + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "universalify": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=", + "dev": true + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true, + "optional": true + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + }, + "wrappy": { + "version": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "optional": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } +} From 9adabdab483aa463e38382bebdad40305f4974f1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 23 Feb 2018 22:10:58 -0500 Subject: [PATCH 0103/1626] add log service abstraction --- src/abstractions/log.service.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/abstractions/log.service.ts diff --git a/src/abstractions/log.service.ts b/src/abstractions/log.service.ts new file mode 100644 index 0000000000..80a4b32472 --- /dev/null +++ b/src/abstractions/log.service.ts @@ -0,0 +1,7 @@ +export abstract class LogService { + debug: (message: string) => void; + info: (message: string) => void; + warning: (message: string) => void; + error: (message: string) => void; + write: (type: string, message: string) => void; +} From b747830c5b4360bf50d6e02126ebaab24e3028d5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 23 Feb 2018 23:10:13 -0500 Subject: [PATCH 0104/1626] log level type --- src/abstractions/index.ts | 1 + src/abstractions/log.service.ts | 4 +++- src/enums/index.ts | 1 + src/enums/logLevelType.ts | 6 ++++++ 4 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 src/enums/logLevelType.ts diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index 0d5551e070..a08ce2a787 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -8,6 +8,7 @@ export { EnvironmentService } from './environment.service'; export { FolderService } from './folder.service'; export { I18nService } from './i18n.service'; export { LockService } from './lock.service'; +export { LogService } from './log.service'; export { MessagingService } from './messaging.service'; export { PasswordGenerationService } from './passwordGeneration.service'; export { PlatformUtilsService } from './platformUtils.service'; diff --git a/src/abstractions/log.service.ts b/src/abstractions/log.service.ts index 80a4b32472..c4cb55035e 100644 --- a/src/abstractions/log.service.ts +++ b/src/abstractions/log.service.ts @@ -1,7 +1,9 @@ +import { LogLevelType } from '../enums/logLevelType'; + export abstract class LogService { debug: (message: string) => void; info: (message: string) => void; warning: (message: string) => void; error: (message: string) => void; - write: (type: string, message: string) => void; + write: (level: LogLevelType, message: string) => void; } diff --git a/src/enums/index.ts b/src/enums/index.ts index efdf363c89..e0eac44ae0 100644 --- a/src/enums/index.ts +++ b/src/enums/index.ts @@ -2,5 +2,6 @@ export { CipherType } from './cipherType'; export { DeviceType } from './deviceType'; export { EncryptionType } from './encryptionType'; export { FieldType } from './fieldType'; +export { LogLevelType } from './logLevelType'; export { SecureNoteType } from './secureNoteType'; export { TwoFactorProviderType } from './twoFactorProviderType'; diff --git a/src/enums/logLevelType.ts b/src/enums/logLevelType.ts new file mode 100644 index 0000000000..09f84b80cf --- /dev/null +++ b/src/enums/logLevelType.ts @@ -0,0 +1,6 @@ +export enum LogLevelType { + Debug, + Info, + Warning, + Error, +} From 783a86fe58ae6b76cf6e09243712cb63905afb92 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 26 Feb 2018 23:32:06 -0500 Subject: [PATCH 0105/1626] isMacAppStore --- src/abstractions/platformUtils.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index 3835bd062d..5ed47ede54 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -9,6 +9,7 @@ export abstract class PlatformUtilsService { isOpera: () => boolean; isVivaldi: () => boolean; isSafari: () => boolean; + isMacAppStore: () => boolean; analyticsId: () => string; getDomain: (uriString: string) => string; isViewOpen: () => boolean; From 902d72457cbeb99ef5d9eb69945ad43658f69277 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 26 Feb 2018 23:34:02 -0500 Subject: [PATCH 0106/1626] analytics disabled by default on mas --- src/misc/analytics.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/misc/analytics.ts b/src/misc/analytics.ts index 98cfdebc5b..e78635815c 100644 --- a/src/misc/analytics.ts +++ b/src/misc/analytics.ts @@ -8,7 +8,7 @@ const GaObj = 'ga'; export class Analytics { private gaTrackingId: string = null; - private isFirefox = false; + private defaultDisabled = false; private appVersion: string; constructor(win: Window, private gaFilter?: () => boolean, @@ -28,7 +28,7 @@ export class Analytics { } this.appVersion = this.platformUtilsService.getApplicationVersion(); - this.isFirefox = this.platformUtilsService.isFirefox(); + this.defaultDisabled = this.platformUtilsService.isFirefox() || this.platformUtilsService.isMacAppStore(); this.gaTrackingId = this.platformUtilsService.analyticsId(); (win as any).GoogleAnalyticsObject = GaObj; @@ -43,8 +43,7 @@ export class Analytics { } const disabled = await this.storageService.get(ConstantsService.disableGaKey); - // Default for Firefox is disabled. - if ((this.isFirefox && disabled == null) || disabled != null && disabled) { + if ((this.defaultDisabled && disabled == null) || disabled != null && disabled) { return; } From f3f91c20aea6965ce1e789a1b86bf8d9892d39b2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 27 Feb 2018 14:19:31 -0500 Subject: [PATCH 0107/1626] Uppercase Bitwarden --- README.md | 4 +++- SECURITY.md | 16 ++++++++-------- package.json | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 5399b6ed7d..8ed823597e 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# jslib +# Bitwarden JavaScript Library + +Common code referenced across Bitwarden JavaScript projects. diff --git a/SECURITY.md b/SECURITY.md index 3564ea7b08..ef94f0b494 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,4 +1,4 @@ -bitwarden believes that working with security researchers across the globe is crucial to keeping our +Bitwarden believes that working with security researchers across the globe is crucial to keeping our users safe. If you believe you've found a security issue in our product or service, we encourage you to notify us. We welcome working with you to resolve the issue promptly. Thanks in advance! @@ -16,7 +16,7 @@ notify us. We welcome working with you to resolve the issue promptly. Thanks in # In-scope -- Security issues in any current release of bitwarden. This includes the web vault, browser extension, +- Security issues in any current release of Bitwarden. This includes the web vault, browser extension, and mobile apps (iOS and Android). Product downloads are available at https://bitwarden.com. Source code is available at https://github.com/bitwarden. @@ -24,14 +24,14 @@ notify us. We welcome working with you to resolve the issue promptly. Thanks in The following bug classes are out-of scope: -- Bugs that are already reported on any of bitwarden's issue trackers (https://github.com/bitwarden), +- Bugs that are already reported on any of Bitwarden's issue trackers (https://github.com/bitwarden), or that we already know of. Note that some of our issue tracking is private. - Issues in an upstream software dependency (ex: Xamarin, ASP.NET) which are already reported to the upstream maintainer. - Attacks requiring physical access to a user's device. - Self-XSS -- Issues related to software or protocols not under bitwarden's control -- Vulnerabilities in outdated versions of bitwarden +- Issues related to software or protocols not under Bitwarden's control +- Vulnerabilities in outdated versions of Bitwarden - Missing security best practices that do not directly lead to a vulnerability - Issues that do not have any impact on the general public @@ -39,7 +39,7 @@ While researching, we'd like to ask you to refrain from: - Denial of service - Spamming -- Social engineering (including phishing) of bitwarden staff or contractors -- Any physical attempts against bitwarden property or data centers +- Social engineering (including phishing) of Bitwarden staff or contractors +- Any physical attempts against Bitwarden property or data centers -Thank you for helping keep bitwarden and our users safe! +Thank you for helping keep Bitwarden and our users safe! diff --git a/package.json b/package.json index 8177c73edd..c3142386fe 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/jslib", "version": "0.0.20", - "description": "Common code used across bitwarden JavaScript projects.", + "description": "Common code used across Bitwarden JavaScript projects.", "keywords": [ "bitwarden" ], From ab00dfd3991c258065d452dfb14b11e90898143e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 27 Feb 2018 22:54:38 -0500 Subject: [PATCH 0108/1626] none folder added last --- src/services/folder.service.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index 81c03a15f5..002085c8a5 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -67,15 +67,12 @@ export class FolderService implements FolderServiceAbstraction { return this.decryptedFolderCache; } - const noneFolder = new FolderView(); - noneFolder.name = this.noneFolder(); - const decFolders: FolderView[] = [noneFolder]; - const key = await this.cryptoService.getKey(); if (key == null) { throw new Error('No key.'); } + const decFolders: FolderView[] = []; const promises: Array> = []; const folders = await this.getAll(); folders.forEach((folder) => { @@ -84,6 +81,11 @@ export class FolderService implements FolderServiceAbstraction { await Promise.all(promises); decFolders.sort(this.getLocaleSortingFunction()); + + const noneFolder = new FolderView(); + noneFolder.name = this.noneFolder(); + decFolders.push(noneFolder); + this.decryptedFolderCache = decFolders; return this.decryptedFolderCache; } @@ -164,11 +166,6 @@ export class FolderService implements FolderServiceAbstraction { private getLocaleSortingFunction(): (a: FolderView, b: FolderView) => number { return (a, b) => { - if (a.id == null) { - // No folder is always last - return Number.MAX_SAFE_INTEGER; - } - return this.i18nService.collator ? this.i18nService.collator.compare(a.name, b.name) : a.name.localeCompare(b.name); }; From 1aabb42e470704d3b818bd493a65054bc43b1112 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Wed, 28 Feb 2018 16:52:35 +0100 Subject: [PATCH 0109/1626] AuditService (#2) * Add AuditService. * Change sha1 to use Webcrypto. * Add interface for AuditService. * Move PwnedPasswodsApi constant outside class. * Change FromBufferToHex implementation to simpler code. * Use correct string to array function. * Change auditService interface to abstract class. Add missing type to utils. --- src/abstractions/audit.service.ts | 3 +++ src/abstractions/crypto.service.ts | 1 + src/services/audit.service.ts | 26 ++++++++++++++++++++++++++ src/services/crypto.service.ts | 11 +++++++++++ src/services/utils.service.ts | 8 ++++++++ 5 files changed, 49 insertions(+) create mode 100644 src/abstractions/audit.service.ts create mode 100644 src/services/audit.service.ts diff --git a/src/abstractions/audit.service.ts b/src/abstractions/audit.service.ts new file mode 100644 index 0000000000..0ea1e1fe89 --- /dev/null +++ b/src/abstractions/audit.service.ts @@ -0,0 +1,3 @@ +export abstract class AuditService { + passwordLeaked: (password: string) => Promise; +} diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index dbf5f0da75..704171f2c8 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -31,4 +31,5 @@ export abstract class CryptoService { decrypt: (cipherString: CipherString, key?: SymmetricCryptoKey, outputEncoding?: string) => Promise; decryptFromBytes: (encBuf: ArrayBuffer, key: SymmetricCryptoKey) => Promise; rsaDecrypt: (encValue: string) => Promise; + sha1: (password: string) => Promise; } diff --git a/src/services/audit.service.ts b/src/services/audit.service.ts new file mode 100644 index 0000000000..f91da9c76d --- /dev/null +++ b/src/services/audit.service.ts @@ -0,0 +1,26 @@ +import { CryptoService } from '../abstractions/crypto.service'; + +const PwnedPasswordsApi = 'https://api.pwnedpasswords.com/range/'; + +export class AuditService { + + constructor(private cryptoService: CryptoService) { + } + + async passwordLeaked(password: string): Promise { + const hash = (await this.cryptoService.sha1(password)).toUpperCase(); + + const response = await fetch(PwnedPasswordsApi + hash.substr(0, 5)); + const leakedHashes = await response.text(); + + const hashEnding = hash.substr(5); + + const match = leakedHashes + .split(/\r?\n/) + .find((v) => { + return v.split(':')[0] === hashEnding; + }); + + return match ? parseInt(match.split(':')[1], 10) : 0; + } +} diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index e6ad642574..93dbcd3b5c 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -452,6 +452,17 @@ export class CryptoService implements CryptoServiceAbstraction { return b64DecValue; } + async sha1(password: string): Promise { + const hash = await Crypto.subtle.digest( + { + name: 'SHA-1', + }, + UtilsService.fromUtf8ToArray(password), + ); + + return UtilsService.fromBufferToHex(hash); + } + // Helpers private async aesEncrypt(plainValue: ArrayBuffer, key: SymmetricCryptoKey): Promise { diff --git a/src/services/utils.service.ts b/src/services/utils.service.ts index 359a743a91..a1e900e761 100644 --- a/src/services/utils.service.ts +++ b/src/services/utils.service.ts @@ -125,6 +125,14 @@ export class UtilsService implements UtilsServiceAbstraction { return decodeURIComponent(escape(encodedString)); } + // Source: Frxstrem, https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex + static fromBufferToHex(buffer: ArrayBuffer): string { + return Array.prototype.map.call( + new Uint8Array(buffer), + (x: number) => ('00' + x.toString(16)).slice(-2), + ).join(''); + } + static getHostname(uriString: string): string { if (uriString == null) { return null; From e1041e9b5bbabc760f4caa337a8284d86d037a44 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 28 Feb 2018 10:58:34 -0500 Subject: [PATCH 0110/1626] cleanup auditservice --- src/services/audit.service.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/services/audit.service.ts b/src/services/audit.service.ts index f91da9c76d..8c5479d5b1 100644 --- a/src/services/audit.service.ts +++ b/src/services/audit.service.ts @@ -3,24 +3,21 @@ import { CryptoService } from '../abstractions/crypto.service'; const PwnedPasswordsApi = 'https://api.pwnedpasswords.com/range/'; export class AuditService { - constructor(private cryptoService: CryptoService) { } async passwordLeaked(password: string): Promise { const hash = (await this.cryptoService.sha1(password)).toUpperCase(); - - const response = await fetch(PwnedPasswordsApi + hash.substr(0, 5)); - const leakedHashes = await response.text(); - + const hashStart = hash.substr(0, 5); const hashEnding = hash.substr(5); - const match = leakedHashes - .split(/\r?\n/) - .find((v) => { - return v.split(':')[0] === hashEnding; - }); + const response = await fetch(PwnedPasswordsApi + hashStart); + const leakedHashes = await response.text(); - return match ? parseInt(match.split(':')[1], 10) : 0; + const match = leakedHashes.split(/\r?\n/).find((v) => { + return v.split(':')[0] === hashEnding; + }); + + return match != null ? parseInt(match.split(':')[1], 10) : 0; } } From e3b3e444dbff7e4541fa5367ee26bc7ed4d73b26 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 28 Feb 2018 11:09:10 -0500 Subject: [PATCH 0111/1626] formatting cleanup --- src/services/crypto.service.ts | 9 ++------- src/services/utils.service.ts | 8 +++----- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 93dbcd3b5c..243afb66aa 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -453,13 +453,8 @@ export class CryptoService implements CryptoServiceAbstraction { } async sha1(password: string): Promise { - const hash = await Crypto.subtle.digest( - { - name: 'SHA-1', - }, - UtilsService.fromUtf8ToArray(password), - ); - + const passwordArr = UtilsService.fromUtf8ToArray(password); + const hash = await Crypto.subtle.digest({ name: 'SHA-1' }, passwordArr); return UtilsService.fromBufferToHex(hash); } diff --git a/src/services/utils.service.ts b/src/services/utils.service.ts index a1e900e761..7f692ae191 100644 --- a/src/services/utils.service.ts +++ b/src/services/utils.service.ts @@ -125,12 +125,10 @@ export class UtilsService implements UtilsServiceAbstraction { return decodeURIComponent(escape(encodedString)); } - // Source: Frxstrem, https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex + // ref: https://stackoverflow.com/a/40031979/1090359 static fromBufferToHex(buffer: ArrayBuffer): string { - return Array.prototype.map.call( - new Uint8Array(buffer), - (x: number) => ('00' + x.toString(16)).slice(-2), - ).join(''); + const bytes = new Uint8Array(buffer); + return Array.prototype.map.call(bytes, (x: number) => ('00' + x.toString(16)).slice(-2)).join(''); } static getHostname(uriString: string): string { From 6aef18ee7f54fc5cfb042206eb27e76f2458b60a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 28 Feb 2018 15:15:10 -0500 Subject: [PATCH 0112/1626] search service with lunr implementation --- package-lock.json | 10 +++++ package.json | 2 + src/abstractions/search.service.ts | 6 +++ src/models/view/cipherView.ts | 8 ++++ src/services/audit.service.ts | 3 +- src/services/search.service.ts | 60 ++++++++++++++++++++++++++++++ 6 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 src/abstractions/search.service.ts create mode 100644 src/services/search.service.ts diff --git a/package-lock.json b/package-lock.json index 10afb26822..c7cf6b454d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,11 @@ "integrity": "sha512-BZknw3E/z3JmCLqQVANcR17okqVTPZdlxvcIz0fJiJVLUCbSH1hK3zs9r634PVSmrzAxN+n/fxlVRiYoArdOIQ==", "dev": true }, + "@types/lunr": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/lunr/-/lunr-2.1.5.tgz", + "integrity": "sha512-esk3CG25hRtHsVHm+LOjiSFYdw8be3uIY653WUwR43Bro914HSimPgPpqgajkhTJ0awK3RQfaIxP7zvbtCpcyg==" + }, "@types/marked": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.3.0.tgz", @@ -297,6 +302,11 @@ "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", "dev": true }, + "lunr": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.1.6.tgz", + "integrity": "sha512-ydJpB8CX8cZ/VE+KMaYaFcZ6+o2LruM6NG76VXdflYTgluvVemz1lW4anE+pyBbLvxJHZdvD1Jy/fOqdzAEJog==" + }, "marked": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.9.tgz", diff --git a/package.json b/package.json index c3142386fe..935e5f8359 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,9 @@ "typescript": "^2.7.1" }, "dependencies": { + "lunr": "2.1.6", "node-forge": "0.7.1", + "@types/lunr": "2.1.5", "@types/node-forge": "0.7.1", "@types/webcrypto": "0.0.28" } diff --git a/src/abstractions/search.service.ts b/src/abstractions/search.service.ts new file mode 100644 index 0000000000..c5983e2427 --- /dev/null +++ b/src/abstractions/search.service.ts @@ -0,0 +1,6 @@ +import { CipherView } from '../models/view/cipherView'; + +export abstract class SearchService { + indexCiphers: () => Promise; + searchCiphers: (query: string) => Promise; +} diff --git a/src/models/view/cipherView.ts b/src/models/view/cipherView.ts index b3f232bcbc..6f00464841 100644 --- a/src/models/view/cipherView.ts +++ b/src/models/view/cipherView.ts @@ -65,4 +65,12 @@ export class CipherView implements View { get hasFields(): boolean { return this.fields && this.fields.length > 0; } + + get login_username(): string { + return this.login != null ? this.login.username : null; + } + + get login_uri(): string { + return this.login != null ? this.login.uri : null; + } } diff --git a/src/services/audit.service.ts b/src/services/audit.service.ts index 8c5479d5b1..60553b834e 100644 --- a/src/services/audit.service.ts +++ b/src/services/audit.service.ts @@ -1,8 +1,9 @@ +import { AuditService as AuditServiceAbstraction } from '../abstractions/audit.service'; import { CryptoService } from '../abstractions/crypto.service'; const PwnedPasswordsApi = 'https://api.pwnedpasswords.com/range/'; -export class AuditService { +export class AuditService implements AuditServiceAbstraction { constructor(private cryptoService: CryptoService) { } diff --git a/src/services/search.service.ts b/src/services/search.service.ts new file mode 100644 index 0000000000..3efe4a2ac4 --- /dev/null +++ b/src/services/search.service.ts @@ -0,0 +1,60 @@ +import * as lunr from 'lunr'; + +import { CipherView } from '../models/view/cipherView'; + +import { CipherService } from '../abstractions/cipher.service'; +import { SearchService as SearchServiceAbstraction } from '../abstractions/search.service'; + +export class SearchService implements SearchServiceAbstraction { + private index: lunr.Index; + + constructor(private cipherService: CipherService) { + } + + async indexCiphers(): Promise { + const builder = new lunr.Builder(); + builder.ref('id'); + builder.field('name'); + builder.field('subTitle'); + builder.field('notes'); + builder.field('login_username'); + builder.field('login_uri'); + + const ciphers = await this.cipherService.getAllDecrypted(); + ciphers.forEach((c) => { + builder.add(c); + }); + + this.index = builder.build(); + } + + async searchCiphers(query: string): Promise { + const results: CipherView[] = []; + if (this.index == null) { + return results; + } + + const ciphers = await this.cipherService.getAllDecrypted(); + const ciphersMap = new Map(); + ciphers.forEach((c) => { + ciphersMap.set(c.id, c); + }); + + query = this.transformQuery(query); + const searchResults = this.index.search(query); + searchResults.forEach((r) => { + if (ciphersMap.has(r.ref)) { + results.push(ciphersMap.get(r.ref)); + } + }); + + return results; + } + + private transformQuery(query: string) { + if (query.indexOf('>') === 0) { + return query.substr(1).trimLeft(); + } + return '*' + query + '*'; + } +} From 52f3ea58d1d875a42c7b01995cba4b90dfd48dff Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 28 Feb 2018 21:03:23 -0500 Subject: [PATCH 0113/1626] add services to index --- src/abstractions/index.ts | 2 ++ src/services/index.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index a08ce2a787..a727ee11a8 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -1,5 +1,6 @@ export { ApiService } from './api.service'; export { AppIdService } from './appId.service'; +export { AuditService } from './audit.service'; export { AuthService } from './auth.service'; export { CipherService } from './cipher.service'; export { CollectionService } from './collection.service'; @@ -12,6 +13,7 @@ export { LogService } from './log.service'; export { MessagingService } from './messaging.service'; export { PasswordGenerationService } from './passwordGeneration.service'; export { PlatformUtilsService } from './platformUtils.service'; +export { SearchService } from './search.service'; export { SettingsService } from './settings.service'; export { StorageService } from './storage.service'; export { StateService } from './state.service'; diff --git a/src/services/index.ts b/src/services/index.ts index c69cafca30..006e7deaeb 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,5 +1,6 @@ export { ApiService } from './api.service'; export { AppIdService } from './appId.service'; +export { AuditService } from './audit.service'; export { AuthService } from './auth.service'; export { CipherService } from './cipher.service'; export { CollectionService } from './collection.service'; @@ -10,6 +11,7 @@ export { EnvironmentService } from './environment.service'; export { FolderService } from './folder.service'; export { LockService } from './lock.service'; export { PasswordGenerationService } from './passwordGeneration.service'; +export { SearchService } from './search.service'; export { SettingsService } from './settings.service'; export { StateService } from './state.service'; export { SyncService } from './sync.service'; From 063bb010db093877c05bd6fc81db00b08a0fd1f3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 1 Mar 2018 23:44:29 -0500 Subject: [PATCH 0114/1626] refactor for login uris and response model changes --- src/enums/index.ts | 1 + src/enums/uriMatchType.ts | 8 ++++ src/models/api/cardApi.ts | 17 ++++++++ src/models/api/fieldApi.ts | 13 ++++++ src/models/api/identityApi.ts | 41 ++++++++++++++++++ src/models/api/loginApi.ts | 21 +++++++++ src/models/api/loginUriApi.ts | 11 +++++ src/models/api/secureNoteApi.ts | 9 ++++ src/models/data/cardData.ts | 16 ++++--- src/models/data/cipherData.ts | 17 ++++---- src/models/data/fieldData.ts | 10 +++-- src/models/data/identityData.ts | 40 +++++++++--------- src/models/data/index.ts | 1 + src/models/data/loginData.ts | 23 +++++++--- src/models/data/loginUriData.ts | 13 ++++++ src/models/data/secureNoteData.ts | 6 ++- src/models/domain/field.ts | 2 +- src/models/domain/index.ts | 1 + src/models/domain/login.ts | 28 +++++++++--- src/models/domain/loginUri.ts | 31 ++++++++++++++ src/models/request/cipherRequest.ts | 35 +++++++++++---- src/models/response/cipherResponse.ts | 40 +++++++++++++++++- src/models/view/cipherView.ts | 4 -- src/models/view/index.ts | 1 + src/models/view/loginUriView.ts | 61 +++++++++++++++++++++++++++ src/models/view/loginView.ts | 43 ++++--------------- src/services/cipher.service.ts | 14 +++++- 27 files changed, 405 insertions(+), 102 deletions(-) create mode 100644 src/enums/uriMatchType.ts create mode 100644 src/models/api/cardApi.ts create mode 100644 src/models/api/fieldApi.ts create mode 100644 src/models/api/identityApi.ts create mode 100644 src/models/api/loginApi.ts create mode 100644 src/models/api/loginUriApi.ts create mode 100644 src/models/api/secureNoteApi.ts create mode 100644 src/models/data/loginUriData.ts create mode 100644 src/models/domain/loginUri.ts create mode 100644 src/models/view/loginUriView.ts diff --git a/src/enums/index.ts b/src/enums/index.ts index e0eac44ae0..740efbf9a9 100644 --- a/src/enums/index.ts +++ b/src/enums/index.ts @@ -5,3 +5,4 @@ export { FieldType } from './fieldType'; export { LogLevelType } from './logLevelType'; export { SecureNoteType } from './secureNoteType'; export { TwoFactorProviderType } from './twoFactorProviderType'; +export { UriMatchType } from './uriMatchType'; diff --git a/src/enums/uriMatchType.ts b/src/enums/uriMatchType.ts new file mode 100644 index 0000000000..ed6b6ab54f --- /dev/null +++ b/src/enums/uriMatchType.ts @@ -0,0 +1,8 @@ +export enum UriMatchType { + BaseDomain = 0, + FullHostname = 1, + StartsWith = 2, + Exact = 3, + RegularExpression = 4, + Never = 5, +} diff --git a/src/models/api/cardApi.ts b/src/models/api/cardApi.ts new file mode 100644 index 0000000000..f91c44ef16 --- /dev/null +++ b/src/models/api/cardApi.ts @@ -0,0 +1,17 @@ +export class CardApi { + 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; + } +} diff --git a/src/models/api/fieldApi.ts b/src/models/api/fieldApi.ts new file mode 100644 index 0000000000..59a8a06aaa --- /dev/null +++ b/src/models/api/fieldApi.ts @@ -0,0 +1,13 @@ +import { FieldType } from '../../enums/fieldType'; + +export class FieldApi { + name: string; + value: string; + type: FieldType; + + constructor(response: any) { + this.type = response.Type; + this.name = response.Name; + this.value = response.Value; + } +} diff --git a/src/models/api/identityApi.ts b/src/models/api/identityApi.ts new file mode 100644 index 0000000000..e498eb71fe --- /dev/null +++ b/src/models/api/identityApi.ts @@ -0,0 +1,41 @@ +export class IdentityApi { + 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; + } +} diff --git a/src/models/api/loginApi.ts b/src/models/api/loginApi.ts new file mode 100644 index 0000000000..ba9a9da11d --- /dev/null +++ b/src/models/api/loginApi.ts @@ -0,0 +1,21 @@ +import { LoginUriApi } from './loginUriApi'; + +export class LoginApi { + uris: LoginUriApi[]; + username: string; + password: string; + totp: string; + + constructor(data: any) { + this.username = data.Username; + this.password = data.Password; + this.totp = data.Totp; + + if (data.Uris) { + this.uris = []; + data.Uris.forEach((u: any) => { + this.uris.push(new LoginUriApi(u)); + }); + } + } +} diff --git a/src/models/api/loginUriApi.ts b/src/models/api/loginUriApi.ts new file mode 100644 index 0000000000..cdaa651e80 --- /dev/null +++ b/src/models/api/loginUriApi.ts @@ -0,0 +1,11 @@ +import { UriMatchType } from '../../enums/uriMatchType'; + +export class LoginUriApi { + uri: string; + match: UriMatchType = null; + + constructor(data: any) { + this.uri = data.Uri; + this.match = data.Match ? data.Match : null; + } +} diff --git a/src/models/api/secureNoteApi.ts b/src/models/api/secureNoteApi.ts new file mode 100644 index 0000000000..4a31aa27b2 --- /dev/null +++ b/src/models/api/secureNoteApi.ts @@ -0,0 +1,9 @@ +import { SecureNoteType } from '../../enums/secureNoteType'; + +export class SecureNoteApi { + type: SecureNoteType; + + constructor(data: any) { + this.type = data.Type; + } +} diff --git a/src/models/data/cardData.ts b/src/models/data/cardData.ts index 238621c72a..1a4edea4ea 100644 --- a/src/models/data/cardData.ts +++ b/src/models/data/cardData.ts @@ -1,3 +1,5 @@ +import { CardApi } from '../api/cardApi'; + export class CardData { cardholderName: string; brand: string; @@ -6,12 +8,12 @@ export class CardData { 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; + constructor(data: CardApi) { + this.cardholderName = data.cardholderName; + this.brand = data.brand; + this.number = data.number; + this.expMonth = data.expMonth; + this.expYear = data.expYear; + this.code = data.code; } } diff --git a/src/models/data/cipherData.ts b/src/models/data/cipherData.ts index 0a4f83c520..1a76ed5c79 100644 --- a/src/models/data/cipherData.ts +++ b/src/models/data/cipherData.ts @@ -40,6 +40,8 @@ export class CipherData { this.favorite = response.favorite; this.revisionDate = response.revisionDate; this.type = response.type; + this.name = response.name; + this.notes = response.notes; if (collectionIds != null) { this.collectionIds = collectionIds; @@ -47,29 +49,26 @@ export class CipherData { 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); + this.login = new LoginData(response.login); break; case CipherType.SecureNote: - this.secureNote = new SecureNoteData(response.data); + this.secureNote = new SecureNoteData(response.secureNote); break; case CipherType.Card: - this.card = new CardData(response.data); + this.card = new CardData(response.card); break; case CipherType.Identity: - this.identity = new IdentityData(response.data); + this.identity = new IdentityData(response.identity); break; default: break; } - if (response.data.Fields != null) { + if (response.fields != null) { this.fields = []; - response.data.Fields.forEach((field: any) => { + response.fields.forEach((field) => { this.fields.push(new FieldData(field)); }); } diff --git a/src/models/data/fieldData.ts b/src/models/data/fieldData.ts index cd0fb2c42b..0632d7349d 100644 --- a/src/models/data/fieldData.ts +++ b/src/models/data/fieldData.ts @@ -1,13 +1,15 @@ import { FieldType } from '../../enums/fieldType'; +import { FieldApi } from '../api/fieldApi'; + export class FieldData { type: FieldType; name: string; value: string; - constructor(response: any) { - this.type = response.Type; - this.name = response.Name; - this.value = response.Value; + constructor(response: FieldApi) { + this.type = response.type; + this.name = response.name; + this.value = response.value; } } diff --git a/src/models/data/identityData.ts b/src/models/data/identityData.ts index 34f2099595..d1ac84a7b2 100644 --- a/src/models/data/identityData.ts +++ b/src/models/data/identityData.ts @@ -1,3 +1,5 @@ +import { IdentityApi } from '../api/identityApi'; + export class IdentityData { title: string; firstName: string; @@ -18,24 +20,24 @@ export class IdentityData { 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; + constructor(data: IdentityApi) { + 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; } } diff --git a/src/models/data/index.ts b/src/models/data/index.ts index 7a5a86df2e..97732b6d76 100644 --- a/src/models/data/index.ts +++ b/src/models/data/index.ts @@ -6,4 +6,5 @@ export { FieldData } from './fieldData'; export { FolderData } from './folderData'; export { IdentityData } from './identityData'; export { LoginData } from './loginData'; +export { LoginUriData } from './loginUriData'; export { SecureNoteData } from './secureNoteData'; diff --git a/src/models/data/loginData.ts b/src/models/data/loginData.ts index 2052a7a889..f81e8e0897 100644 --- a/src/models/data/loginData.ts +++ b/src/models/data/loginData.ts @@ -1,13 +1,24 @@ +import { LoginApi } from '../api/loginApi'; +import { LoginUriApi } from '../api/loginUriApi'; + +import { LoginUriData } from './loginUriData'; + export class LoginData { - uri: string; + uris: LoginUriData[]; 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; + constructor(data: LoginApi) { + this.username = data.username; + this.password = data.password; + this.totp = data.totp; + + if (data.uris) { + this.uris = []; + data.uris.forEach((u) => { + this.uris.push(new LoginUriData(u)); + }); + } } } diff --git a/src/models/data/loginUriData.ts b/src/models/data/loginUriData.ts new file mode 100644 index 0000000000..71ad8079c1 --- /dev/null +++ b/src/models/data/loginUriData.ts @@ -0,0 +1,13 @@ +import { UriMatchType } from '../../enums/uriMatchType'; + +import { LoginUriApi } from '../api/loginUriApi'; + +export class LoginUriData { + uri: string; + match: UriMatchType = null; + + constructor(data: LoginUriApi) { + this.uri = data.uri; + this.match = data.match; + } +} diff --git a/src/models/data/secureNoteData.ts b/src/models/data/secureNoteData.ts index 4b27125a13..a508c1b31d 100644 --- a/src/models/data/secureNoteData.ts +++ b/src/models/data/secureNoteData.ts @@ -1,9 +1,11 @@ import { SecureNoteType } from '../../enums/secureNoteType'; +import { SecureNoteApi } from '../api/secureNoteApi'; + export class SecureNoteData { type: SecureNoteType; - constructor(data: any) { - this.type = data.Type; + constructor(data: SecureNoteApi) { + this.type = data.type; } } diff --git a/src/models/domain/field.ts b/src/models/domain/field.ts index 023fefe5a9..599dabff1c 100644 --- a/src/models/domain/field.ts +++ b/src/models/domain/field.ts @@ -9,7 +9,7 @@ import { FieldView } from '../view/fieldView'; export class Field extends Domain { name: CipherString; - vault: CipherString; + value: CipherString; type: FieldType; constructor(obj?: FieldData, alreadyEncrypted: boolean = false) { diff --git a/src/models/domain/index.ts b/src/models/domain/index.ts index 7597a746d3..99846ea2f2 100644 --- a/src/models/domain/index.ts +++ b/src/models/domain/index.ts @@ -10,6 +10,7 @@ export { Field } from './field'; export { Folder } from './folder'; export { Identity } from './identity'; export { Login } from './login'; +export { LoginUri } from './loginUri'; export { PasswordHistory } from './passwordHistory'; export { SecureNote } from './secureNote'; export { SymmetricCryptoKey } from './symmetricCryptoKey'; diff --git a/src/models/domain/login.ts b/src/models/domain/login.ts index 1c98dc7031..211a371069 100644 --- a/src/models/domain/login.ts +++ b/src/models/domain/login.ts @@ -1,12 +1,15 @@ +import { LoginUri } from './loginUri'; + import { LoginData } from '../data/loginData'; +import { LoginUriView } from '../view/loginUriView'; import { LoginView } from '../view/loginView'; import { CipherString } from './cipherString'; import Domain from './domain'; export class Login extends Domain { - uri: CipherString; + uris: LoginUri[]; username: CipherString; password: CipherString; totp: CipherString; @@ -18,19 +21,34 @@ export class Login extends Domain { } this.buildDomainModel(this, obj, { - uri: null, username: null, password: null, totp: null, }, alreadyEncrypted, []); + + if (obj.uris) { + this.uris = []; + obj.uris.forEach((u) => { + this.uris.push(new LoginUri(u, alreadyEncrypted)); + }); + } } - decrypt(orgId: string): Promise { - return this.decryptObj(new LoginView(this), { - uri: null, + async decrypt(orgId: string): Promise { + const view = await this.decryptObj(new LoginView(this), { username: null, password: null, totp: null, }, orgId); + + if (this.uris != null) { + view.uris = []; + for (let i = 0; i < this.uris.length; i++) { + const uri = await this.uris[i].decrypt(orgId); + view.uris.push(uri); + } + } + + return view; } } diff --git a/src/models/domain/loginUri.ts b/src/models/domain/loginUri.ts new file mode 100644 index 0000000000..0cb6b19a7f --- /dev/null +++ b/src/models/domain/loginUri.ts @@ -0,0 +1,31 @@ +import { UriMatchType } from '../../enums/uriMatchType'; + +import { LoginUriData } from '../data/loginUriData'; + +import { LoginUriView } from '../view/loginUriView'; + +import { CipherString } from './cipherString'; +import Domain from './domain'; + +export class LoginUri extends Domain { + uri: CipherString; + match: UriMatchType; + + constructor(obj?: LoginUriData, alreadyEncrypted: boolean = false) { + super(); + if (obj == null) { + return; + } + + this.match = obj.match; + this.buildDomainModel(this, obj, { + uri: null, + }, alreadyEncrypted, []); + } + + decrypt(orgId: string): Promise { + return this.decryptObj(new LoginUriView(this), { + uri: null, + }, orgId); + } +} diff --git a/src/models/request/cipherRequest.ts b/src/models/request/cipherRequest.ts index 232883f323..68b5c3170a 100644 --- a/src/models/request/cipherRequest.ts +++ b/src/models/request/cipherRequest.ts @@ -1,5 +1,14 @@ import { CipherType } from '../../enums/cipherType'; +import { Cipher } from '../domain/cipher'; +import { Field } from '../domain/field'; + +import { CardApi } from '../api/cardApi'; +import { FieldApi } from '../api/fieldApi'; +import { IdentityApi } from '../api/identityApi'; +import { LoginApi } from '../api/loginApi'; +import { SecureNoteApi } from '../api/secureNoteApi'; + export class CipherRequest { type: CipherType; folderId: string; @@ -7,13 +16,13 @@ export class CipherRequest { name: string; notes: string; favorite: boolean; - login: any; - secureNote: any; - card: any; - identity: any; - fields: any[]; + login: LoginApi; + secureNote: SecureNoteApi; + card: CardApi; + identity: IdentityApi; + fields: FieldApi[]; - constructor(cipher: any) { + constructor(cipher: Cipher) { this.type = cipher.type; this.folderId = cipher.folderId; this.organizationId = cipher.organizationId; @@ -24,11 +33,21 @@ export class CipherRequest { switch (this.type) { case CipherType.Login: this.login = { - uri: cipher.login.uri ? cipher.login.uri.encryptedString : null, + uris: 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, }; + + if (cipher.login.uris) { + this.login.uris = []; + cipher.login.uris.forEach((u) => { + this.login.uris.push({ + uri: u.uri ? u.uri.encryptedString : null, + match: u.match ? u.match : null, + }); + }); + } break; case CipherType.SecureNote: this.secureNote = { @@ -74,7 +93,7 @@ export class CipherRequest { if (cipher.fields) { this.fields = []; - cipher.fields.forEach((field: any) => { + cipher.fields.forEach((field) => { this.fields.push({ type: field.type, name: field.name ? field.name.encryptedString : null, diff --git a/src/models/response/cipherResponse.ts b/src/models/response/cipherResponse.ts index 241b8530a0..c8ed70ffd0 100644 --- a/src/models/response/cipherResponse.ts +++ b/src/models/response/cipherResponse.ts @@ -1,14 +1,26 @@ import { AttachmentResponse } from './attachmentResponse'; +import { CardApi } from '../api/cardApi'; +import { FieldApi } from '../api/fieldApi'; +import { IdentityApi } from '../api/identityApi'; +import { LoginApi } from '../api/loginApi'; +import { SecureNoteApi } from '../api/secureNoteApi'; + export class CipherResponse { id: string; organizationId: string; folderId: string; type: number; + name: string; + notes: string; + fields: FieldApi[]; + login: LoginApi; + card: CardApi; + identity: IdentityApi; + secureNote: SecureNoteApi; favorite: boolean; edit: boolean; organizationUseTotp: boolean; - data: any; revisionDate: string; attachments: AttachmentResponse[]; collectionIds: string[]; @@ -18,12 +30,36 @@ export class CipherResponse { this.organizationId = response.OrganizationId; this.folderId = response.FolderId; this.type = response.Type; + this.name = response.Name; + this.notes = response.Notes; this.favorite = response.Favorite; this.edit = response.Edit; this.organizationUseTotp = response.OrganizationUseTotp; - this.data = response.Data; this.revisionDate = response.RevisionDate; + if (response.Login != null) { + this.login = new LoginApi(response.Login); + } + + if (response.Card != null) { + this.card = new CardApi(response.Card); + } + + if (response.Identity != null) { + this.identity = new IdentityApi(response.Identity); + } + + if (response.SecureNote != null) { + this.secureNote = new SecureNoteApi(response.SecureNote); + } + + if (response.Fields != null) { + this.fields = []; + response.Fields.forEach((field: any) => { + this.fields.push(new FieldApi(field)); + }); + } + if (response.Attachments != null) { this.attachments = []; response.Attachments.forEach((attachment: any) => { diff --git a/src/models/view/cipherView.ts b/src/models/view/cipherView.ts index 6f00464841..9566c7d285 100644 --- a/src/models/view/cipherView.ts +++ b/src/models/view/cipherView.ts @@ -69,8 +69,4 @@ export class CipherView implements View { get login_username(): string { return this.login != null ? this.login.username : null; } - - get login_uri(): string { - return this.login != null ? this.login.uri : null; - } } diff --git a/src/models/view/index.ts b/src/models/view/index.ts index 0e07c256be..1bc3f9d6cb 100644 --- a/src/models/view/index.ts +++ b/src/models/view/index.ts @@ -3,6 +3,7 @@ export { CardView } from './cardView'; export { CipherView } from './cipherView'; export { FieldView } from './fieldView'; export { IdentityView } from './identityView'; +export { LoginUriView } from './loginUriView'; export { LoginView } from './loginView'; export { SecureNoteView } from './secureNoteView'; export { View } from './view'; diff --git a/src/models/view/loginUriView.ts b/src/models/view/loginUriView.ts new file mode 100644 index 0000000000..6222fef96e --- /dev/null +++ b/src/models/view/loginUriView.ts @@ -0,0 +1,61 @@ +import { UriMatchType } from '../../enums/uriMatchType'; + +import { View } from './view'; + +import { LoginUri } from '../domain/loginUri'; + +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +export class LoginUriView implements View { + match: UriMatchType = null; + + // tslint:disable + private _uri: string; + private _domain: string; + // tslint:enable + + constructor(u?: LoginUri) { + if (!u) { + return; + } + + this.match = u.match; + } + + get uri(): string { + return this._uri; + } + set uri(value: string) { + this._uri = value; + this._domain = null; + } + + get domain(): string { + if (this._domain == null && this.uri != null) { + const containerService = (window as any).bitwardenContainerService; + if (containerService) { + const platformUtilsService: PlatformUtilsService = containerService.getPlatformUtilsService(); + this._domain = platformUtilsService.getDomain(this.uri); + if (this._domain === '') { + this._domain = null; + } + } else { + throw new Error('window.bitwardenContainerService not initialized.'); + } + } + + return this._domain; + } + + get domainOrUri(): string { + return this.domain != null ? this.domain : this.uri; + } + + get isWebsite(): boolean { + return this.uri != null && (this.uri.indexOf('http://') === 0 || this.uri.indexOf('https://') === 0); + } + + get canLaunch(): boolean { + return this.uri != null && this.uri.indexOf('://') > -1; + } +} diff --git a/src/models/view/loginView.ts b/src/models/view/loginView.ts index c3554c9c38..4f73b3a739 100644 --- a/src/models/view/loginView.ts +++ b/src/models/view/loginView.ts @@ -1,3 +1,4 @@ +import { LoginUriView } from './loginUriView'; import { View } from './view'; import { Login } from '../domain/login'; @@ -7,12 +8,11 @@ import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; export class LoginView implements View { username: string; totp: string; + uris: LoginUriView[]; // tslint:disable - private _uri: string; private _username: string; private _password: string; - private _domain: string; private _maskedPassword: string; // tslint:enable @@ -20,14 +20,6 @@ export class LoginView implements View { // ctor } - get uri(): string { - return this._uri; - } - set uri(value: string) { - this._uri = value; - this._domain = null; - } - get password(): string { return this._password; } @@ -36,21 +28,8 @@ export class LoginView implements View { this._maskedPassword = null; } - get domain(): string { - if (this._domain == null && this.uri != null) { - const containerService = (window as any).bitwardenContainerService; - if (containerService) { - const platformUtilsService: PlatformUtilsService = containerService.getPlatformUtilsService(); - this._domain = platformUtilsService.getDomain(this.uri); - if (this._domain === '') { - this._domain = null; - } - } else { - throw new Error('window.bitwardenContainerService not initialized.'); - } - } - - return this._domain; + get uri(): string { + return this.hasUris ? this.uris[0].uri : null; } get maskedPassword(): string { @@ -68,15 +47,11 @@ export class LoginView implements View { return this.username; } - get domainOrUri(): string { - return this.domain != null ? this.domain : this.uri; - } - - get isWebsite(): boolean { - return this.uri != null && (this.uri.indexOf('http://') === 0 || this.uri.indexOf('https://') === 0); - } - get canLaunch(): boolean { - return this.uri != null && this.uri.indexOf('://') > -1; + return this.hasUris && this.uris[0].canLaunch; + } + + get hasUris(): boolean { + return this.uris != null && this.uris.length > 0; } } diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index fb8e532d62..5d3c60a64f 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -9,6 +9,7 @@ import Domain from '../models/domain/domain'; import { Field } from '../models/domain/field'; import { Identity } from '../models/domain/identity'; import { Login } from '../models/domain/login'; +import { LoginUri } from '../models/domain/loginUri'; import { SecureNote } from '../models/domain/secureNote'; import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; @@ -487,11 +488,22 @@ export class CipherService implements CipherServiceAbstraction { case CipherType.Login: cipher.login = new Login(); await this.encryptObjProperty(model.login, cipher.login, { - uri: null, username: null, password: null, totp: null, }, key); + + if (model.login.uris != null) { + cipher.login.uris = []; + for (let i = 0; i < model.login.uris.length; i++) { + const loginUri = new LoginUri(); + loginUri.match = model.login.uris[i].match; + await this.encryptObjProperty(model.login.uris[i], loginUri, { + uri: null, + }, key); + cipher.login.uris.push(loginUri); + } + } return; case CipherType.SecureNote: cipher.secureNote = new SecureNote(); From 20389402fc6610ffeaa003cfc663a41dc1061100 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 2 Mar 2018 08:15:31 -0500 Subject: [PATCH 0115/1626] comment out TODO code --- src/services/cipher.service.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 5d3c60a64f..f6ad034167 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -196,12 +196,13 @@ export class CipherService implements CipherServiceAbstraction { const ciphers = result[1]; return ciphers.filter((cipher) => { - if (domain && cipher.type === CipherType.Login && cipher.login.domain && - matchingDomains.indexOf(cipher.login.domain) > -1) { - return true; - } else if (includeOtherTypes && includeOtherTypes.indexOf(cipher.type) > -1) { - return true; - } + // TODO: uris + //if (domain && cipher.type === CipherType.Login && cipher.login.domain && + // matchingDomains.indexOf(cipher.login.domain) > -1) { + // return true; + //} else if (includeOtherTypes && includeOtherTypes.indexOf(cipher.type) > -1) { + // return true; + //} return false; }); From dc0befc9be805af687328b7f28c25f840b2aa733 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 2 Mar 2018 08:33:57 -0500 Subject: [PATCH 0116/1626] other types param now typed with CipherType --- src/abstractions/cipher.service.ts | 4 +++- src/services/cipher.service.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index 330ac71c6c..ca00f5e609 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -1,3 +1,5 @@ +import { CipherType } from '../enums/cipherType'; + import { CipherData } from '../models/data/cipherData'; import { Cipher } from '../models/domain/cipher'; @@ -18,7 +20,7 @@ export abstract class CipherService { getAll: () => Promise; getAllDecrypted: () => Promise; getAllDecryptedForGrouping: (groupingId: string, folder?: boolean) => Promise; - getAllDecryptedForDomain: (domain: string, includeOtherTypes?: any[]) => Promise; + getAllDecryptedForDomain: (domain: string, includeOtherTypes?: CipherType[]) => Promise; getLastUsedForDomain: (domain: string) => Promise; updateLastUsedDate: (id: string) => Promise; saveNeverDomain: (domain: string) => Promise; diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index f6ad034167..2c29ea9ab0 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -170,7 +170,7 @@ export class CipherService implements CipherServiceAbstraction { }); } - async getAllDecryptedForDomain(domain: string, includeOtherTypes?: any[]): Promise { + async getAllDecryptedForDomain(domain: string, includeOtherTypes?: CipherType[]): Promise { if (domain == null && !includeOtherTypes) { return Promise.resolve([]); } From ad4c81ed844f42e45467cd4467d993d30b1f8ce0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 2 Mar 2018 12:03:03 -0500 Subject: [PATCH 0117/1626] support for uri matching types to cipher service --- src/abstractions/cipher.service.ts | 4 +- src/abstractions/utils.service.ts | 1 + src/enums/uriMatchType.ts | 2 +- src/services/cipher.service.ts | 64 ++++++++++++++++++++++++------ src/services/search.service.ts | 2 +- src/services/utils.service.ts | 17 +++++++- 6 files changed, 72 insertions(+), 18 deletions(-) diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index ca00f5e609..2e344dffd8 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -20,8 +20,8 @@ export abstract class CipherService { getAll: () => Promise; getAllDecrypted: () => Promise; getAllDecryptedForGrouping: (groupingId: string, folder?: boolean) => Promise; - getAllDecryptedForDomain: (domain: string, includeOtherTypes?: CipherType[]) => Promise; - getLastUsedForDomain: (domain: string) => Promise; + getAllDecryptedForUrl: (url: string, includeOtherTypes?: CipherType[]) => Promise; + getLastUsedForUrl: (url: string) => Promise; updateLastUsedDate: (id: string) => Promise; saveNeverDomain: (domain: string) => Promise; saveWithServer: (cipher: Cipher) => Promise; diff --git a/src/abstractions/utils.service.ts b/src/abstractions/utils.service.ts index 5ef1cce54d..9dd87774b9 100644 --- a/src/abstractions/utils.service.ts +++ b/src/abstractions/utils.service.ts @@ -1,4 +1,5 @@ export abstract class UtilsService { copyToClipboard: (text: string, doc?: Document) => void; getHostname: (uriString: string) => string; + getHost: (uriString: string) => string; } diff --git a/src/enums/uriMatchType.ts b/src/enums/uriMatchType.ts index ed6b6ab54f..f42200ac95 100644 --- a/src/enums/uriMatchType.ts +++ b/src/enums/uriMatchType.ts @@ -1,6 +1,6 @@ export enum UriMatchType { BaseDomain = 0, - FullHostname = 1, + Host = 1, StartsWith = 2, Exact = 3, RegularExpression = 4, diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 2c29ea9ab0..664c394c3a 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -1,4 +1,5 @@ import { CipherType } from '../enums/cipherType'; +import { UriMatchType } from '../enums/uriMatchType'; import { CipherData } from '../models/data/cipherData'; @@ -31,9 +32,11 @@ import { ApiService } from '../abstractions/api.service'; import { CipherService as CipherServiceAbstraction } from '../abstractions/cipher.service'; import { CryptoService } from '../abstractions/crypto.service'; import { I18nService } from '../abstractions/i18n.service'; +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { SettingsService } from '../abstractions/settings.service'; import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; +import { UtilsService } from '../abstractions/utils.service'; const Keys = { ciphersPrefix: 'ciphers_', @@ -46,7 +49,8 @@ export class CipherService implements CipherServiceAbstraction { constructor(private cryptoService: CryptoService, private userService: UserService, private settingsService: SettingsService, private apiService: ApiService, - private storageService: StorageService, private i18nService: I18nService) { + private storageService: StorageService, private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, private utilsService: UtilsService) { } clearCache(): void { @@ -170,11 +174,12 @@ export class CipherService implements CipherServiceAbstraction { }); } - async getAllDecryptedForDomain(domain: string, includeOtherTypes?: CipherType[]): Promise { - if (domain == null && !includeOtherTypes) { + async getAllDecryptedForUrl(url: string, includeOtherTypes?: CipherType[]): Promise { + if (url == null && !includeOtherTypes) { return Promise.resolve([]); } + const domain = this.platformUtilsService.getDomain(url); const eqDomainsPromise = domain == null ? Promise.resolve([]) : this.settingsService.getEquivalentDomains().then((eqDomains: any[][]) => { let matches: any[] = []; @@ -196,20 +201,55 @@ export class CipherService implements CipherServiceAbstraction { const ciphers = result[1]; return ciphers.filter((cipher) => { - // TODO: uris - //if (domain && cipher.type === CipherType.Login && cipher.login.domain && - // matchingDomains.indexOf(cipher.login.domain) > -1) { - // return true; - //} else if (includeOtherTypes && includeOtherTypes.indexOf(cipher.type) > -1) { - // return true; - //} + if (includeOtherTypes && includeOtherTypes.indexOf(cipher.type) > -1) { + return true; + } + + if (url != null && cipher.type === CipherType.Login && cipher.login.uris != null) { + for (let i = 0; i < cipher.login.uris.length; i++) { + const u = cipher.login.uris[i]; + if (u.uri == null) { + continue; + } + + switch (u.match) { + case null: + case undefined: + case UriMatchType.BaseDomain: + if (domain != null && u.domain != null && matchingDomains.indexOf(u.domain) > -1) { + return true; + } + case UriMatchType.Host: + const urlHost = this.utilsService.getHost(url); + if (urlHost != null && urlHost === this.utilsService.getHost(u.uri)) { + return true; + } + case UriMatchType.Exact: + if (url === u.uri) { + return true; + } + case UriMatchType.StartsWith: + if (url.startsWith(u.uri)) { + return true; + } + case UriMatchType.RegularExpression: + const regex = new RegExp(u.uri, 'i'); + if (regex.test(url)) { + return true; + } + case UriMatchType.Never: + default: + break; + } + } + } return false; }); } - async getLastUsedForDomain(domain: string): Promise { - const ciphers = await this.getAllDecryptedForDomain(domain); + async getLastUsedForUrl(url: string): Promise { + const ciphers = await this.getAllDecryptedForUrl(url); if (ciphers.length === 0) { return null; } diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 3efe4a2ac4..1307d0db74 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -53,7 +53,7 @@ export class SearchService implements SearchServiceAbstraction { private transformQuery(query: string) { if (query.indexOf('>') === 0) { - return query.substr(1).trimLeft(); + return query.substr(1); } return '*' + query + '*'; } diff --git a/src/services/utils.service.ts b/src/services/utils.service.ts index 7f692ae191..d8312f6ab4 100644 --- a/src/services/utils.service.ts +++ b/src/services/utils.service.ts @@ -132,6 +132,16 @@ export class UtilsService implements UtilsServiceAbstraction { } static getHostname(uriString: string): string { + const url = UtilsService.getUrl(uriString); + return url != null ? url.hostname : null; + } + + static getHost(uriString: string): string { + const url = UtilsService.getUrl(uriString); + return url != null ? url.host : null; + } + + private static getUrl(uriString: string): URL { if (uriString == null) { return null; } @@ -143,8 +153,7 @@ export class UtilsService implements UtilsServiceAbstraction { if (uriString.startsWith('http://') || uriString.startsWith('https://')) { try { - const url = new URL(uriString); - return url.hostname; + return new URL(uriString); } catch (e) { } } @@ -155,6 +164,10 @@ export class UtilsService implements UtilsServiceAbstraction { return UtilsService.getHostname(uriString); } + getHost(uriString: string): string { + return UtilsService.getHost(uriString); + } + copyToClipboard(text: string, doc?: Document) { UtilsService.copyToClipboard(text, doc); } From f205a9cc9f226ed6d6add4f43d466e358fb54a0b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 2 Mar 2018 12:36:53 -0500 Subject: [PATCH 0118/1626] break from switch --- src/services/cipher.service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 664c394c3a..8cfac7b17f 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -219,24 +219,29 @@ export class CipherService implements CipherServiceAbstraction { if (domain != null && u.domain != null && matchingDomains.indexOf(u.domain) > -1) { return true; } + break; case UriMatchType.Host: const urlHost = this.utilsService.getHost(url); if (urlHost != null && urlHost === this.utilsService.getHost(u.uri)) { return true; } + break; case UriMatchType.Exact: if (url === u.uri) { return true; } + break; case UriMatchType.StartsWith: if (url.startsWith(u.uri)) { return true; } + break; case UriMatchType.RegularExpression: const regex = new RegExp(u.uri, 'i'); if (regex.test(url)) { return true; } + break; case UriMatchType.Never: default: break; From 0d80216921efa53dfc095fd25dde9b370a070da8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 2 Mar 2018 12:42:11 -0500 Subject: [PATCH 0119/1626] add http protocol if none fiven --- src/services/utils.service.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/utils.service.ts b/src/services/utils.service.ts index d8312f6ab4..efabb5ff0d 100644 --- a/src/services/utils.service.ts +++ b/src/services/utils.service.ts @@ -151,6 +151,10 @@ export class UtilsService implements UtilsServiceAbstraction { return null; } + if (uriString.indexOf('://') === -1) { + uriString = 'http://' + uriString; + } + if (uriString.startsWith('http://') || uriString.startsWith('https://')) { try { return new URL(uriString); From 913336280a1acab058ff9a9d6c3ea47235d9d2ee Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 2 Mar 2018 13:50:26 -0500 Subject: [PATCH 0120/1626] rename enum from basedomain => domain --- src/enums/uriMatchType.ts | 2 +- src/services/cipher.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/enums/uriMatchType.ts b/src/enums/uriMatchType.ts index f42200ac95..e4c0ef4844 100644 --- a/src/enums/uriMatchType.ts +++ b/src/enums/uriMatchType.ts @@ -1,5 +1,5 @@ export enum UriMatchType { - BaseDomain = 0, + Domain = 0, Host = 1, StartsWith = 2, Exact = 3, diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 8cfac7b17f..4ceb882531 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -215,7 +215,7 @@ export class CipherService implements CipherServiceAbstraction { switch (u.match) { case null: case undefined: - case UriMatchType.BaseDomain: + case UriMatchType.Domain: if (domain != null && u.domain != null && matchingDomains.indexOf(u.domain) > -1) { return true; } From 848f50afe7b9dc9e193a036c062ef2f0e4d93018 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 2 Mar 2018 15:15:15 -0500 Subject: [PATCH 0121/1626] uri match value can be 0 --- src/models/api/loginUriApi.ts | 2 +- src/models/request/cipherRequest.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/api/loginUriApi.ts b/src/models/api/loginUriApi.ts index cdaa651e80..b61c673421 100644 --- a/src/models/api/loginUriApi.ts +++ b/src/models/api/loginUriApi.ts @@ -6,6 +6,6 @@ export class LoginUriApi { constructor(data: any) { this.uri = data.Uri; - this.match = data.Match ? data.Match : null; + this.match = data.Match != null ? data.Match : null; } } diff --git a/src/models/request/cipherRequest.ts b/src/models/request/cipherRequest.ts index 68b5c3170a..a608cecb7a 100644 --- a/src/models/request/cipherRequest.ts +++ b/src/models/request/cipherRequest.ts @@ -44,7 +44,7 @@ export class CipherRequest { cipher.login.uris.forEach((u) => { this.login.uris.push({ uri: u.uri ? u.uri.encryptedString : null, - match: u.match ? u.match : null, + match: u.match != null ? u.match : null, }); }); } From 1f2cf2bcdfddc68a6d9ee0ed732d00c8bec65ad4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 3 Mar 2018 23:04:55 -0500 Subject: [PATCH 0122/1626] lock service invokes callback on lock --- src/services/lock.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts index ba54fec1bf..43b9e57d56 100644 --- a/src/services/lock.service.ts +++ b/src/services/lock.service.ts @@ -13,7 +13,7 @@ export class LockService implements LockServiceAbstraction { constructor(private cipherService: CipherService, private folderService: FolderService, private collectionService: CollectionService, private cryptoService: CryptoService, private platformUtilsService: PlatformUtilsService, private storageService: StorageService, - private messagingService: MessagingService) { + private messagingService: MessagingService, private lockedCallback: Function) { this.checkLock(); setInterval(() => this.checkLock(), 10 * 1000); // check every 10 seconds } @@ -60,6 +60,7 @@ export class LockService implements LockServiceAbstraction { this.cipherService.clearCache(); this.collectionService.clearCache(); this.messagingService.send('locked'); + this.lockedCallback(); } async setLockOption(lockOption: number): Promise { From 145188c005aeb9056e7c172e0c645ad8692e2224 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 6 Mar 2018 07:44:32 -0500 Subject: [PATCH 0123/1626] maskedPassword is constant 8 char --- src/models/view/loginView.ts | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/models/view/loginView.ts b/src/models/view/loginView.ts index 4f73b3a739..34a0c45685 100644 --- a/src/models/view/loginView.ts +++ b/src/models/view/loginView.ts @@ -7,40 +7,24 @@ import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; export class LoginView implements View { username: string; + password: string; totp: string; uris: LoginUriView[]; // tslint:disable private _username: string; - private _password: string; - private _maskedPassword: string; // tslint:enable constructor(l?: Login) { // ctor } - get password(): string { - return this._password; - } - set password(value: string) { - this._password = value; - this._maskedPassword = null; - } - get uri(): string { return this.hasUris ? this.uris[0].uri : null; } get maskedPassword(): string { - if (this._maskedPassword == null && this.password != null) { - this._maskedPassword = ''; - for (let i = 0; i < this.password.length; i++) { - this._maskedPassword += '•'; - } - } - - return this._maskedPassword; + return this.password != null ? '••••••••' : null; } get subTitle(): string { From a3beb04f7e09d548bc5223a702f76b9f4e8a4991 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 6 Mar 2018 08:17:39 -0500 Subject: [PATCH 0124/1626] mask field --- src/models/view/fieldView.ts | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/src/models/view/fieldView.ts b/src/models/view/fieldView.ts index b3f68a584f..e09d60960e 100644 --- a/src/models/view/fieldView.ts +++ b/src/models/view/fieldView.ts @@ -6,13 +6,9 @@ import { Field } from '../domain/field'; export class FieldView implements View { name: string; + value: string; type: FieldType; - // tslint:disable - private _value: string; - private _maskedValue: string; - // tslint:enable - constructor(f?: Field) { if (!f) { return; @@ -21,22 +17,7 @@ export class FieldView implements View { this.type = f.type; } - get value(): string { - return this._value; - } - set value(value: string) { - this._value = value; - this._maskedValue = null; - } - get maskedValue(): string { - if (this._maskedValue == null && this.value != null) { - this._maskedValue = ''; - for (let i = 0; i < this.value.length; i++) { - this._maskedValue += '•'; - } - } - - return this._maskedValue; + return this.value != null ? '••••••••' : null; } } From e27df6bc0999de121f4068d6859ea6e48322fd41 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 19 Mar 2018 19:42:38 -0400 Subject: [PATCH 0125/1626] null checks on sort function values --- src/services/cipher.service.ts | 10 ++++++++++ src/services/collection.service.ts | 10 ++++++++++ src/services/folder.service.ts | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 4ceb882531..5f7dc33a27 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -481,6 +481,16 @@ export class CipherService implements CipherServiceAbstraction { let aName = a.name; let bName = b.name; + if (aName == null && bName != null) { + return -1; + } + if (aName != null && bName == null) { + return 1; + } + if (aName == null && bName == null) { + return 0; + } + const result = this.i18nService.collator ? this.i18nService.collator.compare(aName, bName) : aName.localeCompare(bName); diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index f860f5d9d3..caca3c8920 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -127,6 +127,16 @@ export class CollectionService implements CollectionServiceAbstraction { private getLocaleSortingFunction(): (a: CollectionView, b: CollectionView) => number { return (a, b) => { + if (a.name == null && b.name != null) { + return -1; + } + if (a.name != null && b.name == null) { + return 1; + } + if (a.name == null && b.name == null) { + return 0; + } + return this.i18nService.collator ? this.i18nService.collator.compare(a.name, b.name) : a.name.localeCompare(b.name); }; diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index 002085c8a5..f596941bb3 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -166,6 +166,16 @@ export class FolderService implements FolderServiceAbstraction { private getLocaleSortingFunction(): (a: FolderView, b: FolderView) => number { return (a, b) => { + if (a.name == null && b.name != null) { + return -1; + } + if (a.name != null && b.name == null) { + return 1; + } + if (a.name == null && b.name == null) { + return 0; + } + return this.i18nService.collator ? this.i18nService.collator.compare(a.name, b.name) : a.name.localeCompare(b.name); }; From 0b57d8b7f368ba4e90fe13ef8bf6662f3cfdd820 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 21 Mar 2018 11:19:05 -0400 Subject: [PATCH 0126/1626] platform specific identity client id --- src/abstractions/platformUtils.service.ts | 1 + src/models/request/tokenRequest.ts | 4 ++-- src/services/api.service.ts | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index 5ed47ede54..05bbfaab7d 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -1,6 +1,7 @@ import { DeviceType } from '../enums/deviceType'; export abstract class PlatformUtilsService { + identityClientId: string; getDevice: () => DeviceType; getDeviceString: () => string; isFirefox: () => boolean; diff --git a/src/models/request/tokenRequest.ts b/src/models/request/tokenRequest.ts index 6cce0f23b0..4588903241 100644 --- a/src/models/request/tokenRequest.ts +++ b/src/models/request/tokenRequest.ts @@ -18,13 +18,13 @@ export class TokenRequest { this.device = device != null ? device : null; } - toIdentityToken() { + toIdentityToken(clientId: string) { const obj: any = { grant_type: 'password', username: this.email, password: this.masterPasswordHash, scope: 'api offline_access', - client_id: 'browser', + client_id: clientId, }; if (this.device) { diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 1e452af755..cc30228899 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -27,7 +27,7 @@ export class ApiService implements ApiServiceAbstraction { deviceType: string; logoutCallback: Function; - constructor(private tokenService: TokenService, platformUtilsService: PlatformUtilsService, + constructor(private tokenService: TokenService, private platformUtilsService: PlatformUtilsService, logoutCallback: Function) { this.logoutCallback = logoutCallback; this.deviceType = platformUtilsService.getDevice().toString(); @@ -75,7 +75,7 @@ export class ApiService implements ApiServiceAbstraction { async postIdentityToken(request: TokenRequest): Promise { const response = await fetch(new Request(this.identityBaseUrl + '/connect/token', { - body: this.qsStringify(request.toIdentityToken()), + body: this.qsStringify(request.toIdentityToken(this.platformUtilsService.identityClientId)), cache: 'no-cache', headers: new Headers({ 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', From e5fb0fe6c67007f0d31fa17c0da5d5dd8c9a0d4d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 28 Mar 2018 15:20:11 -0400 Subject: [PATCH 0127/1626] refresh clientid --- src/services/api.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/api.service.ts b/src/services/api.service.ts index cc30228899..7b27e6d5ae 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -417,7 +417,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.identityBaseUrl + '/connect/token', { body: this.qsStringify({ grant_type: 'refresh_token', - client_id: 'browser', + client_id: this.platformUtilsService.identityClientId, refresh_token: refreshToken, }), cache: 'no-cache', From bdbb01317d6b41a9c0f74d47bf89e8ed11631fdc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 28 Mar 2018 21:54:21 -0400 Subject: [PATCH 0128/1626] use client_id from decoded token --- src/services/api.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 7b27e6d5ae..35a5f0f8d1 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -414,10 +414,11 @@ export class ApiService implements ApiServiceAbstraction { throw new Error(); } + const decodedToken = this.tokenService.decodeToken(); const response = await fetch(new Request(this.identityBaseUrl + '/connect/token', { body: this.qsStringify({ grant_type: 'refresh_token', - client_id: this.platformUtilsService.identityClientId, + client_id: decodedToken.client_id, refresh_token: refreshToken, }), cache: 'no-cache', From bea9e06506424ba07764f68eaaaa9161a12da767 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 3 Apr 2018 15:11:03 -0400 Subject: [PATCH 0129/1626] added 2fa support for org duo --- src/enums/twoFactorProviderType.ts | 1 + src/services/auth.service.ts | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/enums/twoFactorProviderType.ts b/src/enums/twoFactorProviderType.ts index 9d767ae026..f866295c3b 100644 --- a/src/enums/twoFactorProviderType.ts +++ b/src/enums/twoFactorProviderType.ts @@ -5,4 +5,5 @@ export enum TwoFactorProviderType { Yubikey = 3, U2f = 4, Remember = 5, + OrganizationDuo = 6, } diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index a360dc1c1d..7395e7c420 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -51,6 +51,12 @@ export const TwoFactorProviders = { description: null as string, priority: 0, }, + [TwoFactorProviderType.OrganizationDuo]: { + type: TwoFactorProviderType.OrganizationDuo, + name: 'Duo (Organization)', + description: null as string, + priority: 10, + }, }; export class AuthService { @@ -76,6 +82,11 @@ export class AuthService { TwoFactorProviders[TwoFactorProviderType.Duo].description = this.i18nService.t('duoDesc'); + TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].name = + 'Duo (' + this.i18nService.t('organization') + ')'; + TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].description = + this.i18nService.t('duoOrganizationDesc'); + TwoFactorProviders[TwoFactorProviderType.U2f].name = this.i18nService.t('u2fTitle'); TwoFactorProviders[TwoFactorProviderType.U2f].description = this.i18nService.t('u2fDesc'); From 167558168c4ddff8eff67d7f12dfac47d5d25866 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 3 Apr 2018 15:27:14 -0400 Subject: [PATCH 0130/1626] add duo org support to old constants --- src/services/constants.service.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index e0c695432c..6718730e82 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -56,6 +56,7 @@ export class ConstantsService { authenticator: 0, email: 1, remember: 5, + organizationDuo: 6, }; twoFactorProviderInfo: any[]; @@ -114,6 +115,14 @@ export class ConstantsService { displayOrder: 4, priority: 0, }, + { + type: 6, + name: 'Duo (' + i18nService.organization + ')', + description: i18nService.duoOrganizationDesc, + active: true, + displayOrder: -1, + priority: 10, + }, ]; } } From 00bb0fdbdff119ea8b0e8163768b7717b76e67b7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 4 Apr 2018 08:22:18 -0400 Subject: [PATCH 0131/1626] convert appid to abstract class --- src/abstractions/appId.service.ts | 6 +++--- src/services/appId.service.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/abstractions/appId.service.ts b/src/abstractions/appId.service.ts index 72d3b11948..d087478ac4 100644 --- a/src/abstractions/appId.service.ts +++ b/src/abstractions/appId.service.ts @@ -1,4 +1,4 @@ -export interface AppIdService { - getAppId(): Promise; - getAnonymousAppId(): Promise; +export abstract class AppIdService { + getAppId: () => Promise; + getAnonymousAppId: () => Promise; } diff --git a/src/services/appId.service.ts b/src/services/appId.service.ts index 430a375571..ba60a92551 100644 --- a/src/services/appId.service.ts +++ b/src/services/appId.service.ts @@ -1,9 +1,9 @@ import { UtilsService } from './utils.service'; -import { AppIdService as AppIdServiceInterface } from '../abstractions/appId.service'; +import { AppIdService as AppIdServiceAbstraction } from '../abstractions/appId.service'; import { StorageService } from '../abstractions/storage.service'; -export class AppIdService implements AppIdServiceInterface { +export class AppIdService implements AppIdServiceAbstraction { constructor(private storageService: StorageService) { } From 579f970323ea0bbc0d26f17cb9454c142f47bf6a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 4 Apr 2018 08:22:55 -0400 Subject: [PATCH 0132/1626] move various angular bits into shared jslib --- package-lock.json | 146 +++++++++++++++++- package.json | 32 ++-- .../directives/api-action.directive.ts | 32 ++++ src/angular/directives/autofocus.directive.ts | 24 +++ .../directives/blur-click.directive.ts | 17 ++ .../directives/fallback-src.directive.ts | 20 +++ .../directives/stop-click.directive.ts | 13 ++ src/angular/directives/stop-prop.directive.ts | 13 ++ src/angular/pipes/i18n.pipe.ts | 17 ++ src/angular/services/auth-guard.service.ts | 31 ++++ src/angular/services/validation.service.ts | 37 +++++ 11 files changed, 366 insertions(+), 16 deletions(-) create mode 100644 src/angular/directives/api-action.directive.ts create mode 100644 src/angular/directives/autofocus.directive.ts create mode 100644 src/angular/directives/blur-click.directive.ts create mode 100644 src/angular/directives/fallback-src.directive.ts create mode 100644 src/angular/directives/stop-click.directive.ts create mode 100644 src/angular/directives/stop-prop.directive.ts create mode 100644 src/angular/pipes/i18n.pipe.ts create mode 100644 src/angular/services/auth-guard.service.ts create mode 100644 src/angular/services/validation.service.ts diff --git a/package-lock.json b/package-lock.json index c7cf6b454d..4a8f70fe13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,99 @@ { "name": "@bitwarden/jslib", - "version": "0.0.20", + "version": "0.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { + "@angular/animations": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-5.2.0.tgz", + "integrity": "sha512-JLR42YHiJppO4ruAkFxgbzghUDtHkXHkKPM8udd2qyt16T7e1OX7EEOrrmldUu59CC56tZnJ/32p4SrYmxyBSA==", + "dev": true, + "requires": { + "tslib": "1.9.0" + } + }, + "@angular/common": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-5.2.0.tgz", + "integrity": "sha512-yMFn2isC7/XOs56/2Kzzbb1AASHiwipAPOVFtKe7TdZQClO8fJXwCnk326rzr615+CG0eSBNQWeiFGyWN2riBA==", + "dev": true, + "requires": { + "tslib": "1.9.0" + } + }, + "@angular/compiler": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-5.2.0.tgz", + "integrity": "sha512-RfYa4ESgjGX0T0ob/Xz00IF7nd2xZkoyRy6oKgL82q42uzB3xZUDMrFNgeGxAUs3H22IkL46/5SSPOMOTMZ0NA==", + "dev": true, + "requires": { + "tslib": "1.9.0" + } + }, + "@angular/core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-5.2.0.tgz", + "integrity": "sha512-s2ne45DguNUubhC1YgybGECC4Tyx3G4EZCntUiRMDWWkmKXSK+6dgHMesyDo8R5Oat8VfN4Anf8l3JHS1He8kg==", + "dev": true, + "requires": { + "tslib": "1.9.0" + } + }, + "@angular/forms": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-5.2.0.tgz", + "integrity": "sha512-g1/SF9lY0ZwzJ0w4NXbFsTGGEuUdgtaZny8DmkaqtmA7idby3FW398X0tv25KQfVYKtL+p9Jp1Y8EI0CvrIsvw==", + "dev": true, + "requires": { + "tslib": "1.9.0" + } + }, + "@angular/http": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@angular/http/-/http-5.2.0.tgz", + "integrity": "sha512-V5Cl24dP3rCXTTQvDc0TIKoWqBRAa0DWAQbtr7iuDAt5a1vPGdKz5K1sEiiV6ziwX6gzjiwHjUvL+B+WbIUrQA==", + "dev": true, + "requires": { + "tslib": "1.9.0" + } + }, + "@angular/platform-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-5.2.0.tgz", + "integrity": "sha512-c6cR15MfopPwGZ097HdRuAi9+R9BhA3bRRFpP2HmrSSB/BW4ZNovUYwB2QUMSYbd9s0lYTtnavqGm6DKcyF2QA==", + "dev": true, + "requires": { + "tslib": "1.9.0" + } + }, + "@angular/platform-browser-dynamic": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-5.2.0.tgz", + "integrity": "sha512-xG1eNoi8sm4Jcly2y98r5mqYVe3XV8sUJCtOhvGBYtvt4dKEQ5tOns6fWQ0nUbl6Vv3Y0xgGUS1JCtfut3DuaQ==", + "dev": true, + "requires": { + "tslib": "1.9.0" + } + }, + "@angular/router": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-5.2.0.tgz", + "integrity": "sha512-VXDXtp2A1GQEUEhXg0ZzqHdTUERLgDSo3/Mmpzt+dgLMKlXDSCykcm4gINwE5VQLGD1zQvDFCCRv3seGRNfrqA==", + "dev": true, + "requires": { + "tslib": "1.9.0" + } + }, + "@angular/upgrade": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@angular/upgrade/-/upgrade-5.2.0.tgz", + "integrity": "sha512-ezWfhBCiP7RX+59scxfYfjDMRw+qq0BVbm/EfOXdYFU0NHWo7lXJ3v+cUi18G+5GVjzwRiJDIKWhw1QEyq2nug==", + "dev": true, + "requires": { + "tslib": "1.9.0" + } + }, "@types/fs-extra": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-4.0.0.tgz", @@ -34,7 +124,8 @@ "@types/lunr": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/lunr/-/lunr-2.1.5.tgz", - "integrity": "sha512-esk3CG25hRtHsVHm+LOjiSFYdw8be3uIY653WUwR43Bro914HSimPgPpqgajkhTJ0awK3RQfaIxP7zvbtCpcyg==" + "integrity": "sha512-esk3CG25hRtHsVHm+LOjiSFYdw8be3uIY653WUwR43Bro914HSimPgPpqgajkhTJ0awK3RQfaIxP7zvbtCpcyg==", + "dev": true }, "@types/marked": { "version": "0.3.0", @@ -57,7 +148,8 @@ "@types/node-forge": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.7.1.tgz", - "integrity": "sha1-XoS4q/QthOxenQg+jHVzsxVcYzQ=" + "integrity": "sha1-XoS4q/QthOxenQg+jHVzsxVcYzQ=", + "dev": true }, "@types/shelljs": { "version": "0.7.0", @@ -71,7 +163,8 @@ "@types/webcrypto": { "version": "0.0.28", "resolved": "https://registry.npmjs.org/@types/webcrypto/-/webcrypto-0.0.28.tgz", - "integrity": "sha1-sGFgOQzQAPsgH2LrAqw0Hr9+YP4=" + "integrity": "sha1-sGFgOQzQAPsgH2LrAqw0Hr9+YP4=", + "dev": true }, "align-text": { "version": "0.1.4", @@ -90,6 +183,12 @@ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", "dev": true }, + "angular2-toaster": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/angular2-toaster/-/angular2-toaster-4.0.2.tgz", + "integrity": "sha512-/ndYYbV/7WZx6ujm6avFUqfb+FKbrx7oT+3mYj8i0o9N26Ug+BseFjy6oRnlVVedl39yRP6hhea81QgKmoYbbQ==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -163,6 +262,12 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "core-js": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", + "integrity": "sha1-TekR5mew6ukSTjQlS1OupvxhjT4=", + "dev": true + }, "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -305,7 +410,8 @@ "lunr": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.1.6.tgz", - "integrity": "sha512-ydJpB8CX8cZ/VE+KMaYaFcZ6+o2LruM6NG76VXdflYTgluvVemz1lW4anE+pyBbLvxJHZdvD1Jy/fOqdzAEJog==" + "integrity": "sha512-ydJpB8CX8cZ/VE+KMaYaFcZ6+o2LruM6NG76VXdflYTgluvVemz1lW4anE+pyBbLvxJHZdvD1Jy/fOqdzAEJog==", + "dev": true }, "marked": { "version": "0.3.9", @@ -330,7 +436,8 @@ "node-forge": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz", - "integrity": "sha1-naYR6giYL0uUIGs760zJZl8gwwA=" + "integrity": "sha1-naYR6giYL0uUIGs760zJZl8gwwA=", + "dev": true }, "once": { "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -408,6 +515,15 @@ "glob": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz" } }, + "rxjs": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.6.tgz", + "integrity": "sha512-v4Q5HDC0FHAQ7zcBX7T2IL6O5ltl1a2GX4ENjPXg6SjDY69Cmx9v4113C99a4wGF16ClPv5Z8mghuYorVkg/kg==", + "dev": true, + "requires": { + "symbol-observable": "1.0.1" + } + }, "shelljs": { "version": "0.7.8", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", @@ -434,6 +550,18 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "symbol-observable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=", + "dev": true + }, + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==", + "dev": true + }, "tslint": { "version": "5.9.1", "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.9.1.tgz", @@ -839,6 +967,12 @@ "decamelize": "1.2.0", "window-size": "0.1.0" } + }, + "zone.js": { + "version": "0.8.19", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.19.tgz", + "integrity": "sha512-l9rofaOs6a4y1W8zt4pDmnCUCnYG377dG+5SZlXNWrTWYUuXFqcJZiOarhYiRVR0NI9sH/8ooPJiz4uprB/Mkg==", + "dev": true } } } diff --git a/package.json b/package.json index 935e5f8359..8a4632c076 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/jslib", - "version": "0.0.20", + "version": "0.0.0", "description": "Common code used across Bitwarden JavaScript projects.", "keywords": [ "bitwarden" @@ -27,16 +27,28 @@ "pub": "npm run build && npm publish --access public" }, "devDependencies": { - "rimraf": "^2.6.2", - "tslint": "^5.8.0", - "typedoc": "^0.9.0", - "typescript": "^2.7.1" - }, - "dependencies": { - "lunr": "2.1.6", - "node-forge": "0.7.1", + "@angular/animations": "5.2.0", + "@angular/common": "5.2.0", + "@angular/compiler": "5.2.0", + "@angular/core": "5.2.0", + "@angular/forms": "5.2.0", + "@angular/http": "5.2.0", + "@angular/platform-browser": "5.2.0", + "@angular/platform-browser-dynamic": "5.2.0", + "@angular/router": "5.2.0", + "@angular/upgrade": "5.2.0", "@types/lunr": "2.1.5", "@types/node-forge": "0.7.1", - "@types/webcrypto": "0.0.28" + "@types/webcrypto": "0.0.28", + "angular2-toaster": "4.0.2", + "core-js": "2.4.1", + "lunr": "2.1.6", + "node-forge": "0.7.1", + "rimraf": "^2.6.2", + "rxjs": "5.5.6", + "tslint": "^5.8.0", + "typedoc": "^0.9.0", + "typescript": "^2.7.1", + "zone.js": "0.8.19" } } diff --git a/src/angular/directives/api-action.directive.ts b/src/angular/directives/api-action.directive.ts new file mode 100644 index 0000000000..a04eb47947 --- /dev/null +++ b/src/angular/directives/api-action.directive.ts @@ -0,0 +1,32 @@ +import { + Directive, + ElementRef, + Input, + OnChanges, +} from '@angular/core'; + +import { ValidationService } from '../services/validation.service'; + +@Directive({ + selector: '[appApiAction]', +}) +export class ApiActionDirective implements OnChanges { + @Input() appApiAction: Promise; + + constructor(private el: ElementRef, private validationService: ValidationService) { } + + ngOnChanges(changes: any) { + if (this.appApiAction == null || this.appApiAction.then == null) { + return; + } + + this.el.nativeElement.loading = true; + + this.appApiAction.then((response: any) => { + this.el.nativeElement.loading = false; + }, (e: any) => { + this.el.nativeElement.loading = false; + this.validationService.showError(e); + }); + } +} diff --git a/src/angular/directives/autofocus.directive.ts b/src/angular/directives/autofocus.directive.ts new file mode 100644 index 0000000000..dd708e5ae4 --- /dev/null +++ b/src/angular/directives/autofocus.directive.ts @@ -0,0 +1,24 @@ +import { + Directive, + ElementRef, + Input, +} from '@angular/core'; + +@Directive({ + selector: '[appAutofocus]', +}) +export class AutofocusDirective { + @Input() set appAutofocus(condition: boolean | string) { + this.autofocus = condition === '' || condition === true; + } + + private autofocus: boolean; + + constructor(private el: ElementRef) { } + + ngOnInit() { + if (this.autofocus) { + this.el.nativeElement.focus(); + } + } +} diff --git a/src/angular/directives/blur-click.directive.ts b/src/angular/directives/blur-click.directive.ts new file mode 100644 index 0000000000..48555bb9ae --- /dev/null +++ b/src/angular/directives/blur-click.directive.ts @@ -0,0 +1,17 @@ +import { + Directive, + ElementRef, + HostListener, +} from '@angular/core'; + +@Directive({ + selector: '[appBlurClick]', +}) +export class BlurClickDirective { + constructor(private el: ElementRef) { + } + + @HostListener('click') onClick() { + this.el.nativeElement.blur(); + } +} diff --git a/src/angular/directives/fallback-src.directive.ts b/src/angular/directives/fallback-src.directive.ts new file mode 100644 index 0000000000..3e5e338144 --- /dev/null +++ b/src/angular/directives/fallback-src.directive.ts @@ -0,0 +1,20 @@ +import { + Directive, + ElementRef, + HostListener, + Input, +} from '@angular/core'; + +@Directive({ + selector: '[appFallbackSrc]', +}) +export class FallbackSrcDirective { + @Input('appFallbackSrc') appFallbackSrc: string; + + constructor(private el: ElementRef) { + } + + @HostListener('error') onError() { + this.el.nativeElement.src = this.appFallbackSrc; + } +} diff --git a/src/angular/directives/stop-click.directive.ts b/src/angular/directives/stop-click.directive.ts new file mode 100644 index 0000000000..0529556bc9 --- /dev/null +++ b/src/angular/directives/stop-click.directive.ts @@ -0,0 +1,13 @@ +import { + Directive, + HostListener, +} from '@angular/core'; + +@Directive({ + selector: '[appStopClick]', +}) +export class StopClickDirective { + @HostListener('click', ['$event']) onClick($event: MouseEvent) { + $event.preventDefault(); + } +} diff --git a/src/angular/directives/stop-prop.directive.ts b/src/angular/directives/stop-prop.directive.ts new file mode 100644 index 0000000000..b241f628cc --- /dev/null +++ b/src/angular/directives/stop-prop.directive.ts @@ -0,0 +1,13 @@ +import { + Directive, + HostListener, +} from '@angular/core'; + +@Directive({ + selector: '[appStopProp]', +}) +export class StopPropDirective { + @HostListener('click', ['$event']) onClick($event: MouseEvent) { + $event.stopPropagation(); + } +} diff --git a/src/angular/pipes/i18n.pipe.ts b/src/angular/pipes/i18n.pipe.ts new file mode 100644 index 0000000000..23115d1dbd --- /dev/null +++ b/src/angular/pipes/i18n.pipe.ts @@ -0,0 +1,17 @@ +import { + Pipe, + PipeTransform, +} from '@angular/core'; + +import { I18nService } from '../../abstractions/i18n.service'; + +@Pipe({ + name: 'i18n', +}) +export class I18nPipe implements PipeTransform { + constructor(private i18nService: I18nService) { } + + transform(id: string, p1?: string, p2?: string, p3?: string): string { + return this.i18nService.t(id, p1, p2, p3); + } +} diff --git a/src/angular/services/auth-guard.service.ts b/src/angular/services/auth-guard.service.ts new file mode 100644 index 0000000000..c388e2094c --- /dev/null +++ b/src/angular/services/auth-guard.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { + CanActivate, + Router, +} from '@angular/router'; + +import { CryptoService } from '../../abstractions/crypto.service'; +import { MessagingService } from '../../abstractions/messaging.service'; +import { UserService } from '../../abstractions/user.service'; + +@Injectable() +export class AuthGuardService implements CanActivate { + constructor(private cryptoService: CryptoService, private userService: UserService, private router: Router, + private messagingService: MessagingService) { } + + async canActivate() { + const isAuthed = await this.userService.isAuthenticated(); + if (!isAuthed) { + this.messagingService.send('logout'); + return false; + } + + const key = await this.cryptoService.getKey(); + if (key == null) { + this.router.navigate(['lock']); + return false; + } + + return true; + } +} diff --git a/src/angular/services/validation.service.ts b/src/angular/services/validation.service.ts new file mode 100644 index 0000000000..06c8bde835 --- /dev/null +++ b/src/angular/services/validation.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@angular/core'; + +import { ToasterService } from 'angular2-toaster'; + +import { I18nService } from '../../abstractions/i18n.service'; + +@Injectable() +export class ValidationService { + constructor(private toasterService: ToasterService, private i18nService: I18nService) { } + + showError(data: any): string[] { + const defaultErrorMessage = this.i18nService.t('unexpectedError'); + const errors: string[] = []; + + if (data == null || typeof data !== 'object') { + errors.push(defaultErrorMessage); + } else if (data.validationErrors == null) { + errors.push(data.message ? data.message : defaultErrorMessage); + } else { + for (const key in data.validationErrors) { + if (!data.validationErrors.hasOwnProperty(key)) { + continue; + } + + data.validationErrors[key].forEach((item: string) => { + errors.push(item); + }); + } + } + + if (errors.length > 0) { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), errors[0]); + } + + return errors; + } +} From 7b4d0a71de640ce14c3c446bfd2e8ce36635db3f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 4 Apr 2018 09:19:21 -0400 Subject: [PATCH 0133/1626] share login component --- package-lock.json | 9 +++ package.json | 1 + src/angular/components/login.component.ts | 67 +++++++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 src/angular/components/login.component.ts diff --git a/package-lock.json b/package-lock.json index 4a8f70fe13..9cab90a199 100644 --- a/package-lock.json +++ b/package-lock.json @@ -189,6 +189,15 @@ "integrity": "sha512-/ndYYbV/7WZx6ujm6avFUqfb+FKbrx7oT+3mYj8i0o9N26Ug+BseFjy6oRnlVVedl39yRP6hhea81QgKmoYbbQ==", "dev": true }, + "angulartics2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/angulartics2/-/angulartics2-5.0.1.tgz", + "integrity": "sha512-QYBp7km7xTf/57zKKnYreM0OQ1Pq0kd4L9HJTC79vy7+RG1XqrkA944jTGKDERLWtjEAlQuSyZMS9J5IZZ56sw==", + "dev": true, + "requires": { + "tslib": "1.9.0" + } + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", diff --git a/package.json b/package.json index 8a4632c076..6ec69a7ab1 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@types/node-forge": "0.7.1", "@types/webcrypto": "0.0.28", "angular2-toaster": "4.0.2", + "angulartics2": "5.0.1", "core-js": "2.4.1", "lunr": "2.1.6", "node-forge": "0.7.1", diff --git a/src/angular/components/login.component.ts b/src/angular/components/login.component.ts new file mode 100644 index 0000000000..951490e79f --- /dev/null +++ b/src/angular/components/login.component.ts @@ -0,0 +1,67 @@ +import { + Component, + EventEmitter, + Input, +} from '@angular/core'; +import { Router } from '@angular/router'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { AuthResult } from '../../models/domain/authResult'; + +import { AuthService } from '../../abstractions/auth.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { SyncService } from '../../abstractions/sync.service'; + +export class LoginComponent { + @Input() email: string = ''; + + masterPassword: string = ''; + showPassword: boolean = false; + formPromise: Promise; + + protected twoFactorRoute = '2fa'; + protected successRoute = 'vault'; + + constructor(protected authService: AuthService, protected router: Router, + protected analytics: Angulartics2, protected toasterService: ToasterService, + protected i18nService: I18nService, protected syncService: SyncService) { } + + async submit() { + if (this.email == null || this.email === '') { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('emailRequired')); + return; + } + if (this.email.indexOf('@') === -1) { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('invalidEmail')); + return; + } + if (this.masterPassword == null || this.masterPassword === '') { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassRequired')); + return; + } + + try { + this.formPromise = this.authService.logIn(this.email, this.masterPassword); + const response = await this.formPromise; + if (response.twoFactor) { + this.analytics.eventTrack.next({ action: 'Logged In To Two-step' }); + this.router.navigate([this.twoFactorRoute]); + } else { + this.syncService.fullSync(true); + this.analytics.eventTrack.next({ action: 'Logged In' }); + this.router.navigate([this.successRoute]); + } + } catch { } + } + + togglePassword() { + this.analytics.eventTrack.next({ action: 'Toggled Master Password on Login' }); + this.showPassword = !this.showPassword; + document.getElementById('masterPassword').focus(); + } +} From f855a8272c7dc3c3099643cf34e49d3089e6575e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 4 Apr 2018 09:47:43 -0400 Subject: [PATCH 0134/1626] share components with jslib --- .../components/environment.component.ts | 59 +++++++ src/angular/components/hint.component.ts | 41 +++++ src/angular/components/login.component.ts | 6 +- src/angular/components/register.component.ts | 78 ++++++++++ .../two-factor-options.component.ts | 66 ++++++++ .../components/two-factor.component.ts | 147 ++++++++++++++++++ 6 files changed, 392 insertions(+), 5 deletions(-) create mode 100644 src/angular/components/environment.component.ts create mode 100644 src/angular/components/hint.component.ts create mode 100644 src/angular/components/register.component.ts create mode 100644 src/angular/components/two-factor-options.component.ts create mode 100644 src/angular/components/two-factor.component.ts diff --git a/src/angular/components/environment.component.ts b/src/angular/components/environment.component.ts new file mode 100644 index 0000000000..3bc31043f1 --- /dev/null +++ b/src/angular/components/environment.component.ts @@ -0,0 +1,59 @@ +import { + EventEmitter, + Output, +} from '@angular/core'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { EnvironmentService } from '../../abstractions/environment.service'; +import { I18nService } from '../../abstractions/i18n.service'; + +export class EnvironmentComponent { + @Output() onSaved = new EventEmitter(); + + iconsUrl: string; + identityUrl: string; + apiUrl: string; + webVaultUrl: string; + baseUrl: string; + showCustom = false; + + constructor(protected analytics: Angulartics2, protected toasterService: ToasterService, + protected environmentService: EnvironmentService, protected i18nService: I18nService) { + this.baseUrl = environmentService.baseUrl || ''; + this.webVaultUrl = environmentService.webVaultUrl || ''; + this.apiUrl = environmentService.apiUrl || ''; + this.identityUrl = environmentService.identityUrl || ''; + this.iconsUrl = environmentService.iconsUrl || ''; + } + + async submit() { + const resUrls = await this.environmentService.setUrls({ + base: this.baseUrl, + api: this.apiUrl, + identity: this.identityUrl, + webVault: this.webVaultUrl, + icons: this.iconsUrl, + }); + + // re-set urls since service can change them, ex: prefixing https:// + this.baseUrl = resUrls.base; + this.apiUrl = resUrls.api; + this.identityUrl = resUrls.identity; + this.webVaultUrl = resUrls.webVault; + this.iconsUrl = resUrls.icons; + + this.analytics.eventTrack.next({ action: 'Set Environment URLs' }); + this.toasterService.popAsync('success', null, this.i18nService.t('environmentSaved')); + this.saved(); + } + + toggleCustom() { + this.showCustom = !this.showCustom; + } + + protected saved() { + this.onSaved.emit(); + } +} diff --git a/src/angular/components/hint.component.ts b/src/angular/components/hint.component.ts new file mode 100644 index 0000000000..be8a8ec8e4 --- /dev/null +++ b/src/angular/components/hint.component.ts @@ -0,0 +1,41 @@ +import { Router } from '@angular/router'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { PasswordHintRequest } from '../../models/request/passwordHintRequest'; + +import { ApiService } from '../../abstractions/api.service'; +import { I18nService } from '../../abstractions/i18n.service'; + +export class HintComponent { + email: string = ''; + formPromise: Promise; + + protected successRoute = 'login'; + + constructor(protected router: Router, protected analytics: Angulartics2, + protected toasterService: ToasterService, protected i18nService: I18nService, + protected apiService: ApiService) { } + + async submit() { + if (this.email == null || this.email === '') { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('emailRequired')); + return; + } + if (this.email.indexOf('@') === -1) { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('invalidEmail')); + return; + } + + try { + this.formPromise = this.apiService.postPasswordHint(new PasswordHintRequest(this.email)); + await this.formPromise; + this.analytics.eventTrack.next({ action: 'Requested Hint' }); + this.toasterService.popAsync('success', null, this.i18nService.t('masterPassSent')); + this.router.navigate([this.successRoute]); + } catch { } + } +} diff --git a/src/angular/components/login.component.ts b/src/angular/components/login.component.ts index 951490e79f..0da933fa28 100644 --- a/src/angular/components/login.component.ts +++ b/src/angular/components/login.component.ts @@ -1,8 +1,4 @@ -import { - Component, - EventEmitter, - Input, -} from '@angular/core'; +import { Input } from '@angular/core'; import { Router } from '@angular/router'; import { ToasterService } from 'angular2-toaster'; diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts new file mode 100644 index 0000000000..eb9c39f0da --- /dev/null +++ b/src/angular/components/register.component.ts @@ -0,0 +1,78 @@ +import { Router } from '@angular/router'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { RegisterRequest } from '../../models/request/registerRequest'; + +import { ApiService } from '../../abstractions/api.service'; +import { AuthService } from '../../abstractions/auth.service'; +import { CryptoService } from '../../abstractions/crypto.service'; +import { I18nService } from '../../abstractions/i18n.service'; + +export class RegisterComponent { + email: string = ''; + masterPassword: string = ''; + confirmMasterPassword: string = ''; + hint: string = ''; + showPassword: boolean = false; + formPromise: Promise; + + protected successRoute = 'login'; + + constructor(protected authService: AuthService, protected router: Router, + protected analytics: Angulartics2, protected toasterService: ToasterService, + protected i18nService: I18nService, protected cryptoService: CryptoService, + protected apiService: ApiService) { } + + async submit() { + if (this.email == null || this.email === '') { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('emailRequired')); + return; + } + if (this.email.indexOf('@') === -1) { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('invalidEmail')); + return; + } + if (this.masterPassword == null || this.masterPassword === '') { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassRequired')); + return; + } + if (this.masterPassword.length < 8) { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassLength')); + return; + } + if (this.masterPassword !== this.confirmMasterPassword) { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassDoesntMatch')); + return; + } + + try { + this.formPromise = this.register(); + await this.formPromise; + this.analytics.eventTrack.next({ action: 'Registered' }); + this.toasterService.popAsync('success', null, this.i18nService.t('newAccountCreated')); + this.router.navigate([this.successRoute]); + } catch { } + } + + togglePassword(confirmField: boolean) { + this.analytics.eventTrack.next({ action: 'Toggled Master Password on Register' }); + this.showPassword = !this.showPassword; + document.getElementById(confirmField ? 'masterPasswordRetype' : 'masterPassword').focus(); + } + + private async register() { + this.email = this.email.toLowerCase(); + const key = this.cryptoService.makeKey(this.masterPassword, this.email); + const encKey = await this.cryptoService.makeEncKey(key); + const hashedPassword = await this.cryptoService.hashPassword(this.masterPassword, key); + const request = new RegisterRequest(this.email, hashedPassword, this.hint, encKey.encryptedString); + await this.apiService.postRegister(request); + } +} diff --git a/src/angular/components/two-factor-options.component.ts b/src/angular/components/two-factor-options.component.ts new file mode 100644 index 0000000000..cad2734cf6 --- /dev/null +++ b/src/angular/components/two-factor-options.component.ts @@ -0,0 +1,66 @@ +import { + EventEmitter, + Input, + OnInit, + Output, +} from '@angular/core'; +import { Router } from '@angular/router'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; + +import { AuthService } from '../../abstractions/auth.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +import { TwoFactorProviders } from '../../services/auth.service'; + +export class TwoFactorOptionsComponent implements OnInit { + @Output() onProviderSelected = new EventEmitter(); + @Output() onRecoverSelected = new EventEmitter(); + + providers: any[] = []; + + constructor(protected authService: AuthService, protected router: Router, + protected analytics: Angulartics2, protected toasterService: ToasterService, + protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService) { } + + ngOnInit() { + if (this.authService.twoFactorProviders.has(TwoFactorProviderType.OrganizationDuo)) { + this.providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]); + } + + if (this.authService.twoFactorProviders.has(TwoFactorProviderType.Authenticator)) { + this.providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]); + } + + if (this.authService.twoFactorProviders.has(TwoFactorProviderType.Yubikey)) { + this.providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]); + } + + if (this.authService.twoFactorProviders.has(TwoFactorProviderType.Duo)) { + this.providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]); + } + + if (this.authService.twoFactorProviders.has(TwoFactorProviderType.U2f) && + this.platformUtilsService.supportsU2f(window)) { + this.providers.push(TwoFactorProviders[TwoFactorProviderType.U2f]); + } + + if (this.authService.twoFactorProviders.has(TwoFactorProviderType.Email)) { + this.providers.push(TwoFactorProviders[TwoFactorProviderType.Email]); + } + } + + choose(p: any) { + this.onProviderSelected.emit(p.type); + } + + recover() { + this.analytics.eventTrack.next({ action: 'Selected Recover' }); + this.platformUtilsService.launchUri('https://help.bitwarden.com/article/lost-two-step-device/'); + this.onRecoverSelected.emit(); + } +} diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts new file mode 100644 index 0000000000..2005bcac49 --- /dev/null +++ b/src/angular/components/two-factor.component.ts @@ -0,0 +1,147 @@ +import { OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { TwoFactorOptionsComponent } from './two-factor-options.component'; + +import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; + +import { TwoFactorEmailRequest } from '../../models/request/twoFactorEmailRequest'; + +import { ApiService } from '../../abstractions/api.service'; +import { AuthService } from '../../abstractions/auth.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { SyncService } from '../../abstractions/sync.service'; + +import { TwoFactorProviders } from '../../services/auth.service'; + +export class TwoFactorComponent implements OnInit { + token: string = ''; + remember: boolean = false; + u2fReady: boolean = false; + providers = TwoFactorProviders; + providerType = TwoFactorProviderType; + selectedProviderType: TwoFactorProviderType = TwoFactorProviderType.Authenticator; + u2fSupported: boolean = false; + u2f: any = null; + title: string = ''; + twoFactorEmail: string = null; + formPromise: Promise; + emailPromise: Promise; + + protected successRoute = 'vault'; + + constructor(protected authService: AuthService, protected router: Router, + protected analytics: Angulartics2, protected toasterService: ToasterService, + protected i18nService: I18nService, protected apiService: ApiService, + protected platformUtilsService: PlatformUtilsService, protected syncService: SyncService) { + this.u2fSupported = this.platformUtilsService.supportsU2f(window); + } + + async ngOnInit() { + if (this.authService.email == null || this.authService.masterPasswordHash == null || + this.authService.twoFactorProviders == null) { + this.router.navigate(['login']); + return; + } + + this.selectedProviderType = this.authService.getDefaultTwoFactorProvider(this.u2fSupported); + await this.init(); + } + + async init() { + if (this.selectedProviderType == null) { + this.title = this.i18nService.t('loginUnavailable'); + return; + } + + this.title = (TwoFactorProviders as any)[this.selectedProviderType].name; + const params = this.authService.twoFactorProviders.get(this.selectedProviderType); + switch (this.selectedProviderType) { + case TwoFactorProviderType.U2f: + if (!this.u2fSupported) { + break; + } + + const challenges = JSON.parse(params.Challenges); + // TODO: init u2f + break; + case TwoFactorProviderType.Duo: + case TwoFactorProviderType.OrganizationDuo: + setTimeout(() => { + (window as any).Duo.init({ + host: params.Host, + sig_request: params.Signature, + submit_callback: async (f: HTMLFormElement) => { + const sig = f.querySelector('input[name="sig_response"]') as HTMLInputElement; + if (sig != null) { + this.token = sig.value; + await this.submit(); + } + }, + }); + }); + break; + case TwoFactorProviderType.Email: + this.twoFactorEmail = params.Email; + if (this.authService.twoFactorProviders.size > 1) { + await this.sendEmail(false); + } + break; + default: + break; + } + } + + async submit() { + if (this.token == null || this.token === '') { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('verificationCodeRequired')); + return; + } + + if (this.selectedProviderType === TwoFactorProviderType.U2f) { + // TODO: stop U2f + } else if (this.selectedProviderType === TwoFactorProviderType.Email || + this.selectedProviderType === TwoFactorProviderType.Authenticator) { + this.token = this.token.replace(' ', '').trim(); + } + + try { + this.formPromise = this.authService.logInTwoFactor(this.selectedProviderType, this.token, this.remember); + await this.formPromise; + this.syncService.fullSync(true); + this.analytics.eventTrack.next({ action: 'Logged In From Two-step' }); + this.router.navigate([this.successRoute]); + } catch { + if (this.selectedProviderType === TwoFactorProviderType.U2f) { + // TODO: start U2F again + } + } + } + + async sendEmail(doToast: boolean) { + if (this.selectedProviderType !== TwoFactorProviderType.Email) { + return; + } + + if (this.emailPromise != null) { + return; + } + + try { + const request = new TwoFactorEmailRequest(this.authService.email, this.authService.masterPasswordHash); + this.emailPromise = this.apiService.postTwoFactorEmail(request); + await this.emailPromise; + if (doToast) { + this.toasterService.popAsync('success', null, + this.i18nService.t('verificationCodeEmailSent', this.twoFactorEmail)); + } + } catch { } + + this.emailPromise = null; + } +} From f673bd62d7abb773fa5a6abfb5307b7c3feca59b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 4 Apr 2018 14:18:42 -0400 Subject: [PATCH 0135/1626] move bow-row direct, and selected provider support --- src/abstractions/auth.service.ts | 1 + src/angular/directives/box-row.directive.ts | 49 +++++++++++++++++++++ src/services/auth.service.ts | 8 ++++ 3 files changed, 58 insertions(+) create mode 100644 src/angular/directives/box-row.directive.ts diff --git a/src/abstractions/auth.service.ts b/src/abstractions/auth.service.ts index 08ca102729..4b2b2434d0 100644 --- a/src/abstractions/auth.service.ts +++ b/src/abstractions/auth.service.ts @@ -6,6 +6,7 @@ export abstract class AuthService { email: string; masterPasswordHash: string; twoFactorProviders: Map; + selectedTwoFactorProviderType: TwoFactorProviderType; logIn: (email: string, masterPassword: string) => Promise; logInTwoFactor: (twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, diff --git a/src/angular/directives/box-row.directive.ts b/src/angular/directives/box-row.directive.ts new file mode 100644 index 0000000000..241a5b3f82 --- /dev/null +++ b/src/angular/directives/box-row.directive.ts @@ -0,0 +1,49 @@ +import { + Directive, + ElementRef, + HostListener, + OnInit, +} from '@angular/core'; + +@Directive({ + selector: '[appBoxRow]', +}) +export class BoxRowDirective implements OnInit { + el: HTMLElement = null; + formEls: NodeListOf; + + constructor(private elRef: ElementRef) { + this.el = elRef.nativeElement; + } + + ngOnInit(): void { + this.formEls = this.el.querySelectorAll('input:not([type="hidden"]), select, textarea'); + this.formEls.forEach((formEl) => { + formEl.addEventListener('focus', (event: Event) => { + this.el.classList.add('active'); + }, false); + + formEl.addEventListener('blur', (event: Event) => { + this.el.classList.remove('active'); + }, false); + }); + } + + @HostListener('click', ['$event']) onClick(event: Event) { + if (event.target !== this.el) { + return; + } + + if (this.formEls.length > 0) { + const formEl = (this.formEls[0] as HTMLElement); + if (formEl.tagName.toLowerCase() === 'input') { + const inputEl = (formEl as HTMLInputElement); + if (inputEl.type != null && inputEl.type.toLowerCase() === 'checkbox') { + inputEl.click(); + return; + } + } + formEl.focus(); + } + } +} diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 7395e7c420..348c2c1d84 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -63,6 +63,7 @@ export class AuthService { email: string; masterPasswordHash: string; twoFactorProviders: Map; + selectedTwoFactorProviderType: TwoFactorProviderType = null; private key: SymmetricCryptoKey; @@ -95,6 +96,7 @@ export class AuthService { } async logIn(email: string, masterPassword: string): Promise { + this.selectedTwoFactorProviderType = null; email = email.toLowerCase(); const key = this.cryptoService.makeKey(masterPassword, email); const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); @@ -117,6 +119,11 @@ export class AuthService { return null; } + if (this.selectedTwoFactorProviderType != null && + this.twoFactorProviders.has(this.selectedTwoFactorProviderType)) { + return this.selectedTwoFactorProviderType; + } + let providerType: TwoFactorProviderType = null; let providerPriority = -1; this.twoFactorProviders.forEach((value, type) => { @@ -188,5 +195,6 @@ export class AuthService { this.email = null; this.masterPasswordHash = null; this.twoFactorProviders = null; + this.selectedTwoFactorProviderType = null; } } From 013bf20a35c74383389337ac2c5ae9ac19a0bba7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 4 Apr 2018 16:27:30 -0400 Subject: [PATCH 0136/1626] add u2f support to two factor component --- .../components/two-factor.component.ts | 76 ++++++++++++++++--- src/misc/u2f.ts | 73 ++++++++++++++++++ 2 files changed, 139 insertions(+), 10 deletions(-) create mode 100644 src/misc/u2f.ts diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 2005bcac49..eaba393d5b 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -1,4 +1,7 @@ -import { OnInit } from '@angular/core'; +import { + OnDestroy, + OnInit, +} from '@angular/core'; import { Router } from '@angular/router'; import { ToasterService } from 'angular2-toaster'; @@ -12,13 +15,16 @@ import { TwoFactorEmailRequest } from '../../models/request/twoFactorEmailReques import { ApiService } from '../../abstractions/api.service'; import { AuthService } from '../../abstractions/auth.service'; +import { EnvironmentService } from '../../abstractions/environment.service'; import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { SyncService } from '../../abstractions/sync.service'; import { TwoFactorProviders } from '../../services/auth.service'; -export class TwoFactorComponent implements OnInit { +import { U2f } from '../../misc/u2f'; + +export class TwoFactorComponent implements OnInit, OnDestroy { token: string = ''; remember: boolean = false; u2fReady: boolean = false; @@ -26,7 +32,7 @@ export class TwoFactorComponent implements OnInit { providerType = TwoFactorProviderType; selectedProviderType: TwoFactorProviderType = TwoFactorProviderType.Authenticator; u2fSupported: boolean = false; - u2f: any = null; + u2f: U2f = null; title: string = ''; twoFactorEmail: string = null; formPromise: Promise; @@ -37,7 +43,8 @@ export class TwoFactorComponent implements OnInit { constructor(protected authService: AuthService, protected router: Router, protected analytics: Angulartics2, protected toasterService: ToasterService, protected i18nService: I18nService, protected apiService: ApiService, - protected platformUtilsService: PlatformUtilsService, protected syncService: SyncService) { + protected platformUtilsService: PlatformUtilsService, protected syncService: SyncService, + protected win: Window, protected environmentService: EnvironmentService) { this.u2fSupported = this.platformUtilsService.supportsU2f(window); } @@ -48,26 +55,62 @@ export class TwoFactorComponent implements OnInit { return; } + if (this.win != null && this.u2fSupported) { + let customWebVaultUrl: string = null; + if (this.environmentService.baseUrl) { + customWebVaultUrl = this.environmentService.baseUrl; + } + else if (this.environmentService.webVaultUrl) { + customWebVaultUrl = this.environmentService.webVaultUrl; + } + + this.u2f = new U2f(this.win, customWebVaultUrl, (token: string) => { + this.token = token; + this.submit(); + }, (error: string) => { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), error); + }, (info: string) => { + if (info === 'ready') { + this.u2fReady = true; + } + }); + } + this.selectedProviderType = this.authService.getDefaultTwoFactorProvider(this.u2fSupported); await this.init(); } + ngOnDestroy(): void { + this.cleanupU2f(); + this.u2f = null; + } + async init() { if (this.selectedProviderType == null) { this.title = this.i18nService.t('loginUnavailable'); return; } + this.cleanupU2f(); this.title = (TwoFactorProviders as any)[this.selectedProviderType].name; const params = this.authService.twoFactorProviders.get(this.selectedProviderType); switch (this.selectedProviderType) { case TwoFactorProviderType.U2f: - if (!this.u2fSupported) { + if (!this.u2fSupported || this.u2f == null) { break; } const challenges = JSON.parse(params.Challenges); - // TODO: init u2f + if (challenges.length > 0) { + this.u2f.init({ + appId: challenges[0].appId, + challenge: challenges[0].challenge, + keys: [{ + version: challenges[0].version, + keyHandle: challenges[0].keyHandle + }], + }); + } break; case TwoFactorProviderType.Duo: case TwoFactorProviderType.OrganizationDuo: @@ -104,7 +147,11 @@ export class TwoFactorComponent implements OnInit { } if (this.selectedProviderType === TwoFactorProviderType.U2f) { - // TODO: stop U2f + if (this.u2f != null) { + this.u2f.stop(); + } else { + return; + } } else if (this.selectedProviderType === TwoFactorProviderType.Email || this.selectedProviderType === TwoFactorProviderType.Authenticator) { this.token = this.token.replace(' ', '').trim(); @@ -116,9 +163,11 @@ export class TwoFactorComponent implements OnInit { this.syncService.fullSync(true); this.analytics.eventTrack.next({ action: 'Logged In From Two-step' }); this.router.navigate([this.successRoute]); - } catch { - if (this.selectedProviderType === TwoFactorProviderType.U2f) { - // TODO: start U2F again + } catch (e) { + if (this.selectedProviderType === TwoFactorProviderType.U2f && this.u2f != null) { + this.u2f.start(); + } else { + throw e; } } } @@ -144,4 +193,11 @@ export class TwoFactorComponent implements OnInit { this.emailPromise = null; } + + private cleanupU2f() { + if (this.u2f != null) { + this.u2f.stop(); + this.u2f.cleanup(); + } + } } diff --git a/src/misc/u2f.ts b/src/misc/u2f.ts new file mode 100644 index 0000000000..cb1696c62f --- /dev/null +++ b/src/misc/u2f.ts @@ -0,0 +1,73 @@ +export class U2f { + private iframe: HTMLIFrameElement = null; + private connectorLink: HTMLAnchorElement; + + constructor(private win: Window, private webVaultUrl: string, private successCallback: Function, + private errorCallback: Function, private infoCallback: Function) { + this.connectorLink = win.document.createElement('a'); + this.webVaultUrl = webVaultUrl != null && webVaultUrl !== '' ? webVaultUrl : 'https://vault.bitwarden.com'; + } + + init(data: any): void { + this.connectorLink.href = this.webVaultUrl + '/u2f-connector.html' + + '?data=' + this.base64Encode(JSON.stringify(data)) + + '&parent=' + encodeURIComponent(this.win.document.location.href) + + '&v=1'; + + this.iframe = this.win.document.getElementById('u2f_iframe') as HTMLIFrameElement; + this.iframe.src = this.connectorLink.href; + + this.win.addEventListener('message', (e) => this.parseMessage(e), false); + } + + stop() { + this.sendMessage('stop'); + } + + start() { + this.sendMessage('start'); + } + + sendMessage(message: any) { + if (!this.iframe || !this.iframe.src || !this.iframe.contentWindow) { + return; + } + + this.iframe.contentWindow.postMessage(message, this.iframe.src); + } + + base64Encode(str: string): string { + return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => { + return String.fromCharCode(('0x' + p1) as any); + })); + } + + cleanup() { + this.win.removeEventListener('message', (e) => this.parseMessage(e), false); + } + + private parseMessage(event: any) { + if (!this.validMessage(event)) { + this.errorCallback('Invalid message.'); + return; + } + + const parts: string[] = event.data.split('|'); + if (parts[0] === 'success' && this.successCallback) { + this.successCallback(parts[1]); + } else if (parts[0] === 'error' && this.errorCallback) { + this.errorCallback(parts[1]); + } else if (parts[0] === 'info' && this.infoCallback) { + this.infoCallback(parts[1]); + } + } + + private validMessage(event: any) { + if (!event.origin || event.origin === '' || event.origin !== (this.connectorLink as any).origin) { + return false; + } + + return event.data.indexOf('success|') === 0 || event.data.indexOf('error|') === 0 || + event.data.indexOf('info|') === 0; + } +} From fd19efa9f260addca1f0fc14dd1641a276a22759 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 4 Apr 2018 16:51:11 -0400 Subject: [PATCH 0137/1626] do not init duo if safari --- src/angular/components/two-factor.component.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index eaba393d5b..610aeb1841 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -114,6 +114,10 @@ export class TwoFactorComponent implements OnInit, OnDestroy { break; case TwoFactorProviderType.Duo: case TwoFactorProviderType.OrganizationDuo: + if (this.platformUtilsService.isSafari()) { + break; + } + setTimeout(() => { (window as any).Duo.init({ host: params.Host, From d429cd2199e8a0880dfc0edbaea5fcad874eb424 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 4 Apr 2018 17:04:31 -0400 Subject: [PATCH 0138/1626] more icon component into jslib --- src/angular/app.d.ts | 1 + src/angular/components/icon.component.html | 4 + src/angular/components/icon.component.ts | 91 +++++++++++++++++++ .../components/two-factor.component.ts | 3 +- 4 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 src/angular/app.d.ts create mode 100644 src/angular/components/icon.component.html create mode 100644 src/angular/components/icon.component.ts diff --git a/src/angular/app.d.ts b/src/angular/app.d.ts new file mode 100644 index 0000000000..7bf7506fcf --- /dev/null +++ b/src/angular/app.d.ts @@ -0,0 +1 @@ +declare module '*.html'; diff --git a/src/angular/components/icon.component.html b/src/angular/components/icon.component.html new file mode 100644 index 0000000000..fd42b99dde --- /dev/null +++ b/src/angular/components/icon.component.html @@ -0,0 +1,4 @@ +
+ + +
diff --git a/src/angular/components/icon.component.ts b/src/angular/components/icon.component.ts new file mode 100644 index 0000000000..7304bed83f --- /dev/null +++ b/src/angular/components/icon.component.ts @@ -0,0 +1,91 @@ +import * as template from './icon.component.html'; + +import { + Component, + Input, + OnChanges, +} from '@angular/core'; + +import { CipherType } from '../../enums/cipherType'; + +import { EnvironmentService } from '../../abstractions/environment.service'; +import { StateService } from '../../abstractions/state.service'; + +import { ConstantsService } from '../../services/constants.service'; + +@Component({ + selector: 'app-vault-icon', + template: template, +}) +export class IconComponent implements OnChanges { + @Input() cipher: any; + icon: string; + image: string; + fallbackImage: string; + imageEnabled: boolean; + + private iconsUrl: string; + + constructor(private environmentService: EnvironmentService, private stateService: StateService) { + this.iconsUrl = environmentService.iconsUrl; + if (!this.iconsUrl) { + if (environmentService.baseUrl) { + this.iconsUrl = environmentService.baseUrl + '/icons'; + } else { + this.iconsUrl = 'https://icons.bitwarden.com'; + } + } + } + + async ngOnChanges() { + this.imageEnabled = !(await this.stateService.get(ConstantsService.disableFaviconKey)); + + switch (this.cipher.type) { + case CipherType.Login: + this.icon = 'fa-globe'; + this.setLoginIcon(); + break; + case CipherType.SecureNote: + this.icon = 'fa-sticky-note-o'; + break; + case CipherType.Card: + this.icon = 'fa-credit-card'; + break; + case CipherType.Identity: + this.icon = 'fa-id-card-o'; + break; + default: + break; + } + } + + private setLoginIcon() { + if (this.cipher.login.uri) { + let hostnameUri = this.cipher.login.uri; + let isWebsite = false; + + if (hostnameUri.indexOf('androidapp://') === 0) { + this.icon = 'fa-android'; + this.image = null; + } else if (hostnameUri.indexOf('iosapp://') === 0) { + this.icon = 'fa-apple'; + this.image = null; + } else if (this.imageEnabled && hostnameUri.indexOf('://') === -1 && hostnameUri.indexOf('.') > -1) { + hostnameUri = 'http://' + hostnameUri; + isWebsite = true; + } else if (this.imageEnabled) { + isWebsite = hostnameUri.indexOf('http') === 0 && hostnameUri.indexOf('.') > -1; + } + + if (this.imageEnabled && isWebsite) { + try { + const url = new URL(hostnameUri); + this.image = this.iconsUrl + '/' + url.hostname + '/icon.png'; + this.fallbackImage = 'images/fa-globe.png'; + } catch (e) { } + } + } else { + this.image = null; + } + } +} diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 610aeb1841..7ab1608e70 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -59,8 +59,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { let customWebVaultUrl: string = null; if (this.environmentService.baseUrl) { customWebVaultUrl = this.environmentService.baseUrl; - } - else if (this.environmentService.webVaultUrl) { + } else if (this.environmentService.webVaultUrl) { customWebVaultUrl = this.environmentService.webVaultUrl; } From a0ca51dda4ebbf710284a0f4e59d47ac8f31c8e7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 4 Apr 2018 22:59:14 -0400 Subject: [PATCH 0139/1626] lock component to jslib --- src/angular/components/lock.component.ts | 58 +++++++++++++++++++ .../components/two-factor.component.ts | 3 +- 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 src/angular/components/lock.component.ts diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts new file mode 100644 index 0000000000..a109cf31b0 --- /dev/null +++ b/src/angular/components/lock.component.ts @@ -0,0 +1,58 @@ +import { Router } from '@angular/router'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { CryptoService } from '../../abstractions/crypto.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { MessagingService } from '../../abstractions/messaging.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { UserService } from '../../abstractions/user.service'; + +export class LockComponent { + masterPassword: string = ''; + showPassword: boolean = false; + + protected successRoute: string = 'vault'; + + constructor(protected router: Router, protected analytics: Angulartics2, + protected toasterService: ToasterService, protected i18nService: I18nService, + protected platformUtilsService: PlatformUtilsService, protected messagingService: MessagingService, + protected userService: UserService, protected cryptoService: CryptoService) { } + + async submit() { + if (this.masterPassword == null || this.masterPassword === '') { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassRequired')); + return; + } + + const email = await this.userService.getEmail(); + const key = this.cryptoService.makeKey(this.masterPassword, email); + const keyHash = await this.cryptoService.hashPassword(this.masterPassword, key); + const storedKeyHash = await this.cryptoService.getKeyHash(); + + if (storedKeyHash != null && keyHash != null && storedKeyHash === keyHash) { + await this.cryptoService.setKey(key); + this.messagingService.send('unlocked'); + this.router.navigate([this.successRoute]); + } else { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('invalidMasterPassword')); + } + } + + async logOut() { + const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('logOutConfirmation'), + this.i18nService.t('logOut'), this.i18nService.t('logOut'), this.i18nService.t('cancel')); + if (confirmed) { + this.messagingService.send('logout'); + } + } + + togglePassword() { + this.analytics.eventTrack.next({ action: 'Toggled Master Password on Unlock' }); + this.showPassword = !this.showPassword; + document.getElementById('masterPassword').focus(); + } +} diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 7ab1608e70..799bd5b4b7 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -38,6 +38,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { formPromise: Promise; emailPromise: Promise; + protected loginRoute = 'login'; protected successRoute = 'vault'; constructor(protected authService: AuthService, protected router: Router, @@ -51,7 +52,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { async ngOnInit() { if (this.authService.email == null || this.authService.masterPasswordHash == null || this.authService.twoFactorProviders == null) { - this.router.navigate(['login']); + this.router.navigate([this.loginRoute]); return; } From 22f0f97cda0286859ceb889b9c80b9b5bb88affa Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 5 Apr 2018 11:12:00 -0400 Subject: [PATCH 0140/1626] move ciphers, groupings, and search pipe to jslib --- src/angular/components/ciphers.component.ts | 60 +++++++++++ src/angular/components/groupings.component.ts | 99 +++++++++++++++++++ src/angular/pipes/search-ciphers.pipe.ts | 36 +++++++ 3 files changed, 195 insertions(+) create mode 100644 src/angular/components/ciphers.component.ts create mode 100644 src/angular/components/groupings.component.ts create mode 100644 src/angular/pipes/search-ciphers.pipe.ts diff --git a/src/angular/components/ciphers.component.ts b/src/angular/components/ciphers.component.ts new file mode 100644 index 0000000000..40f1899346 --- /dev/null +++ b/src/angular/components/ciphers.component.ts @@ -0,0 +1,60 @@ +import { + EventEmitter, + Input, + Output, +} from '@angular/core'; + +import { CipherService } from '../../abstractions/cipher.service'; + +import { CipherView } from '../../models/view/cipherView'; + +export class CiphersComponent { + @Input() activeCipherId: string = null; + @Output() onCipherClicked = new EventEmitter(); + @Output() onCipherRightClicked = new EventEmitter(); + @Output() onAddCipher = new EventEmitter(); + @Output() onAddCipherOptions = new EventEmitter(); + + loaded: boolean = false; + ciphers: CipherView[] = []; + searchText: string; + searchPlaceholder: string = null; + private filter: (cipher: CipherView) => boolean = null; + + constructor(protected cipherService: CipherService) { } + + async load(filter: (cipher: CipherView) => boolean = null) { + this.filter = filter; + const ciphers = await this.cipherService.getAllDecrypted(); + + if (this.filter == null) { + this.ciphers = ciphers; + } else { + this.ciphers = ciphers.filter(this.filter); + } + + this.loaded = true; + } + + async refresh() { + this.loaded = false; + this.ciphers = []; + await this.load(this.filter); + } + + selectCipher(cipher: CipherView) { + this.onCipherClicked.emit(cipher); + } + + rightClickCipher(cipher: CipherView) { + this.onCipherRightClicked.emit(cipher); + } + + addCipher() { + this.onAddCipher.emit(); + } + + addCipherOptions() { + this.onAddCipherOptions.emit(); + } +} diff --git a/src/angular/components/groupings.component.ts b/src/angular/components/groupings.component.ts new file mode 100644 index 0000000000..3bec8a866f --- /dev/null +++ b/src/angular/components/groupings.component.ts @@ -0,0 +1,99 @@ +import { + Component, + EventEmitter, + Input, + Output, +} from '@angular/core'; + +import { CipherType } from '../../enums/cipherType'; + +import { CollectionView } from '../../models/view/collectionView'; +import { FolderView } from '../../models/view/folderView'; + +import { CollectionService } from '../../abstractions/collection.service'; +import { FolderService } from '../../abstractions/folder.service'; + +export class GroupingsComponent { + @Output() onAllClicked = new EventEmitter(); + @Output() onFavoritesClicked = new EventEmitter(); + @Output() onCipherTypeClicked = new EventEmitter(); + @Output() onFolderClicked = new EventEmitter(); + @Output() onAddFolder = new EventEmitter(); + @Output() onEditFolder = new EventEmitter(); + @Output() onCollectionClicked = new EventEmitter(); + + folders: FolderView[]; + collections: CollectionView[]; + loaded: boolean = false; + cipherType = CipherType; + selectedAll: boolean = false; + selectedFavorites: boolean = false; + selectedType: CipherType = null; + selectedFolder: boolean = false; + selectedFolderId: string = null; + selectedCollectionId: string = null; + + constructor(protected collectionService: CollectionService, protected folderService: FolderService) { } + + async load() { + await this.loadFolders(); + await this.loadCollections(); + this.loaded = true; + } + + async loadCollections() { + this.collections = await this.collectionService.getAllDecrypted(); + } + + async loadFolders() { + this.folders = await this.folderService.getAllDecrypted(); + } + + selectAll() { + this.clearSelections(); + this.selectedAll = true; + this.onAllClicked.emit(); + } + + selectFavorites() { + this.clearSelections(); + this.selectedFavorites = true; + this.onFavoritesClicked.emit(); + } + + selectType(type: CipherType) { + this.clearSelections(); + this.selectedType = type; + this.onCipherTypeClicked.emit(type); + } + + selectFolder(folder: FolderView) { + this.clearSelections(); + this.selectedFolder = true; + this.selectedFolderId = folder.id; + this.onFolderClicked.emit(folder); + } + + addFolder() { + this.onAddFolder.emit(); + } + + editFolder(folder: FolderView) { + this.onEditFolder.emit(folder); + } + + selectCollection(collection: CollectionView) { + this.clearSelections(); + this.selectedCollectionId = collection.id; + this.onCollectionClicked.emit(collection); + } + + clearSelections() { + this.selectedAll = false; + this.selectedFavorites = false; + this.selectedType = null; + this.selectedFolder = false; + this.selectedFolderId = null; + this.selectedCollectionId = null; + } +} diff --git a/src/angular/pipes/search-ciphers.pipe.ts b/src/angular/pipes/search-ciphers.pipe.ts new file mode 100644 index 0000000000..1d255885ca --- /dev/null +++ b/src/angular/pipes/search-ciphers.pipe.ts @@ -0,0 +1,36 @@ +import { + Pipe, + PipeTransform, +} from '@angular/core'; + +import { CipherView } from '../../models/view/cipherView'; + +@Pipe({ + name: 'searchCiphers', +}) +export class SearchCiphersPipe implements PipeTransform { + transform(ciphers: CipherView[], searchText: string): CipherView[] { + if (ciphers == null || ciphers.length === 0) { + return []; + } + + if (searchText == null || searchText.length < 2) { + return ciphers; + } + + searchText = searchText.toLowerCase(); + return ciphers.filter((c) => { + if (c.name != null && c.name.toLowerCase().indexOf(searchText) > -1) { + return true; + } + if (c.subTitle != null && c.subTitle.toLowerCase().indexOf(searchText) > -1) { + return true; + } + if (c.login && c.login.uri != null && c.login.uri.toLowerCase().indexOf(searchText) > -1) { + return true; + } + + return false; + }); + } +} From 45ba62937114689ff981818d384b391195d62ec6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 5 Apr 2018 22:21:18 -0400 Subject: [PATCH 0141/1626] move vault components to jslib --- src/angular/components/add-edit.component.ts | 277 ++++++++++++++++++ .../components/attachments.component.ts | 125 ++++++++ .../password-generator-history.component.ts | 33 +++ .../password-generator.component.ts | 128 ++++++++ src/angular/components/view.component.ts | 194 ++++++++++++ 5 files changed, 757 insertions(+) create mode 100644 src/angular/components/add-edit.component.ts create mode 100644 src/angular/components/attachments.component.ts create mode 100644 src/angular/components/password-generator-history.component.ts create mode 100644 src/angular/components/password-generator.component.ts create mode 100644 src/angular/components/view.component.ts diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts new file mode 100644 index 0000000000..cc05dc6789 --- /dev/null +++ b/src/angular/components/add-edit.component.ts @@ -0,0 +1,277 @@ +import { + EventEmitter, + Input, + OnChanges, + Output, +} from '@angular/core'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { CipherType } from '../../enums/cipherType'; +import { FieldType } from '../../enums/fieldType'; +import { SecureNoteType } from '../../enums/secureNoteType'; +import { UriMatchType } from '../../enums/uriMatchType'; + +import { AuditService } from '../../abstractions/audit.service'; +import { CipherService } from '../../abstractions/cipher.service'; +import { FolderService } from '../../abstractions/folder.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +import { CardView } from '../../models/view/cardView'; +import { CipherView } from '../../models/view/cipherView'; +import { FieldView } from '../../models/view/fieldView'; +import { FolderView } from '../../models/view/folderView'; +import { IdentityView } from '../../models/view/identityView'; +import { LoginUriView } from '../../models/view/loginUriView'; +import { LoginView } from '../../models/view/loginView'; +import { SecureNoteView } from '../../models/view/secureNoteView'; + +export class AddEditComponent implements OnChanges { + @Input() folderId: string; + @Input() cipherId: string; + @Input() type: CipherType; + @Output() onSavedCipher = new EventEmitter(); + @Output() onDeletedCipher = new EventEmitter(); + @Output() onCancelled = new EventEmitter(); + @Output() onEditAttachments = new EventEmitter(); + @Output() onGeneratePassword = new EventEmitter(); + + editMode: boolean = false; + cipher: CipherView; + folders: FolderView[]; + title: string; + formPromise: Promise; + deletePromise: Promise; + checkPasswordPromise: Promise; + showPassword: boolean = false; + cipherType = CipherType; + fieldType = FieldType; + addFieldType: FieldType = FieldType.Text; + typeOptions: any[]; + cardBrandOptions: any[]; + cardExpMonthOptions: any[]; + identityTitleOptions: any[]; + addFieldTypeOptions: any[]; + uriMatchOptions: any[]; + + constructor(protected cipherService: CipherService, protected folderService: FolderService, + protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, + protected analytics: Angulartics2, protected toasterService: ToasterService, + protected auditService: AuditService) { + this.typeOptions = [ + { name: i18nService.t('typeLogin'), value: CipherType.Login }, + { name: i18nService.t('typeCard'), value: CipherType.Card }, + { name: i18nService.t('typeIdentity'), value: CipherType.Identity }, + { name: i18nService.t('typeSecureNote'), value: CipherType.SecureNote }, + ]; + this.cardBrandOptions = [ + { name: '-- ' + i18nService.t('select') + ' --', value: null }, + { name: 'Visa', value: 'Visa' }, + { name: 'Mastercard', value: 'Mastercard' }, + { name: 'American Express', value: 'Amex' }, + { name: 'Discover', value: 'Discover' }, + { name: 'Diners Club', value: 'Diners Club' }, + { name: 'JCB', value: 'JCB' }, + { name: 'Maestro', value: 'Maestro' }, + { name: 'UnionPay', value: 'UnionPay' }, + { name: i18nService.t('other'), value: 'Other' }, + ]; + this.cardExpMonthOptions = [ + { name: '-- ' + i18nService.t('select') + ' --', value: null }, + { name: '01 - ' + i18nService.t('january'), value: '1' }, + { name: '02 - ' + i18nService.t('february'), value: '2' }, + { name: '03 - ' + i18nService.t('march'), value: '3' }, + { name: '04 - ' + i18nService.t('april'), value: '4' }, + { name: '05 - ' + i18nService.t('may'), value: '5' }, + { name: '06 - ' + i18nService.t('june'), value: '6' }, + { name: '07 - ' + i18nService.t('july'), value: '7' }, + { name: '08 - ' + i18nService.t('august'), value: '8' }, + { name: '09 - ' + i18nService.t('september'), value: '9' }, + { name: '10 - ' + i18nService.t('october'), value: '10' }, + { name: '11 - ' + i18nService.t('november'), value: '11' }, + { name: '12 - ' + i18nService.t('december'), value: '12' }, + ]; + this.identityTitleOptions = [ + { name: '-- ' + i18nService.t('select') + ' --', value: null }, + { name: i18nService.t('mr'), value: i18nService.t('mr') }, + { name: i18nService.t('mrs'), value: i18nService.t('mrs') }, + { name: i18nService.t('ms'), value: i18nService.t('ms') }, + { name: i18nService.t('dr'), value: i18nService.t('dr') }, + ]; + this.addFieldTypeOptions = [ + { name: i18nService.t('cfTypeText'), value: FieldType.Text }, + { name: i18nService.t('cfTypeHidden'), value: FieldType.Hidden }, + { name: i18nService.t('cfTypeBoolean'), value: FieldType.Boolean }, + ]; + this.uriMatchOptions = [ + { name: i18nService.t('defaultMatchDetection'), value: null }, + { name: i18nService.t('baseDomain'), value: UriMatchType.Domain }, + { name: i18nService.t('host'), value: UriMatchType.Host }, + { name: i18nService.t('startsWith'), value: UriMatchType.StartsWith }, + { name: i18nService.t('regEx'), value: UriMatchType.RegularExpression }, + { name: i18nService.t('exact'), value: UriMatchType.Exact }, + { name: i18nService.t('never'), value: UriMatchType.Never }, + ]; + } + + async ngOnChanges() { + this.editMode = this.cipherId != null; + + if (this.editMode) { + this.editMode = true; + this.title = this.i18nService.t('editItem'); + const cipher = await this.cipherService.get(this.cipherId); + this.cipher = await cipher.decrypt(); + } else { + this.title = this.i18nService.t('addItem'); + this.cipher = new CipherView(); + this.cipher.folderId = this.folderId; + this.cipher.type = this.type == null ? CipherType.Login : this.type; + this.cipher.login = new LoginView(); + this.cipher.login.uris = [new LoginUriView()]; + this.cipher.card = new CardView(); + this.cipher.identity = new IdentityView(); + this.cipher.secureNote = new SecureNoteView(); + this.cipher.secureNote.type = SecureNoteType.Generic; + } + + this.folders = await this.folderService.getAllDecrypted(); + } + + async submit() { + if (this.cipher.name == null || this.cipher.name === '') { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('nameRequired')); + return; + } + + const cipher = await this.cipherService.encrypt(this.cipher); + + try { + this.formPromise = this.cipherService.saveWithServer(cipher); + await this.formPromise; + this.cipher.id = cipher.id; + this.analytics.eventTrack.next({ action: this.editMode ? 'Edited Cipher' : 'Added Cipher' }); + this.toasterService.popAsync('success', null, + this.i18nService.t(this.editMode ? 'editedItem' : 'addedItem')); + this.onSavedCipher.emit(this.cipher); + } catch { } + } + + addUri() { + if (this.cipher.type !== CipherType.Login) { + return; + } + + if (this.cipher.login.uris == null) { + this.cipher.login.uris = []; + } + + this.cipher.login.uris.push(new LoginUriView()); + } + + removeUri(uri: LoginUriView) { + if (this.cipher.type !== CipherType.Login || this.cipher.login.uris == null) { + return; + } + + const i = this.cipher.login.uris.indexOf(uri); + if (i > -1) { + this.cipher.login.uris.splice(i, 1); + } + } + + addField() { + if (this.cipher.fields == null) { + this.cipher.fields = []; + } + + const f = new FieldView(); + f.type = this.addFieldType; + this.cipher.fields.push(f); + } + + removeField(field: FieldView) { + const i = this.cipher.fields.indexOf(field); + if (i > -1) { + this.cipher.fields.splice(i, 1); + } + } + + cancel() { + this.onCancelled.emit(this.cipher); + } + + attachments() { + this.onEditAttachments.emit(this.cipher); + } + + async delete() { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('deleteItemConfirmation'), this.i18nService.t('deleteItem'), + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return; + } + + try { + this.deletePromise = this.cipherService.deleteWithServer(this.cipher.id); + await this.deletePromise; + this.analytics.eventTrack.next({ action: 'Deleted Cipher' }); + this.toasterService.popAsync('success', null, this.i18nService.t('deletedItem')); + this.onDeletedCipher.emit(this.cipher); + } catch { } + } + + async generatePassword() { + if (this.cipher.login != null && this.cipher.login.password != null && this.cipher.login.password.length) { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('overwritePasswordConfirmation'), this.i18nService.t('overwritePassword'), + this.i18nService.t('yes'), this.i18nService.t('no')); + if (!confirmed) { + return; + } + } + + this.onGeneratePassword.emit(); + } + + togglePassword() { + this.analytics.eventTrack.next({ action: 'Toggled Password on Edit' }); + this.showPassword = !this.showPassword; + document.getElementById('loginPassword').focus(); + } + + toggleFieldValue(field: FieldView) { + const f = (field as any); + f.showValue = !f.showValue; + } + + toggleUriOptions(uri: LoginUriView) { + const u = (uri as any); + u.showOptions = u.showOptions == null && uri.match != null ? false : !u.showOptions; + } + + loginUriMatchChanged(uri: LoginUriView) { + const u = (uri as any); + u.showOptions = u.showOptions == null ? true : u.showOptions; + } + + async checkPassword() { + if (this.cipher.login == null || this.cipher.login.password == null || this.cipher.login.password === '') { + return; + } + + this.analytics.eventTrack.next({ action: 'Check Password' }); + this.checkPasswordPromise = this.auditService.passwordLeaked(this.cipher.login.password); + const matches = await this.checkPasswordPromise; + + if (matches > 0) { + this.toasterService.popAsync('warning', null, this.i18nService.t('passwordExposed', matches.toString())); + } else { + this.toasterService.popAsync('success', null, this.i18nService.t('passwordSafe')); + } + } +} diff --git a/src/angular/components/attachments.component.ts b/src/angular/components/attachments.component.ts new file mode 100644 index 0000000000..6a02a7c259 --- /dev/null +++ b/src/angular/components/attachments.component.ts @@ -0,0 +1,125 @@ +import { + EventEmitter, + Input, + OnInit, + Output, +} from '@angular/core'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { CipherService } from '../../abstractions/cipher.service'; +import { CryptoService } from '../../abstractions/crypto.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { TokenService } from '../../abstractions/token.service'; + +import { Cipher } from '../../models/domain/cipher'; + +import { AttachmentView } from '../../models/view/attachmentView'; +import { CipherView } from '../../models/view/cipherView'; + +export class AttachmentsComponent implements OnInit { + @Input() cipherId: string; + + cipher: CipherView; + cipherDomain: Cipher; + hasUpdatedKey: boolean; + canAccessAttachments: boolean; + formPromise: Promise; + deletePromises: { [id: string]: Promise; } = {}; + + constructor(protected cipherService: CipherService, protected analytics: Angulartics2, + protected toasterService: ToasterService, protected i18nService: I18nService, + protected cryptoService: CryptoService, protected tokenService: TokenService, + protected platformUtilsService: PlatformUtilsService) { } + + async ngOnInit() { + this.cipherDomain = await this.cipherService.get(this.cipherId); + this.cipher = await this.cipherDomain.decrypt(); + + const key = await this.cryptoService.getEncKey(); + this.hasUpdatedKey = key != null; + const isPremium = this.tokenService.getPremium(); + this.canAccessAttachments = isPremium || this.cipher.organizationId != null; + + if (!this.canAccessAttachments) { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('premiumRequiredDesc'), this.i18nService.t('premiumRequired'), + this.i18nService.t('learnMore'), this.i18nService.t('cancel')); + if (confirmed) { + this.platformUtilsService.launchUri('https://vault.bitwarden.com/#/?premium=purchase'); + } + } else if (!this.hasUpdatedKey) { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('updateKey'), this.i18nService.t('featureUnavailable'), + this.i18nService.t('learnMore'), this.i18nService.t('cancel'), 'warning'); + if (confirmed) { + this.platformUtilsService.launchUri('https://help.bitwarden.com/article/update-encryption-key/'); + } + } + } + + async submit() { + if (!this.hasUpdatedKey) { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('updateKey')); + return; + } + + const fileEl = document.getElementById('file') as HTMLInputElement; + const files = fileEl.files; + if (files == null || files.length === 0) { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('selectFile')); + return; + } + + if (files[0].size > 104857600) { // 100 MB + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('maxFileSize')); + return; + } + + try { + this.formPromise = this.cipherService.saveAttachmentWithServer(this.cipherDomain, files[0]); + this.cipherDomain = await this.formPromise; + this.cipher = await this.cipherDomain.decrypt(); + this.analytics.eventTrack.next({ action: 'Added Attachment' }); + this.toasterService.popAsync('success', null, this.i18nService.t('attachmentSaved')); + } catch { } + + // reset file input + // ref: https://stackoverflow.com/a/20552042 + fileEl.type = ''; + fileEl.type = 'file'; + fileEl.value = ''; + } + + async delete(attachment: AttachmentView) { + if (this.deletePromises[attachment.id] != null) { + return; + } + + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('deleteAttachmentConfirmation'), this.i18nService.t('deleteAttachment'), + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return; + } + + try { + this.deletePromises[attachment.id] = this.cipherService.deleteAttachmentWithServer( + this.cipher.id, attachment.id); + await this.deletePromises[attachment.id]; + this.analytics.eventTrack.next({ action: 'Deleted Attachment' }); + this.toasterService.popAsync('success', null, this.i18nService.t('deletedAttachment')); + const i = this.cipher.attachments.indexOf(attachment); + if (i > -1) { + this.cipher.attachments.splice(i, 1); + } + } catch { } + + this.deletePromises[attachment.id] = null; + } +} diff --git a/src/angular/components/password-generator-history.component.ts b/src/angular/components/password-generator-history.component.ts new file mode 100644 index 0000000000..fd2a7dc372 --- /dev/null +++ b/src/angular/components/password-generator-history.component.ts @@ -0,0 +1,33 @@ +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { OnInit } from '@angular/core'; + +import { I18nService } from '../../abstractions/i18n.service'; +import { PasswordGenerationService } from '../../abstractions/passwordGeneration.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +import { PasswordHistory } from '../../models/domain/passwordHistory'; + +export class PasswordGeneratorHistoryComponent implements OnInit { + history: PasswordHistory[] = []; + + constructor(protected passwordGenerationService: PasswordGenerationService, protected analytics: Angulartics2, + protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, + protected toasterService: ToasterService) { } + + async ngOnInit() { + this.history = await this.passwordGenerationService.getHistory(); + } + + clear() { + this.history = []; + this.passwordGenerationService.clear(); + } + + copy(password: string) { + this.analytics.eventTrack.next({ action: 'Copied Historical Password' }); + this.platformUtilsService.copyToClipboard(password); + this.toasterService.popAsync('info', null, this.i18nService.t('valueCopied', this.i18nService.t('password'))); + } +} diff --git a/src/angular/components/password-generator.component.ts b/src/angular/components/password-generator.component.ts new file mode 100644 index 0000000000..10b318888a --- /dev/null +++ b/src/angular/components/password-generator.component.ts @@ -0,0 +1,128 @@ +import * as template from './password-generator.component.html'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { + ChangeDetectorRef, + EventEmitter, + Input, + NgZone, + OnInit, + Output, +} from '@angular/core'; + +import { I18nService } from '../../abstractions/i18n.service'; +import { PasswordGenerationService } from '../../abstractions/passwordGeneration.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +export class PasswordGeneratorComponent implements OnInit { + @Input() showSelect: boolean = false; + @Output() onSelected = new EventEmitter(); + + options: any = {}; + password: string = '-'; + showOptions = false; + avoidAmbiguous = false; + + constructor(protected passwordGenerationService: PasswordGenerationService, protected analytics: Angulartics2, + protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, + protected toasterService: ToasterService, protected ngZone: NgZone, + protected changeDetectorRef: ChangeDetectorRef) { } + + async ngOnInit() { + this.options = await this.passwordGenerationService.getOptions(); + this.avoidAmbiguous = !this.options.ambiguous; + this.password = this.passwordGenerationService.generatePassword(this.options); + this.analytics.eventTrack.next({ action: 'Generated Password' }); + await this.passwordGenerationService.addHistory(this.password); + } + + async sliderChanged() { + this.saveOptions(false); + await this.passwordGenerationService.addHistory(this.password); + this.analytics.eventTrack.next({ action: 'Regenerated Password' }); + } + + async sliderInput() { + this.normalizeOptions(); + this.password = this.passwordGenerationService.generatePassword(this.options); + } + + async saveOptions(regenerate: boolean = true) { + this.normalizeOptions(); + await this.passwordGenerationService.saveOptions(this.options); + + if (regenerate) { + await this.regenerate(); + } + } + + async regenerate() { + this.password = this.passwordGenerationService.generatePassword(this.options); + await this.passwordGenerationService.addHistory(this.password); + this.analytics.eventTrack.next({ action: 'Regenerated Password' }); + } + + copy() { + this.analytics.eventTrack.next({ action: 'Copied Generated Password' }); + this.platformUtilsService.copyToClipboard(this.password); + this.toasterService.popAsync('info', null, this.i18nService.t('valueCopied', this.i18nService.t('password'))); + } + + select() { + this.analytics.eventTrack.next({ action: 'Selected Generated Password' }); + this.onSelected.emit(this.password); + } + + toggleOptions() { + this.showOptions = !this.showOptions; + } + + private normalizeOptions() { + this.options.minLowercase = 0; + this.options.minUppercase = 0; + this.options.ambiguous = !this.avoidAmbiguous; + + if (!this.options.uppercase && !this.options.lowercase && !this.options.number && !this.options.special) { + this.options.lowercase = true; + const lowercase = document.querySelector('#lowercase') as HTMLInputElement; + if (lowercase) { + lowercase.checked = true; + } + } + + if (!this.options.length) { + this.options.length = 5; + } else if (this.options.length > 128) { + this.options.length = 128; + } + + if (!this.options.minNumber) { + this.options.minNumber = 0; + } else if (this.options.minNumber > this.options.length) { + this.options.minNumber = this.options.length; + } else if (this.options.minNumber > 9) { + this.options.minNumber = 9; + } + + if (!this.options.minSpecial) { + this.options.minSpecial = 0; + } else if (this.options.minSpecial > this.options.length) { + this.options.minSpecial = this.options.length; + } else if (this.options.minSpecial > 9) { + this.options.minSpecial = 9; + } + + if (this.options.minSpecial + this.options.minNumber > this.options.length) { + this.options.minSpecial = this.options.length - this.options.minNumber; + } + } + + private functionWithChangeDetection(func: Function) { + this.ngZone.run(async () => { + func(); + this.changeDetectorRef.detectChanges(); + }); + } +} diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts new file mode 100644 index 0000000000..ef9d52f346 --- /dev/null +++ b/src/angular/components/view.component.ts @@ -0,0 +1,194 @@ +import { + EventEmitter, + Input, + OnChanges, + OnDestroy, + Output, +} from '@angular/core'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { CipherType } from '../../enums/cipherType'; +import { FieldType } from '../../enums/fieldType'; + +import { AuditService } from '../../abstractions/audit.service'; +import { CipherService } from '../../abstractions/cipher.service'; +import { CryptoService } from '../../abstractions/crypto.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { TokenService } from '../../abstractions/token.service'; +import { TotpService } from '../../abstractions/totp.service'; + +import { AttachmentView } from '../../models/view/attachmentView'; +import { CipherView } from '../../models/view/cipherView'; +import { FieldView } from '../../models/view/fieldView'; +import { LoginUriView } from '../../models/view/loginUriView'; + +export class ViewComponent implements OnChanges, OnDestroy { + @Input() cipherId: string; + @Output() onEditCipher = new EventEmitter(); + + cipher: CipherView; + showPassword: boolean; + isPremium: boolean; + totpCode: string; + totpCodeFormatted: string; + totpDash: number; + totpSec: number; + totpLow: boolean; + fieldType = FieldType; + checkPasswordPromise: Promise; + + private totpInterval: any; + + constructor(protected cipherService: CipherService, protected totpService: TotpService, + protected tokenService: TokenService, protected toasterService: ToasterService, + protected cryptoService: CryptoService, protected platformUtilsService: PlatformUtilsService, + protected i18nService: I18nService, protected analytics: Angulartics2, + protected auditService: AuditService) { } + + async ngOnChanges() { + this.cleanUp(); + + const cipher = await this.cipherService.get(this.cipherId); + this.cipher = await cipher.decrypt(); + + this.isPremium = this.tokenService.getPremium(); + + if (this.cipher.type === CipherType.Login && this.cipher.login.totp && + (cipher.organizationUseTotp || this.isPremium)) { + await this.totpUpdateCode(); + await this.totpTick(); + + this.totpInterval = setInterval(async () => { + await this.totpTick(); + }, 1000); + } + } + + ngOnDestroy() { + this.cleanUp(); + } + + edit() { + this.onEditCipher.emit(this.cipher); + } + + togglePassword() { + this.analytics.eventTrack.next({ action: 'Toggled Password' }); + this.showPassword = !this.showPassword; + } + + async checkPassword() { + if (this.cipher.login == null || this.cipher.login.password == null || this.cipher.login.password === '') { + return; + } + + this.analytics.eventTrack.next({ action: 'Check Password' }); + this.checkPasswordPromise = this.auditService.passwordLeaked(this.cipher.login.password); + const matches = await this.checkPasswordPromise; + + if (matches > 0) { + this.toasterService.popAsync('warning', null, this.i18nService.t('passwordExposed', matches.toString())); + } else { + this.toasterService.popAsync('success', null, this.i18nService.t('passwordSafe')); + } + } + + toggleFieldValue(field: FieldView) { + const f = (field as any); + f.showValue = !f.showValue; + } + + launch(uri: LoginUriView) { + if (!uri.canLaunch) { + return; + } + + this.analytics.eventTrack.next({ action: 'Launched Login URI' }); + this.platformUtilsService.launchUri(uri.uri); + } + + copy(value: string, typeI18nKey: string, aType: string) { + if (value == null) { + return; + } + + this.analytics.eventTrack.next({ action: 'Copied ' + aType }); + this.platformUtilsService.copyToClipboard(value); + this.toasterService.popAsync('info', null, + this.i18nService.t('valueCopied', this.i18nService.t(typeI18nKey))); + } + + async downloadAttachment(attachment: AttachmentView) { + const a = (attachment as any); + if (a.downloading) { + return; + } + + if (this.cipher.organizationId == null && !this.isPremium) { + this.toasterService.popAsync('error', this.i18nService.t('premiumRequired'), + this.i18nService.t('premiumRequiredDesc')); + return; + } + + a.downloading = true; + const response = await fetch(new Request(attachment.url, { cache: 'no-cache' })); + if (response.status !== 200) { + this.toasterService.popAsync('error', null, this.i18nService.t('errorOccurred')); + a.downloading = false; + return; + } + + try { + const buf = await response.arrayBuffer(); + const key = await this.cryptoService.getOrgKey(this.cipher.organizationId); + const decBuf = await this.cryptoService.decryptFromBytes(buf, key); + this.platformUtilsService.saveFile(window, decBuf, null, attachment.fileName); + } catch (e) { + this.toasterService.popAsync('error', null, this.i18nService.t('errorOccurred')); + } + + a.downloading = false; + } + + private cleanUp() { + this.cipher = null; + this.showPassword = false; + if (this.totpInterval) { + clearInterval(this.totpInterval); + } + } + + private async totpUpdateCode() { + if (this.cipher == null || this.cipher.type !== CipherType.Login || this.cipher.login.totp == null) { + if (this.totpInterval) { + clearInterval(this.totpInterval); + } + return; + } + + this.totpCode = await this.totpService.getCode(this.cipher.login.totp); + if (this.totpCode != null) { + this.totpCodeFormatted = this.totpCode.substring(0, 3) + ' ' + this.totpCode.substring(3); + } else { + this.totpCodeFormatted = null; + if (this.totpInterval) { + clearInterval(this.totpInterval); + } + } + } + + private async totpTick() { + const epoch = Math.round(new Date().getTime() / 1000.0); + const mod = epoch % 30; + + this.totpSec = 30 - mod; + this.totpDash = +(Math.round(((2.62 * mod) + 'e+2') as any) + 'e-2'); + this.totpLow = this.totpSec <= 7; + if (mod === 0) { + await this.totpUpdateCode(); + } + } +} From db83cab552296c609bc3dc6841070ec7fa231818 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 5 Apr 2018 23:49:38 -0400 Subject: [PATCH 0142/1626] call into load method --- src/angular/components/add-edit.component.ts | 12 +++++++----- src/angular/components/view.component.ts | 12 ++++++------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index cc05dc6789..91ad981176 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -1,7 +1,6 @@ import { EventEmitter, Input, - OnChanges, Output, } from '@angular/core'; @@ -28,7 +27,7 @@ import { LoginUriView } from '../../models/view/loginUriView'; import { LoginView } from '../../models/view/loginView'; import { SecureNoteView } from '../../models/view/secureNoteView'; -export class AddEditComponent implements OnChanges { +export class AddEditComponent { @Input() folderId: string; @Input() cipherId: string; @Input() type: CipherType; @@ -116,7 +115,7 @@ export class AddEditComponent implements OnChanges { ]; } - async ngOnChanges() { + async load() { this.editMode = this.cipherId != null; if (this.editMode) { @@ -140,11 +139,11 @@ export class AddEditComponent implements OnChanges { this.folders = await this.folderService.getAllDecrypted(); } - async submit() { + async submit(): Promise { if (this.cipher.name == null || this.cipher.name === '') { this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), this.i18nService.t('nameRequired')); - return; + return false; } const cipher = await this.cipherService.encrypt(this.cipher); @@ -157,7 +156,10 @@ export class AddEditComponent implements OnChanges { this.toasterService.popAsync('success', null, this.i18nService.t(this.editMode ? 'editedItem' : 'addedItem')); this.onSavedCipher.emit(this.cipher); + return true; } catch { } + + return false; } addUri() { diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index ef9d52f346..dc3a1e8d19 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -25,7 +25,7 @@ import { CipherView } from '../../models/view/cipherView'; import { FieldView } from '../../models/view/fieldView'; import { LoginUriView } from '../../models/view/loginUriView'; -export class ViewComponent implements OnChanges, OnDestroy { +export class ViewComponent implements OnDestroy { @Input() cipherId: string; @Output() onEditCipher = new EventEmitter(); @@ -48,7 +48,11 @@ export class ViewComponent implements OnChanges, OnDestroy { protected i18nService: I18nService, protected analytics: Angulartics2, protected auditService: AuditService) { } - async ngOnChanges() { + ngOnDestroy() { + this.cleanUp(); + } + + async load() { this.cleanUp(); const cipher = await this.cipherService.get(this.cipherId); @@ -67,10 +71,6 @@ export class ViewComponent implements OnChanges, OnDestroy { } } - ngOnDestroy() { - this.cleanUp(); - } - edit() { this.onEditCipher.emit(this.cipher); } From 69afd7923ba13cd1248425ae142fad69437167cb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 6 Apr 2018 12:01:21 -0400 Subject: [PATCH 0143/1626] ref icon templateUrl --- src/angular/app.d.ts | 1 - src/angular/components/icon.component.ts | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 src/angular/app.d.ts diff --git a/src/angular/app.d.ts b/src/angular/app.d.ts deleted file mode 100644 index 7bf7506fcf..0000000000 --- a/src/angular/app.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module '*.html'; diff --git a/src/angular/components/icon.component.ts b/src/angular/components/icon.component.ts index 7304bed83f..65b9339e2f 100644 --- a/src/angular/components/icon.component.ts +++ b/src/angular/components/icon.component.ts @@ -1,5 +1,3 @@ -import * as template from './icon.component.html'; - import { Component, Input, @@ -15,7 +13,7 @@ import { ConstantsService } from '../../services/constants.service'; @Component({ selector: 'app-vault-icon', - template: template, + templateUrl: 'icon.component.html', }) export class IconComponent implements OnChanges { @Input() cipher: any; From 0b9c2f518d72b3b3a6bcf97f97bc7dfa15b4e153 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 6 Apr 2018 12:25:42 -0400 Subject: [PATCH 0144/1626] remove template ref --- src/angular/components/password-generator.component.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/angular/components/password-generator.component.ts b/src/angular/components/password-generator.component.ts index 10b318888a..4bd133ff00 100644 --- a/src/angular/components/password-generator.component.ts +++ b/src/angular/components/password-generator.component.ts @@ -1,5 +1,3 @@ -import * as template from './password-generator.component.html'; - import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; From 5d66ede2ea1e73ba0ad8bf949bd74992e7a78582 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 6 Apr 2018 15:33:38 -0400 Subject: [PATCH 0145/1626] move broadcaster to jslib --- src/angular/services/broadcaster.service.ts | 33 +++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/angular/services/broadcaster.service.ts diff --git a/src/angular/services/broadcaster.service.ts b/src/angular/services/broadcaster.service.ts new file mode 100644 index 0000000000..fe747e7b0b --- /dev/null +++ b/src/angular/services/broadcaster.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class BroadcasterService { + subscribers: Map any> = new Map any>(); + + send(message: any, id?: string) { + if (id != null) { + if (this.subscribers.has(id)) { + this.subscribers.get(id)(message); + } + return; + } + + this.subscribers.forEach((value) => { + value(message); + }); + } + + subscribe(id: string, messageCallback: (message: any) => any) { + if (this.subscribers.has(id)) { + return; + } + + this.subscribers.set(id, messageCallback); + } + + unsubscribe(id: string) { + if (this.subscribers.has(id)) { + this.subscribers.delete(id); + } + } +} From 2c87f127185f073745daa596152ee9039032c655 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 7 Apr 2018 00:18:31 -0400 Subject: [PATCH 0146/1626] use win variables --- src/angular/components/two-factor-options.component.ts | 5 +++-- src/angular/components/two-factor.component.ts | 4 ++-- src/angular/components/view.component.ts | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/angular/components/two-factor-options.component.ts b/src/angular/components/two-factor-options.component.ts index cad2734cf6..e28fdef321 100644 --- a/src/angular/components/two-factor-options.component.ts +++ b/src/angular/components/two-factor-options.component.ts @@ -25,7 +25,8 @@ export class TwoFactorOptionsComponent implements OnInit { constructor(protected authService: AuthService, protected router: Router, protected analytics: Angulartics2, protected toasterService: ToasterService, - protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService) { } + protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, + protected win: Window) { } ngOnInit() { if (this.authService.twoFactorProviders.has(TwoFactorProviderType.OrganizationDuo)) { @@ -45,7 +46,7 @@ export class TwoFactorOptionsComponent implements OnInit { } if (this.authService.twoFactorProviders.has(TwoFactorProviderType.U2f) && - this.platformUtilsService.supportsU2f(window)) { + this.platformUtilsService.supportsU2f(this.win)) { this.providers.push(TwoFactorProviders[TwoFactorProviderType.U2f]); } diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 799bd5b4b7..226d2b2676 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -46,7 +46,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { protected i18nService: I18nService, protected apiService: ApiService, protected platformUtilsService: PlatformUtilsService, protected syncService: SyncService, protected win: Window, protected environmentService: EnvironmentService) { - this.u2fSupported = this.platformUtilsService.supportsU2f(window); + this.u2fSupported = this.platformUtilsService.supportsU2f(win); } async ngOnInit() { @@ -119,7 +119,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { } setTimeout(() => { - (window as any).Duo.init({ + (this.win as any).Duo.init({ host: params.Host, sig_request: params.Signature, submit_callback: async (f: HTMLFormElement) => { diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index dc3a1e8d19..da63d5c69f 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -46,7 +46,7 @@ export class ViewComponent implements OnDestroy { protected tokenService: TokenService, protected toasterService: ToasterService, protected cryptoService: CryptoService, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, protected analytics: Angulartics2, - protected auditService: AuditService) { } + protected auditService: AuditService, protected win: Window) { } ngOnDestroy() { this.cleanUp(); @@ -145,7 +145,7 @@ export class ViewComponent implements OnDestroy { const buf = await response.arrayBuffer(); const key = await this.cryptoService.getOrgKey(this.cipher.organizationId); const decBuf = await this.cryptoService.decryptFromBytes(buf, key); - this.platformUtilsService.saveFile(window, decBuf, null, attachment.fileName); + this.platformUtilsService.saveFile(this.win, decBuf, null, attachment.fileName); } catch (e) { this.toasterService.popAsync('error', null, this.i18nService.t('errorOccurred')); } From 4f79addb8a05fd829c4aebc5999e1cf9ba510410 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 9 Apr 2018 17:35:56 -0400 Subject: [PATCH 0147/1626] load cipher from state --- src/angular/components/add-edit.component.ts | 34 ++++++++++++------- .../password-generator.component.ts | 12 +------ 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 91ad981176..f0681f7b0c 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -17,6 +17,7 @@ import { CipherService } from '../../abstractions/cipher.service'; import { FolderService } from '../../abstractions/folder.service'; import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { StateService } from '../../abstractions/state.service'; import { CardView } from '../../models/view/cardView'; import { CipherView } from '../../models/view/cipherView'; @@ -58,7 +59,7 @@ export class AddEditComponent { constructor(protected cipherService: CipherService, protected folderService: FolderService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected analytics: Angulartics2, protected toasterService: ToasterService, - protected auditService: AuditService) { + protected auditService: AuditService, protected stateService: StateService) { this.typeOptions = [ { name: i18nService.t('typeLogin'), value: CipherType.Login }, { name: i18nService.t('typeCard'), value: CipherType.Card }, @@ -117,23 +118,30 @@ export class AddEditComponent { async load() { this.editMode = this.cipherId != null; - if (this.editMode) { this.editMode = true; this.title = this.i18nService.t('editItem'); - const cipher = await this.cipherService.get(this.cipherId); - this.cipher = await cipher.decrypt(); } else { this.title = this.i18nService.t('addItem'); - this.cipher = new CipherView(); - this.cipher.folderId = this.folderId; - this.cipher.type = this.type == null ? CipherType.Login : this.type; - this.cipher.login = new LoginView(); - this.cipher.login.uris = [new LoginUriView()]; - this.cipher.card = new CardView(); - this.cipher.identity = new IdentityView(); - this.cipher.secureNote = new SecureNoteView(); - this.cipher.secureNote.type = SecureNoteType.Generic; + } + + this.cipher = await this.stateService.get('addEditCipher'); + await this.stateService.remove('addEditCipher'); + if (this.cipher == null) { + if (this.editMode) { + const cipher = await this.cipherService.get(this.cipherId); + this.cipher = await cipher.decrypt(); + } else { + this.cipher = new CipherView(); + this.cipher.folderId = this.folderId; + this.cipher.type = this.type == null ? CipherType.Login : this.type; + this.cipher.login = new LoginView(); + this.cipher.login.uris = [new LoginUriView()]; + this.cipher.card = new CardView(); + this.cipher.identity = new IdentityView(); + this.cipher.secureNote = new SecureNoteView(); + this.cipher.secureNote.type = SecureNoteType.Generic; + } } this.folders = await this.folderService.getAllDecrypted(); diff --git a/src/angular/components/password-generator.component.ts b/src/angular/components/password-generator.component.ts index 4bd133ff00..7ca52070e0 100644 --- a/src/angular/components/password-generator.component.ts +++ b/src/angular/components/password-generator.component.ts @@ -2,10 +2,8 @@ import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; import { - ChangeDetectorRef, EventEmitter, Input, - NgZone, OnInit, Output, } from '@angular/core'; @@ -25,8 +23,7 @@ export class PasswordGeneratorComponent implements OnInit { constructor(protected passwordGenerationService: PasswordGenerationService, protected analytics: Angulartics2, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, - protected toasterService: ToasterService, protected ngZone: NgZone, - protected changeDetectorRef: ChangeDetectorRef) { } + protected toasterService: ToasterService) { } async ngOnInit() { this.options = await this.passwordGenerationService.getOptions(); @@ -116,11 +113,4 @@ export class PasswordGeneratorComponent implements OnInit { this.options.minSpecial = this.options.length - this.options.minNumber; } } - - private functionWithChangeDetection(func: Function) { - this.ngZone.run(async () => { - func(); - this.changeDetectorRef.detectChanges(); - }); - } } From cd1828da23129bd8a53d2891313c6b71ac5c22e3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 9 Apr 2018 19:05:13 -0400 Subject: [PATCH 0148/1626] export component --- package-lock.json | 6 + package.json | 1 + src/angular/components/export.component.ts | 178 +++++++++++++++++++++ 3 files changed, 185 insertions(+) create mode 100644 src/angular/components/export.component.ts diff --git a/package-lock.json b/package-lock.json index 9cab90a199..5f769ef6a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -151,6 +151,12 @@ "integrity": "sha1-XoS4q/QthOxenQg+jHVzsxVcYzQ=", "dev": true }, + "@types/papaparse": { + "version": "4.1.31", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-4.1.31.tgz", + "integrity": "sha512-8+d1hk3GgF+NJ6mMZZ5zKimqIOc+8OTzpLw4RQ8wnS1NkJh/dMH3NEhSud4Ituq2SGXJjOG6wIczCBAKsSsBdQ==", + "dev": true + }, "@types/shelljs": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.7.0.tgz", diff --git a/package.json b/package.json index 6ec69a7ab1..b63e1baeb6 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@angular/upgrade": "5.2.0", "@types/lunr": "2.1.5", "@types/node-forge": "0.7.1", + "@types/papaparse": "4.1.31", "@types/webcrypto": "0.0.28", "angular2-toaster": "4.0.2", "angulartics2": "5.0.1", diff --git a/src/angular/components/export.component.ts b/src/angular/components/export.component.ts new file mode 100644 index 0000000000..f763cf33ab --- /dev/null +++ b/src/angular/components/export.component.ts @@ -0,0 +1,178 @@ +import * as papa from 'papaparse'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { + EventEmitter, + Output, +} from '@angular/core'; + +import { CipherType } from '../../enums/cipherType'; + +import { CipherView } from '../../models/view/cipherView'; +import { FolderView } from '../../models/view/folderView'; + +import { CipherService } from '../../abstractions/cipher.service'; +import { CryptoService } from '../../abstractions/crypto.service'; +import { FolderService } from '../../abstractions/folder.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { UserService } from '../../abstractions/user.service'; + +export class ExportComponent { + @Output() onSaved = new EventEmitter(); + + masterPassword: string; + showPassword = false; + + constructor(protected analytics: Angulartics2, protected toasterService: ToasterService, + protected cipherService: CipherService, protected folderService: FolderService, + protected cryptoService: CryptoService, protected userService: UserService, + protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, + protected win: Window) { } + + async submit() { + if (this.masterPassword == null || this.masterPassword === '') { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('invalidMasterPassword')); + return; + } + + const email = await this.userService.getEmail(); + const key = this.cryptoService.makeKey(this.masterPassword, email); + const keyHash = await this.cryptoService.hashPassword(this.masterPassword, key); + const storedKeyHash = await this.cryptoService.getKeyHash(); + + if (storedKeyHash != null && keyHash != null && storedKeyHash === keyHash) { + const csv = await this.getCsv(); + this.analytics.eventTrack.next({ action: 'Exported Data' }); + this.downloadFile(csv); + this.saved(); + } else { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('invalidMasterPassword')); + } + } + + togglePassword() { + this.analytics.eventTrack.next({ action: 'Toggled Master Password on Export' }); + this.showPassword = !this.showPassword; + document.getElementById('masterPassword').focus(); + } + + protected saved() { + this.onSaved.emit(); + } + + private async checkPassword() { + const email = await this.userService.getEmail(); + const key = this.cryptoService.makeKey(this.masterPassword, email); + const keyHash = await this.cryptoService.hashPassword(this.masterPassword, key); + const storedKeyHash = await this.cryptoService.getKeyHash(); + if (storedKeyHash == null || keyHash == null || storedKeyHash !== keyHash) { + throw new Error('Invalid password.'); + } + } + + private async getCsv(): Promise { + let decFolders: FolderView[] = []; + let decCiphers: CipherView[] = []; + const promises = []; + + promises.push(this.folderService.getAllDecrypted().then((folders) => { + decFolders = folders; + })); + + promises.push(this.cipherService.getAllDecrypted().then((ciphers) => { + decCiphers = ciphers; + })); + + await Promise.all(promises); + + const foldersMap = new Map(); + decFolders.forEach((f) => { + foldersMap.set(f.id, f); + }); + + const exportCiphers: any[] = []; + decCiphers.forEach((c) => { + // only export logins and secure notes + if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) { + return; + } + + const cipher: any = { + folder: c.folderId && foldersMap.has(c.folderId) ? foldersMap.get(c.folderId).name : null, + favorite: c.favorite ? 1 : null, + type: null, + name: c.name, + notes: c.notes, + fields: null, + // Login props + login_uri: null, + login_username: null, + login_password: null, + login_totp: null, + }; + + if (c.fields) { + c.fields.forEach((f: any) => { + if (!cipher.fields) { + cipher.fields = ''; + } else { + cipher.fields += '\n'; + } + + cipher.fields += ((f.name || '') + ': ' + f.value); + }); + } + + switch (c.type) { + case CipherType.Login: + cipher.type = 'login'; + cipher.login_username = c.login.username; + cipher.login_password = c.login.password; + cipher.login_totp = c.login.totp; + + if (c.login.uris) { + cipher.login_uri = []; + c.login.uris.forEach((u) => { + cipher.login_uri.push(u.uri); + }); + } + break; + case CipherType.SecureNote: + cipher.type = 'note'; + break; + default: + return; + } + + exportCiphers.push(cipher); + }); + + return papa.unparse(exportCiphers); + } + + private downloadFile(csv: string): void { + const fileName = this.makeFileName(); + this.platformUtilsService.saveFile(this.win, csv, { type: 'text/plain' }, fileName); + } + + private makeFileName(): string { + const now = new Date(); + const dateString = + now.getFullYear() + '' + this.padNumber(now.getMonth() + 1, 2) + '' + this.padNumber(now.getDate(), 2) + + this.padNumber(now.getHours(), 2) + '' + this.padNumber(now.getMinutes(), 2) + + this.padNumber(now.getSeconds(), 2); + + return 'bitwarden_export_' + dateString + '.csv'; + } + + private padNumber(num: number, width: number, padCharacter: string = '0'): string { + const numString = num.toString(); + return numString.length >= width ? numString : + new Array(width - numString.length + 1).join(padCharacter) + numString; + } +} From f8d8ca225360a0ed05f54642ed89da282d8a1021 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 10 Apr 2018 00:04:39 -0400 Subject: [PATCH 0149/1626] default folderId null --- src/angular/components/add-edit.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index f0681f7b0c..c91e0b47eb 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -29,7 +29,7 @@ import { LoginView } from '../../models/view/loginView'; import { SecureNoteView } from '../../models/view/secureNoteView'; export class AddEditComponent { - @Input() folderId: string; + @Input() folderId: string = null; @Input() cipherId: string; @Input() type: CipherType; @Output() onSavedCipher = new EventEmitter(); From fa5f6c09068add3951889961f545e6990d9c3ea7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 10 Apr 2018 13:53:21 -0400 Subject: [PATCH 0150/1626] return boolean from generate password --- src/angular/components/add-edit.component.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index c91e0b47eb..623dc38338 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -235,17 +235,18 @@ export class AddEditComponent { } catch { } } - async generatePassword() { + async generatePassword(): Promise { if (this.cipher.login != null && this.cipher.login.password != null && this.cipher.login.password.length) { const confirmed = await this.platformUtilsService.showDialog( this.i18nService.t('overwritePasswordConfirmation'), this.i18nService.t('overwritePassword'), this.i18nService.t('yes'), this.i18nService.t('no')); if (!confirmed) { - return; + return false; } } this.onGeneratePassword.emit(); + return true; } togglePassword() { From 5f341b963852bac391f36ba5989185eaa0bded31 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 10 Apr 2018 22:05:36 -0400 Subject: [PATCH 0151/1626] update settings --- .editorconfig | 3 +-- .gitignore | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.editorconfig b/.editorconfig index 65932d2edc..332f3a5d3f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,9 +8,8 @@ root = true end_of_line = lf insert_final_newline = true -# Matches multiple files with brace expansion notation # Set default charset -[*.{js,ts,less}] +[*.{js,ts,scss,html}] charset = utf-8 indent_style = space indent_size = 4 diff --git a/.gitignore b/.gitignore index 584bc1013b..cdbb247cb1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .idea node_modules npm-debug.log +vwd.webinfo *.crx *.pem dist From 7933746d822206d58ca852521dc59467af885239 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 10 Apr 2018 23:49:09 -0400 Subject: [PATCH 0152/1626] lint fixes --- src/angular/components/two-factor.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 226d2b2676..a448e12843 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -107,7 +107,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { challenge: challenges[0].challenge, keys: [{ version: challenges[0].version, - keyHandle: challenges[0].keyHandle + keyHandle: challenges[0].keyHandle, }], }); } From 97be728b31204448fcf307e67a2ce7667ec5fe7b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 11 Apr 2018 15:07:33 -0400 Subject: [PATCH 0153/1626] move base i18nservice out to jslib --- src/services/i18n.service.ts | 102 +++++++++++++++++++++++++++++++++++ src/services/index.ts | 1 + 2 files changed, 103 insertions(+) create mode 100644 src/services/i18n.service.ts diff --git a/src/services/i18n.service.ts b/src/services/i18n.service.ts new file mode 100644 index 0000000000..54d081c143 --- /dev/null +++ b/src/services/i18n.service.ts @@ -0,0 +1,102 @@ +import { I18nService as I18nServiceAbstraction } from '../abstractions/i18n.service'; + +export class I18nService implements I18nServiceAbstraction { + locale: string; + translationLocale: string; + collator: Intl.Collator; + + // First locale is the default (English) + protected supportedTranslationLocales: string[] = ['en']; + protected inited: boolean; + protected defaultMessages: any = {}; + protected localeMessages: any = {}; + + constructor(protected systemLanguage: string, protected localesDirectory: string) { } + + async init(locale?: string) { + if (this.inited) { + throw new Error('i18n already initialized.'); + } + if (this.supportedTranslationLocales == null || this.supportedTranslationLocales.length === 0) { + throw new Error('supportedTranslationLocales not set.'); + } + + this.inited = true; + this.locale = this.translationLocale = locale != null ? locale : this.systemLanguage; + this.collator = new Intl.Collator(this.locale); + + if (this.supportedTranslationLocales.indexOf(this.translationLocale) === -1) { + this.translationLocale = this.translationLocale.slice(0, 2); + + if (this.supportedTranslationLocales.indexOf(this.translationLocale) === -1) { + this.translationLocale = this.supportedTranslationLocales[0]; + } + } + + if (this.localesDirectory != null) { + await this.loadMessages(this.localesDirectory, this.translationLocale, this.localeMessages); + if (this.translationLocale !== this.supportedTranslationLocales[0]) { + await this.loadMessages(this.localesDirectory, this.supportedTranslationLocales[0], + this.defaultMessages); + } + } + } + + t(id: string, p1?: string, p2?: string, p3?: string): string { + return this.translate(id, p1, p2, p3); + } + + translate(id: string, p1?: string, p2?: string, p3?: string): string { + let result: string; + if (this.localeMessages.hasOwnProperty(id) && this.localeMessages[id]) { + result = this.localeMessages[id]; + } else if (this.defaultMessages.hasOwnProperty(id) && this.defaultMessages[id]) { + result = this.defaultMessages[id]; + } else { + result = ''; + } + + if (result !== '') { + if (p1 != null) { + result = result.split('__$1__').join(p1); + } + if (p2 != null) { + result = result.split('__$2__').join(p2); + } + if (p3 != null) { + result = result.split('__$3__').join(p3); + } + } + + return result; + } + + private async loadMessages(localesDir: string, locale: string, messagesObj: any): Promise { + const formattedLocale = locale.replace('-', '_'); + const file = await fetch(localesDir + formattedLocale + '/messages.json'); + const locales = await file.json(); + for (const prop in locales) { + if (!locales.hasOwnProperty(prop)) { + continue; + } + messagesObj[prop] = locales[prop].message; + + if (locales[prop].placeholders) { + for (const placeProp in locales[prop].placeholders) { + if (!locales[prop].placeholders.hasOwnProperty(placeProp) || + !locales[prop].placeholders[placeProp].content) { + continue; + } + + const replaceToken = '\\$' + placeProp.toUpperCase() + '\\$'; + let replaceContent = locales[prop].placeholders[placeProp].content; + if (replaceContent === '$1' || replaceContent === '$2' || replaceContent === '$3') { + replaceContent = '__' + replaceContent + '__'; + } + messagesObj[prop] = messagesObj[prop].replace(new RegExp(replaceToken, 'g'), replaceContent); + } + } + } + } + +} diff --git a/src/services/index.ts b/src/services/index.ts index 006e7deaeb..bddd1809b8 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -9,6 +9,7 @@ export { ContainerService } from './container.service'; export { CryptoService } from './crypto.service'; export { EnvironmentService } from './environment.service'; export { FolderService } from './folder.service'; +export { I18nService } from './i18n.service'; export { LockService } from './lock.service'; export { PasswordGenerationService } from './passwordGeneration.service'; export { SearchService } from './search.service'; From 44a9d17dba4806bb822c5d8d5cf4f3100656db84 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 11 Apr 2018 15:16:17 -0400 Subject: [PATCH 0154/1626] abstract away loading messages file json --- src/services/i18n.service.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/services/i18n.service.ts b/src/services/i18n.service.ts index 54d081c143..ba6ef876a8 100644 --- a/src/services/i18n.service.ts +++ b/src/services/i18n.service.ts @@ -11,7 +11,8 @@ export class I18nService implements I18nServiceAbstraction { protected defaultMessages: any = {}; protected localeMessages: any = {}; - constructor(protected systemLanguage: string, protected localesDirectory: string) { } + constructor(protected systemLanguage: string, protected localesDirectory: string, + protected getLocalesJson: (formattedLocale: string) => Promise) { } async init(locale?: string) { if (this.inited) { @@ -34,10 +35,9 @@ export class I18nService implements I18nServiceAbstraction { } if (this.localesDirectory != null) { - await this.loadMessages(this.localesDirectory, this.translationLocale, this.localeMessages); + await this.loadMessages(this.translationLocale, this.localeMessages); if (this.translationLocale !== this.supportedTranslationLocales[0]) { - await this.loadMessages(this.localesDirectory, this.supportedTranslationLocales[0], - this.defaultMessages); + await this.loadMessages(this.supportedTranslationLocales[0], this.defaultMessages); } } } @@ -71,10 +71,9 @@ export class I18nService implements I18nServiceAbstraction { return result; } - private async loadMessages(localesDir: string, locale: string, messagesObj: any): Promise { + private async loadMessages(locale: string, messagesObj: any): Promise { const formattedLocale = locale.replace('-', '_'); - const file = await fetch(localesDir + formattedLocale + '/messages.json'); - const locales = await file.json(); + const locales = await this.getLocalesJson(formattedLocale); for (const prop in locales) { if (!locales.hasOwnProperty(prop)) { continue; From 7bc65b83667b0864af2b7aaf093ec611ecb625d8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 11 Apr 2018 16:27:10 -0400 Subject: [PATCH 0155/1626] return confirmed from delete --- src/angular/components/add-edit.component.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 623dc38338..9a92df409b 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -218,12 +218,12 @@ export class AddEditComponent { this.onEditAttachments.emit(this.cipher); } - async delete() { + async delete(): Promise { const confirmed = await this.platformUtilsService.showDialog( this.i18nService.t('deleteItemConfirmation'), this.i18nService.t('deleteItem'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); if (!confirmed) { - return; + return false; } try { @@ -233,6 +233,8 @@ export class AddEditComponent { this.toasterService.popAsync('success', null, this.i18nService.t('deletedItem')); this.onDeletedCipher.emit(this.cipher); } catch { } + + return true; } async generatePassword(): Promise { From 286e839ebb299d36d3d4af88290bd3dfdf0a23a8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 12 Apr 2018 14:48:12 -0400 Subject: [PATCH 0156/1626] remove unused member --- src/models/view/loginView.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/models/view/loginView.ts b/src/models/view/loginView.ts index 34a0c45685..c36540fec6 100644 --- a/src/models/view/loginView.ts +++ b/src/models/view/loginView.ts @@ -11,10 +11,6 @@ export class LoginView implements View { totp: string; uris: LoginUriView[]; - // tslint:disable - private _username: string; - // tslint:enable - constructor(l?: Login) { // ctor } From c2bdabc0987a4cae6c64b62a43bb122e850fc02b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 12 Apr 2018 15:42:22 -0400 Subject: [PATCH 0157/1626] only search name on edge --- src/angular/pipes/search-ciphers.pipe.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/angular/pipes/search-ciphers.pipe.ts b/src/angular/pipes/search-ciphers.pipe.ts index 1d255885ca..0ffb12b52a 100644 --- a/src/angular/pipes/search-ciphers.pipe.ts +++ b/src/angular/pipes/search-ciphers.pipe.ts @@ -5,10 +5,18 @@ import { import { CipherView } from '../../models/view/cipherView'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + @Pipe({ name: 'searchCiphers', }) export class SearchCiphersPipe implements PipeTransform { + private onlySearchName = false; + + constructor(private platformUtilsService: PlatformUtilsService) { + this.onlySearchName = platformUtilsService.isEdge(); + } + transform(ciphers: CipherView[], searchText: string): CipherView[] { if (ciphers == null || ciphers.length === 0) { return []; @@ -23,6 +31,9 @@ export class SearchCiphersPipe implements PipeTransform { if (c.name != null && c.name.toLowerCase().indexOf(searchText) > -1) { return true; } + if (this.onlySearchName) { + return false; + } if (c.subTitle != null && c.subTitle.toLowerCase().indexOf(searchText) > -1) { return true; } From 2782ae33fcd60f9b84396e54a7d08cfa583f47be Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 12 Apr 2018 15:42:34 -0400 Subject: [PATCH 0158/1626] optionally set loaded --- src/angular/components/groupings.component.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/angular/components/groupings.component.ts b/src/angular/components/groupings.component.ts index 3bec8a866f..0e3a000afe 100644 --- a/src/angular/components/groupings.component.ts +++ b/src/angular/components/groupings.component.ts @@ -35,10 +35,13 @@ export class GroupingsComponent { constructor(protected collectionService: CollectionService, protected folderService: FolderService) { } - async load() { + async load(setLoaded = true) { await this.loadFolders(); await this.loadCollections(); - this.loaded = true; + + if (setLoaded) { + this.loaded = true; + } } async loadCollections() { From df9074a0e58a1b4a499a0682e964013b546d786f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 13 Apr 2018 00:05:58 -0400 Subject: [PATCH 0159/1626] move folder add/edit to jslib --- .../components/folder-add-edit.component.ts | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/angular/components/folder-add-edit.component.ts diff --git a/src/angular/components/folder-add-edit.component.ts b/src/angular/components/folder-add-edit.component.ts new file mode 100644 index 0000000000..c84be649c1 --- /dev/null +++ b/src/angular/components/folder-add-edit.component.ts @@ -0,0 +1,84 @@ +import { + EventEmitter, + Input, + OnInit, + Output, +} from '@angular/core'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { FolderService } from '../../abstractions/folder.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +import { FolderView } from '../../models/view/folderView'; + +export class FolderAddEditComponent implements OnInit { + @Input() folderId: string; + @Output() onSavedFolder = new EventEmitter(); + @Output() onDeletedFolder = new EventEmitter(); + + editMode: boolean = false; + folder: FolderView = new FolderView(); + title: string; + formPromise: Promise; + deletePromise: Promise; + + constructor(protected folderService: FolderService, protected i18nService: I18nService, + protected analytics: Angulartics2, protected toasterService: ToasterService, + protected platformUtilsService: PlatformUtilsService) { } + + async ngOnInit() { + this.editMode = this.folderId != null; + + if (this.editMode) { + this.editMode = true; + this.title = this.i18nService.t('editFolder'); + const folder = await this.folderService.get(this.folderId); + this.folder = await folder.decrypt(); + } else { + this.title = this.i18nService.t('addFolder'); + } + } + + async submit(): Promise { + if (this.folder.name == null || this.folder.name === '') { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('nameRequired')); + return false; + } + + try { + const folder = await this.folderService.encrypt(this.folder); + this.formPromise = this.folderService.saveWithServer(folder); + await this.formPromise; + this.analytics.eventTrack.next({ action: this.editMode ? 'Edited Folder' : 'Added Folder' }); + this.toasterService.popAsync('success', null, + this.i18nService.t(this.editMode ? 'editedFolder' : 'addedFolder')); + this.onSavedFolder.emit(this.folder); + return true; + } catch { } + + return false; + } + + async delete(): Promise { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('deleteFolderConfirmation'), this.i18nService.t('deleteFolder'), + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return false; + } + + try { + this.deletePromise = this.folderService.deleteWithServer(this.folder.id); + await this.deletePromise; + this.analytics.eventTrack.next({ action: 'Deleted Folder' }); + this.toasterService.popAsync('success', null, this.i18nService.t('deletedFolder')); + this.onDeletedFolder.emit(this.folder); + } catch { } + + return true; + } +} From 95cd9153c9ede6bbecc2b65634ef30505be67558 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 13 Apr 2018 13:19:14 -0400 Subject: [PATCH 0160/1626] move premium page to jslib --- src/angular/components/premium.component.ts | 50 +++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/angular/components/premium.component.ts diff --git a/src/angular/components/premium.component.ts b/src/angular/components/premium.component.ts new file mode 100644 index 0000000000..fb90fa50f2 --- /dev/null +++ b/src/angular/components/premium.component.ts @@ -0,0 +1,50 @@ +import { OnInit } from '@angular/core'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { ApiService } from '../../abstractions/api.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { TokenService } from '../../abstractions/token.service'; + +export class PremiumComponent implements OnInit { + isPremium: boolean = false; + price: string = '$10'; + refreshPromise: Promise; + + constructor(protected analytics: Angulartics2, protected toasterService: ToasterService, + protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, + protected tokenService: TokenService, protected apiService: ApiService) { } + + async ngOnInit() { + this.isPremium = this.tokenService.getPremium(); + } + + async refresh() { + try { + this.refreshPromise = this.apiService.refreshIdentityToken(); + await this.refreshPromise; + this.toasterService.popAsync('success', null, this.i18nService.t('refreshComplete')); + this.isPremium = this.tokenService.getPremium(); + } catch { } + } + + async purchase() { + const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('premiumPurchaseAlert'), + this.i18nService.t('premiumPurchase'), this.i18nService.t('yes'), this.i18nService.t('cancel')); + if (confirmed) { + this.analytics.eventTrack.next({ action: 'Clicked Purchase Premium' }); + this.platformUtilsService.launchUri('https://vault.bitwarden.com/#/?premium=purchase'); + } + } + + async manage() { + const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('premiumManageAlert'), + this.i18nService.t('premiumManage'), this.i18nService.t('yes'), this.i18nService.t('cancel')); + if (confirmed) { + this.analytics.eventTrack.next({ action: 'Clicked Manage Membership' }); + this.platformUtilsService.launchUri('https://vault.bitwarden.com/#/?premium=manage'); + } + } +} From 5608f119252ca29ddae8fe98599efd27f7922a2e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 13 Apr 2018 23:55:07 -0400 Subject: [PATCH 0161/1626] formatting fixes --- src/services/audit.service.ts | 3 +-- src/services/crypto.service.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/services/audit.service.ts b/src/services/audit.service.ts index 60553b834e..57279a09da 100644 --- a/src/services/audit.service.ts +++ b/src/services/audit.service.ts @@ -4,8 +4,7 @@ import { CryptoService } from '../abstractions/crypto.service'; const PwnedPasswordsApi = 'https://api.pwnedpasswords.com/range/'; export class AuditService implements AuditServiceAbstraction { - constructor(private cryptoService: CryptoService) { - } + constructor(private cryptoService: CryptoService) { } async passwordLeaked(password: string): Promise { const hash = (await this.cryptoService.sha1(password)).toUpperCase(); diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 243afb66aa..b8db2f048e 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -454,7 +454,7 @@ export class CryptoService implements CryptoServiceAbstraction { async sha1(password: string): Promise { const passwordArr = UtilsService.fromUtf8ToArray(password); - const hash = await Crypto.subtle.digest({ name: 'SHA-1' }, passwordArr); + const hash = await Subtle.digest({ name: 'SHA-1' }, passwordArr); return UtilsService.fromBufferToHex(hash); } From e883dfdaac6b475d032bac9be6aca7cd18d70861 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 14 Apr 2018 00:16:12 -0400 Subject: [PATCH 0162/1626] remove empty uri on add --- src/angular/components/add-edit.component.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 9a92df409b..44b4afb7eb 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -154,6 +154,11 @@ export class AddEditComponent { return false; } + if (!this.editMode && this.cipher.type == CipherType.Login && this.cipher.login.uris.length === 1 && + (this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === '')) { + this.cipher.login.uris = null; + } + const cipher = await this.cipherService.encrypt(this.cipher); try { From 831b4cff7b8a4969bb050cad5e16f372e0c5ec30 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 14 Apr 2018 10:01:40 -0400 Subject: [PATCH 0163/1626] hostname fix --- src/services/utils.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/utils.service.ts b/src/services/utils.service.ts index efabb5ff0d..390d710924 100644 --- a/src/services/utils.service.ts +++ b/src/services/utils.service.ts @@ -151,7 +151,7 @@ export class UtilsService implements UtilsServiceAbstraction { return null; } - if (uriString.indexOf('://') === -1) { + if (uriString.indexOf('://') === -1 && uriString.indexOf('.') > -1) { uriString = 'http://' + uriString; } From 2930d7daf7ce31ea029153a5b54f167277ce3ea0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 14 Apr 2018 13:38:04 -0400 Subject: [PATCH 0164/1626] === --- src/angular/components/add-edit.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 44b4afb7eb..dced8314a1 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -154,7 +154,7 @@ export class AddEditComponent { return false; } - if (!this.editMode && this.cipher.type == CipherType.Login && this.cipher.login.uris.length === 1 && + if (!this.editMode && this.cipher.type === CipherType.Login && this.cipher.login.uris.length === 1 && (this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === '')) { this.cipher.login.uris = null; } From 94ca579168368a9075554cdb01dfbee47d8774aa Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 14 Apr 2018 20:17:10 -0400 Subject: [PATCH 0165/1626] sanitize system language --- src/services/i18n.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/i18n.service.ts b/src/services/i18n.service.ts index ba6ef876a8..6a6fa69c9a 100644 --- a/src/services/i18n.service.ts +++ b/src/services/i18n.service.ts @@ -12,7 +12,9 @@ export class I18nService implements I18nServiceAbstraction { protected localeMessages: any = {}; constructor(protected systemLanguage: string, protected localesDirectory: string, - protected getLocalesJson: (formattedLocale: string) => Promise) { } + protected getLocalesJson: (formattedLocale: string) => Promise) { + this.systemLanguage = systemLanguage.replace('_', '-'); + } async init(locale?: string) { if (this.inited) { From 759ac04ee93a447339d40afa57ac513b3c130f9c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 14 Apr 2018 20:26:04 -0400 Subject: [PATCH 0166/1626] try catch collator set --- src/services/i18n.service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/services/i18n.service.ts b/src/services/i18n.service.ts index 6a6fa69c9a..9b9aaea68f 100644 --- a/src/services/i18n.service.ts +++ b/src/services/i18n.service.ts @@ -26,7 +26,12 @@ export class I18nService implements I18nServiceAbstraction { this.inited = true; this.locale = this.translationLocale = locale != null ? locale : this.systemLanguage; - this.collator = new Intl.Collator(this.locale); + + try { + this.collator = new Intl.Collator(this.locale); + } catch { + this.collator = null; + } if (this.supportedTranslationLocales.indexOf(this.translationLocale) === -1) { this.translationLocale = this.translationLocale.slice(0, 2); From 4ad29e25f3ba048521b71b4faa48b13836de9019 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 14 Apr 2018 23:20:37 -0400 Subject: [PATCH 0167/1626] stub out crypto function service with pbkdf2 --- package-lock.json | 10 +++--- package.json | 1 + src/abstractions/cryptoFunction.service.ts | 3 ++ src/services/nodeCryptoFunction.service.ts | 17 +++++++++ src/services/webCryptoFunction.service.ts | 42 ++++++++++++++++++++++ 5 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 src/abstractions/cryptoFunction.service.ts create mode 100644 src/services/nodeCryptoFunction.service.ts create mode 100644 src/services/webCryptoFunction.service.ts diff --git a/package-lock.json b/package-lock.json index 5f769ef6a9..4d7d5667fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,7 +100,7 @@ "integrity": "sha512-PlKJw6ujJXLJjbvB3T0UCbY3jibKM6/Ya5cc9j1q+mYDeK3aR4Dp+20ZwxSuvJr9mIoPxp7+IL4aMOEvsscRTA==", "dev": true, "requires": { - "@types/node": "8.5.7" + "@types/node": "8.0.19" } }, "@types/handlebars": { @@ -140,9 +140,9 @@ "dev": true }, "@types/node": { - "version": "8.5.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.5.7.tgz", - "integrity": "sha512-+1ZfzGIq8Y3EV7hPF7bs3i+Gi2mqYOiEGGRxGYPrn+hTYLMmzg+/5TkMkCHiRtLB38XSNvr/43aQ9+cUq4BbBg==", + "version": "8.0.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.19.tgz", + "integrity": "sha512-VRQB+Q0L3YZWs45uRdpN9oWr82meL/8TrJ6faoKT5tp0uub2l/aRMhtm5fo68h7kjYKH60f9/bay1nF7ZpTW5g==", "dev": true }, "@types/node-forge": { @@ -163,7 +163,7 @@ "integrity": "sha1-IpwVfGvB5n1rmQ5sXhjb0v9Yz/A=", "dev": true, "requires": { - "@types/node": "8.5.7" + "@types/node": "8.0.19" } }, "@types/webcrypto": { diff --git a/package.json b/package.json index b63e1baeb6..5d4f7e55dc 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@angular/router": "5.2.0", "@angular/upgrade": "5.2.0", "@types/lunr": "2.1.5", + "@types/node": "8.0.19", "@types/node-forge": "0.7.1", "@types/papaparse": "4.1.31", "@types/webcrypto": "0.0.28", diff --git a/src/abstractions/cryptoFunction.service.ts b/src/abstractions/cryptoFunction.service.ts new file mode 100644 index 0000000000..5daffbd4f2 --- /dev/null +++ b/src/abstractions/cryptoFunction.service.ts @@ -0,0 +1,3 @@ +export abstract class CryptoFunctionService { + pbkdf2: (password: Buffer, salt: Buffer, iterations: number, length: number) => Promise +} diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts new file mode 100644 index 0000000000..750b18a78d --- /dev/null +++ b/src/services/nodeCryptoFunction.service.ts @@ -0,0 +1,17 @@ +import * as crypto from 'crypto'; + +import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; + +export class NodeCryptoFunctionService implements CryptoFunctionService { + async pbkdf2(password: Buffer, salt: Buffer, iterations: number, length: number): Promise { + return new Promise((resolve, reject) => { + crypto.pbkdf2(password, salt, iterations, length, 'sha256', (error, key) => { + if (error != null) { + reject(error); + } else { + resolve(key.buffer); + } + }); + }); + } +} diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts new file mode 100644 index 0000000000..ea563b32c8 --- /dev/null +++ b/src/services/webCryptoFunction.service.ts @@ -0,0 +1,42 @@ +import * as forge from 'node-forge'; + +import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; + +export class WebCryptoFunctionService implements CryptoFunctionService { + private crypto: Crypto; + private subtle: SubtleCrypto; + + constructor(private win: Window, private platformUtilsService: PlatformUtilsService) { + this.crypto = win.crypto; + this.subtle = win.crypto.subtle; + } + + async pbkdf2(password: Buffer, salt: Buffer, iterations: number, length: number): Promise { + const importedKey = await this.subtle.importKey('raw', password, { name: 'PBKDF2' }, + false, ['deriveKey', 'deriveBits']); + + const alg: Pbkdf2Params = { + name: 'PBKDF2', + salt: salt, + iterations: iterations, + hash: { name: 'SHA-256' }, + }; + + const keyType: AesDerivedKeyParams = { + name: 'AES-CBC', + length: length, + }; + + const derivedKey = await this.subtle.deriveKey(alg, importedKey, keyType, true, ['encrypt', 'decrypt']); + return await this.subtle.exportKey('raw', derivedKey); + } + + async sha1(value: Buffer): Promise { + if (this.platformUtilsService.isEdge()) { + return new Uint8Array([1]).buffer; // TODO: sha1 with forge + } else { + return await this.subtle.digest({ name: 'SHA-1' }, value); + } + } +} From d6474aee0e84c933c5a36aa2fa20b209be18d74c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 16 Apr 2018 12:07:51 -0400 Subject: [PATCH 0168/1626] crypto function implementations --- src/abstractions/cryptoFunction.service.ts | 3 +- src/services/nodeCryptoFunction.service.ts | 19 ++++++++- src/services/webCryptoFunction.service.ts | 48 ++++++++++++++++++---- 3 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/abstractions/cryptoFunction.service.ts b/src/abstractions/cryptoFunction.service.ts index 5daffbd4f2..2385977b5e 100644 --- a/src/abstractions/cryptoFunction.service.ts +++ b/src/abstractions/cryptoFunction.service.ts @@ -1,3 +1,4 @@ export abstract class CryptoFunctionService { - pbkdf2: (password: Buffer, salt: Buffer, iterations: number, length: number) => Promise + pbkdf2: (password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', + iterations: number, length: number) => Promise; } diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index 750b18a78d..ed53e8d5c3 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -3,9 +3,24 @@ import * as crypto from 'crypto'; import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; export class NodeCryptoFunctionService implements CryptoFunctionService { - async pbkdf2(password: Buffer, salt: Buffer, iterations: number, length: number): Promise { + async pbkdf2(password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', + iterations: number, length: number): Promise { + let nodePassword: string | Buffer; + if (typeof (password) === 'string') { + nodePassword = password; + } else { + nodePassword = Buffer.from(new Uint8Array(password) as any); + } + + let nodeSalt: string | Buffer; + if (typeof (salt) === 'string') { + nodeSalt = salt; + } else { + nodeSalt = Buffer.from(new Uint8Array(salt) as any); + } + return new Promise((resolve, reject) => { - crypto.pbkdf2(password, salt, iterations, length, 'sha256', (error, key) => { + crypto.pbkdf2(nodePassword, nodeSalt, iterations, length, algorithm, (error, key) => { if (error != null) { reject(error); } else { diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index ea563b32c8..91e3d7d90f 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -3,6 +3,8 @@ import * as forge from 'node-forge'; import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; +import { UtilsService } from '../services/utils.service'; + export class WebCryptoFunctionService implements CryptoFunctionService { private crypto: Crypto; private subtle: SubtleCrypto; @@ -12,15 +14,35 @@ export class WebCryptoFunctionService implements CryptoFunctionService { this.subtle = win.crypto.subtle; } - async pbkdf2(password: Buffer, salt: Buffer, iterations: number, length: number): Promise { - const importedKey = await this.subtle.importKey('raw', password, { name: 'PBKDF2' }, + async pbkdf2(password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', + iterations: number, length: number): Promise { + if (this.platformUtilsService.isEdge()) { + // TODO + return new Uint8Array([]).buffer; + } + + let passwordBuf: ArrayBuffer; + if (typeof (password) === 'string') { + passwordBuf = UtilsService.fromUtf8ToArray(password).buffer; + } else { + passwordBuf = password; + } + + let saltBuf: ArrayBuffer; + if (typeof (salt) === 'string') { + saltBuf = UtilsService.fromUtf8ToArray(salt).buffer; + } else { + saltBuf = salt; + } + + const importedKey = await this.subtle.importKey('raw', passwordBuf, { name: 'PBKDF2' }, false, ['deriveKey', 'deriveBits']); const alg: Pbkdf2Params = { name: 'PBKDF2', - salt: salt, + salt: saltBuf, iterations: iterations, - hash: { name: 'SHA-256' }, + hash: { name: algorithm === 'sha256' ? 'SHA-256' : 'SHA-512' }, }; const keyType: AesDerivedKeyParams = { @@ -32,11 +54,21 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return await this.subtle.exportKey('raw', derivedKey); } - async sha1(value: Buffer): Promise { + async hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256'): Promise { if (this.platformUtilsService.isEdge()) { - return new Uint8Array([1]).buffer; // TODO: sha1 with forge - } else { - return await this.subtle.digest({ name: 'SHA-1' }, value); + // TODO + return new Uint8Array([]).buffer; } + + let valueBuf: ArrayBuffer; + if (typeof (value) === 'string') { + valueBuf = UtilsService.fromUtf8ToArray(value).buffer; + } else { + valueBuf = value; + } + + return await this.subtle.digest({ + name: algorithm === 'sha256' ? 'SHA-256' : 'SHA-1' + }, valueBuf); } } From 5d085b74f5910cb84da7cc546cd408fba4bed39b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 17 Apr 2018 15:59:38 -0400 Subject: [PATCH 0169/1626] crypto function implementations for hash --- src/abstractions/cryptoFunction.service.ts | 1 + src/services/nodeCryptoFunction.service.ts | 33 +++++----- src/services/webCryptoFunction.service.ts | 73 ++++++++++++++-------- 3 files changed, 67 insertions(+), 40 deletions(-) diff --git a/src/abstractions/cryptoFunction.service.ts b/src/abstractions/cryptoFunction.service.ts index 2385977b5e..a992da8379 100644 --- a/src/abstractions/cryptoFunction.service.ts +++ b/src/abstractions/cryptoFunction.service.ts @@ -1,4 +1,5 @@ export abstract class CryptoFunctionService { pbkdf2: (password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', iterations: number, length: number) => Promise; + hash: (value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise; } diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index ed53e8d5c3..0c0d4aaf00 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -5,20 +5,8 @@ import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; export class NodeCryptoFunctionService implements CryptoFunctionService { async pbkdf2(password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', iterations: number, length: number): Promise { - let nodePassword: string | Buffer; - if (typeof (password) === 'string') { - nodePassword = password; - } else { - nodePassword = Buffer.from(new Uint8Array(password) as any); - } - - let nodeSalt: string | Buffer; - if (typeof (salt) === 'string') { - nodeSalt = salt; - } else { - nodeSalt = Buffer.from(new Uint8Array(salt) as any); - } - + const nodePassword = this.toNodeValue(password); + const nodeSalt = this.toNodeValue(salt); return new Promise((resolve, reject) => { crypto.pbkdf2(nodePassword, nodeSalt, iterations, length, algorithm, (error, key) => { if (error != null) { @@ -29,4 +17,21 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { }); }); } + + async hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { + const nodeValue = this.toNodeValue(value); + const hash = crypto.createHash(algorithm); + hash.update(nodeValue); + return hash.digest().buffer; + } + + private toNodeValue(value: string | ArrayBuffer): string | Buffer { + let nodeValue: string | Buffer; + if (typeof (value) === 'string') { + nodeValue = value; + } else { + nodeValue = Buffer.from(new Uint8Array(value) as any); + } + return nodeValue; + } } diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index 91e3d7d90f..2eb4e68a03 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -17,23 +17,14 @@ export class WebCryptoFunctionService implements CryptoFunctionService { async pbkdf2(password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', iterations: number, length: number): Promise { if (this.platformUtilsService.isEdge()) { - // TODO - return new Uint8Array([]).buffer; + const passwordBytes = this.toForgeBytes(password); + const saltBytes = this.toForgeBytes(salt); + const derivedKeyBytes = (forge as any).pbkdf2(passwordBytes, saltBytes, iterations, length / 8, algorithm); + return this.fromForgeBytesToBuf(derivedKeyBytes); } - let passwordBuf: ArrayBuffer; - if (typeof (password) === 'string') { - passwordBuf = UtilsService.fromUtf8ToArray(password).buffer; - } else { - passwordBuf = password; - } - - let saltBuf: ArrayBuffer; - if (typeof (salt) === 'string') { - saltBuf = UtilsService.fromUtf8ToArray(salt).buffer; - } else { - saltBuf = salt; - } + const passwordBuf = this.toBuf(password); + const saltBuf = this.toBuf(salt); const importedKey = await this.subtle.importKey('raw', passwordBuf, { name: 'PBKDF2' }, false, ['deriveKey', 'deriveBits']); @@ -54,21 +45,51 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return await this.subtle.exportKey('raw', derivedKey); } - async hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256'): Promise { + async hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { if (this.platformUtilsService.isEdge()) { - // TODO - return new Uint8Array([]).buffer; - } - - let valueBuf: ArrayBuffer; - if (typeof (value) === 'string') { - valueBuf = UtilsService.fromUtf8ToArray(value).buffer; - } else { - valueBuf = value; + let md: forge.md.MessageDigest; + if (algorithm === 'sha1') { + md = forge.md.sha1.create(); + } else if (algorithm === 'sha256') { + md = forge.md.sha256.create(); + } else { + md = (forge as any).md.sha512.create(); + } + + const valueBytes = this.toForgeBytes(value); + md.update(valueBytes, 'raw'); + return this.fromForgeBytesToBuf(md.digest().data); } + const valueBuf = this.toBuf(value); return await this.subtle.digest({ - name: algorithm === 'sha256' ? 'SHA-256' : 'SHA-1' + name: algorithm === 'sha1' ? 'SHA-1' : algorithm === 'sha256' ? 'SHA-256' : 'SHA-512' }, valueBuf); } + + private toBuf(value: string | ArrayBuffer): ArrayBuffer { + let buf: ArrayBuffer; + if (typeof (value) === 'string') { + buf = UtilsService.fromUtf8ToArray(value).buffer; + } else { + buf = value; + } + return buf; + } + + private toForgeBytes(value: string | ArrayBuffer): string { + let bytes: string; + if (typeof (value) === 'string') { + bytes = forge.util.encodeUtf8(value); + } else { + const value64 = UtilsService.fromBufferToB64(value); + bytes = forge.util.encode64(value64); + } + return bytes; + } + + private fromForgeBytesToBuf(byteString: string): ArrayBuffer { + const b64 = forge.util.decode64(byteString); + return UtilsService.fromB64ToArray(b64).buffer; + } } From 709e2eb3c043d81a5f3978c893dd5e93d717ffac Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 17 Apr 2018 16:15:19 -0400 Subject: [PATCH 0170/1626] setup karma testing --- .gitignore | 1 + karma.conf.js | 65 + package-lock.json | 5153 +++++++++++++++++++++++++++- package.json | 40 +- src/services/utils.service.spec.ts | 34 + 5 files changed, 5118 insertions(+), 175 deletions(-) create mode 100644 karma.conf.js create mode 100644 src/services/utils.service.spec.ts diff --git a/.gitignore b/.gitignore index cdbb247cb1..20b786361b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ vwd.webinfo *.crx *.pem dist +coverage diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000000..726902707d --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,65 @@ +module.exports = function(config) { + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['jasmine', 'karma-typescript'], + + // list of files / patterns to load in the browser + files: [ + 'src/abstractions/**/*.ts', + 'src/enums/**/*.ts', + 'src/models/**/*.ts', + 'src/services/**/*.ts' + ], + + // list of files to exclude + exclude: [ + ], + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + '**/*.ts': 'karma-typescript' + }, + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress', 'karma-typescript', 'kjhtml'], + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['Chrome'], + + // Concurrency level + // how many browser should be started simultaneous + concurrency: Infinity, + + client:{ + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + + karmaTypescriptConfig: { + tsconfig: './tsconfig.json', + compilerOptions: { + module: 'CommonJS' + }, + bundlerOptions: { + entrypoints: /\.spec\.ts$/ + } + }, + }) +} diff --git a/package-lock.json b/package-lock.json index 4d7d5667fe..5f37db06ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-5.2.0.tgz", "integrity": "sha512-JLR42YHiJppO4ruAkFxgbzghUDtHkXHkKPM8udd2qyt16T7e1OX7EEOrrmldUu59CC56tZnJ/32p4SrYmxyBSA==", - "dev": true, "requires": { "tslib": "1.9.0" } @@ -17,7 +16,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@angular/common/-/common-5.2.0.tgz", "integrity": "sha512-yMFn2isC7/XOs56/2Kzzbb1AASHiwipAPOVFtKe7TdZQClO8fJXwCnk326rzr615+CG0eSBNQWeiFGyWN2riBA==", - "dev": true, "requires": { "tslib": "1.9.0" } @@ -26,7 +24,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-5.2.0.tgz", "integrity": "sha512-RfYa4ESgjGX0T0ob/Xz00IF7nd2xZkoyRy6oKgL82q42uzB3xZUDMrFNgeGxAUs3H22IkL46/5SSPOMOTMZ0NA==", - "dev": true, "requires": { "tslib": "1.9.0" } @@ -35,7 +32,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@angular/core/-/core-5.2.0.tgz", "integrity": "sha512-s2ne45DguNUubhC1YgybGECC4Tyx3G4EZCntUiRMDWWkmKXSK+6dgHMesyDo8R5Oat8VfN4Anf8l3JHS1He8kg==", - "dev": true, "requires": { "tslib": "1.9.0" } @@ -44,7 +40,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-5.2.0.tgz", "integrity": "sha512-g1/SF9lY0ZwzJ0w4NXbFsTGGEuUdgtaZny8DmkaqtmA7idby3FW398X0tv25KQfVYKtL+p9Jp1Y8EI0CvrIsvw==", - "dev": true, "requires": { "tslib": "1.9.0" } @@ -53,7 +48,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@angular/http/-/http-5.2.0.tgz", "integrity": "sha512-V5Cl24dP3rCXTTQvDc0TIKoWqBRAa0DWAQbtr7iuDAt5a1vPGdKz5K1sEiiV6ziwX6gzjiwHjUvL+B+WbIUrQA==", - "dev": true, "requires": { "tslib": "1.9.0" } @@ -62,7 +56,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-5.2.0.tgz", "integrity": "sha512-c6cR15MfopPwGZ097HdRuAi9+R9BhA3bRRFpP2HmrSSB/BW4ZNovUYwB2QUMSYbd9s0lYTtnavqGm6DKcyF2QA==", - "dev": true, "requires": { "tslib": "1.9.0" } @@ -71,7 +64,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-5.2.0.tgz", "integrity": "sha512-xG1eNoi8sm4Jcly2y98r5mqYVe3XV8sUJCtOhvGBYtvt4dKEQ5tOns6fWQ0nUbl6Vv3Y0xgGUS1JCtfut3DuaQ==", - "dev": true, "requires": { "tslib": "1.9.0" } @@ -80,7 +72,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@angular/router/-/router-5.2.0.tgz", "integrity": "sha512-VXDXtp2A1GQEUEhXg0ZzqHdTUERLgDSo3/Mmpzt+dgLMKlXDSCykcm4gINwE5VQLGD1zQvDFCCRv3seGRNfrqA==", - "dev": true, "requires": { "tslib": "1.9.0" } @@ -89,36 +80,14 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@angular/upgrade/-/upgrade-5.2.0.tgz", "integrity": "sha512-ezWfhBCiP7RX+59scxfYfjDMRw+qq0BVbm/EfOXdYFU0NHWo7lXJ3v+cUi18G+5GVjzwRiJDIKWhw1QEyq2nug==", - "dev": true, "requires": { "tslib": "1.9.0" } }, - "@types/fs-extra": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-4.0.0.tgz", - "integrity": "sha512-PlKJw6ujJXLJjbvB3T0UCbY3jibKM6/Ya5cc9j1q+mYDeK3aR4Dp+20ZwxSuvJr9mIoPxp7+IL4aMOEvsscRTA==", - "dev": true, - "requires": { - "@types/node": "8.0.19" - } - }, - "@types/handlebars": { - "version": "4.0.31", - "resolved": "https://registry.npmjs.org/@types/handlebars/-/handlebars-4.0.31.tgz", - "integrity": "sha1-p/umb6/kJxOu6I7sqNuRGS7+bnI=", - "dev": true - }, - "@types/highlight.js": { - "version": "9.1.8", - "resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.1.8.tgz", - "integrity": "sha1-0ifxi8uPPxh+FpZfJESFmgRol1g=", - "dev": true - }, - "@types/lodash": { - "version": "4.14.74", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.74.tgz", - "integrity": "sha512-BZknw3E/z3JmCLqQVANcR17okqVTPZdlxvcIz0fJiJVLUCbSH1hK3zs9r634PVSmrzAxN+n/fxlVRiYoArdOIQ==", + "@types/jasmine": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.6.tgz", + "integrity": "sha512-clg9raJTY0EOo5pVZKX3ZlMjlYzVU73L71q5OV1jhE2Uezb7oF94jh4CvwrW6wInquQAdhOxJz5VDF2TLUGmmA==", "dev": true }, "@types/lunr": { @@ -127,18 +96,6 @@ "integrity": "sha512-esk3CG25hRtHsVHm+LOjiSFYdw8be3uIY653WUwR43Bro914HSimPgPpqgajkhTJ0awK3RQfaIxP7zvbtCpcyg==", "dev": true }, - "@types/marked": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.3.0.tgz", - "integrity": "sha512-CSf9YWJdX1DkTNu9zcNtdCcn6hkRtB5ILjbhRId4ZOQqx30fXmdecuaXhugQL6eyrhuXtaHJ7PHI+Vm7k9ZJjg==", - "dev": true - }, - "@types/minimatch": { - "version": "2.0.29", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-2.0.29.tgz", - "integrity": "sha1-UALhT3Xi1x5WQoHfBDHIwbSio2o=", - "dev": true - }, "@types/node": { "version": "8.0.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.19.tgz", @@ -157,21 +114,40 @@ "integrity": "sha512-8+d1hk3GgF+NJ6mMZZ5zKimqIOc+8OTzpLw4RQ8wnS1NkJh/dMH3NEhSud4Ituq2SGXJjOG6wIczCBAKsSsBdQ==", "dev": true }, - "@types/shelljs": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.7.0.tgz", - "integrity": "sha1-IpwVfGvB5n1rmQ5sXhjb0v9Yz/A=", - "dev": true, - "requires": { - "@types/node": "8.0.19" - } - }, "@types/webcrypto": { "version": "0.0.28", "resolved": "https://registry.npmjs.org/@types/webcrypto/-/webcrypto-0.0.28.tgz", "integrity": "sha1-sGFgOQzQAPsgH2LrAqw0Hr9+YP4=", "dev": true }, + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "accepts": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", + "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=", + "dev": true, + "requires": { + "mime-types": "2.1.18", + "negotiator": "0.6.1" + } + }, + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", + "dev": true + }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", + "dev": true + }, "align-text": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", @@ -192,18 +168,71 @@ "angular2-toaster": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/angular2-toaster/-/angular2-toaster-4.0.2.tgz", - "integrity": "sha512-/ndYYbV/7WZx6ujm6avFUqfb+FKbrx7oT+3mYj8i0o9N26Ug+BseFjy6oRnlVVedl39yRP6hhea81QgKmoYbbQ==", - "dev": true + "integrity": "sha512-/ndYYbV/7WZx6ujm6avFUqfb+FKbrx7oT+3mYj8i0o9N26Ug+BseFjy6oRnlVVedl39yRP6hhea81QgKmoYbbQ==" }, "angulartics2": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/angulartics2/-/angulartics2-5.0.1.tgz", "integrity": "sha512-QYBp7km7xTf/57zKKnYreM0OQ1Pq0kd4L9HJTC79vy7+RG1XqrkA944jTGKDERLWtjEAlQuSyZMS9J5IZZ56sw==", - "dev": true, "requires": { "tslib": "1.9.0" } }, + "ansi-cyan": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", + "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-red": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", + "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "ansi-wrap": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", + "dev": true + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "2.3.11", + "normalize-path": "2.1.1" + } + }, + "append-transform": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", + "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=", + "dev": true, + "requires": { + "default-require-extensions": "1.0.0" + } + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -213,17 +242,273 @@ "sprintf-js": "1.0.3" } }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", + "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "arraybuffer.slice": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz", + "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco=", + "dev": true + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "dev": true, + "requires": { + "util": "0.10.3" + } + }, "async": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", "dev": true }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": true, + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.4", + "source-map": "0.5.7", + "trim-right": "1.0.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "requires": { + "core-js": "2.4.1", + "regenerator-runtime": "0.11.1" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash": "4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.9", + "globals": "9.18.0", + "invariant": "2.2.4", + "lodash": "4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.4", + "to-fast-properties": "1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", + "dev": true + }, "balanced-match": { "version": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", + "dev": true + }, + "base64-js": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.3.tgz", + "integrity": "sha512-MsAhsUW1GxCdgYSO6tAfZrNapmUKk7mWx/k5mFY/A1gBtkaCaNapTg+FExCw1r9yeaZhqx/xPg43xgTFH6KL5w==", + "dev": true + }, + "base64id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", + "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=", + "dev": true + }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "dev": true, + "requires": { + "callsite": "1.0.0" + } + }, + "binary-extensions": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", + "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", + "dev": true + }, + "blob": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", + "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=", + "dev": true + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", + "dev": true + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "body-parser": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "dev": true, + "requires": { + "bytes": "3.0.0", + "content-type": "1.0.4", + "debug": "2.6.9", + "depd": "1.1.2", + "http-errors": "1.6.3", + "iconv-lite": "0.4.19", + "on-finished": "2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "1.6.16" + } + }, "brace-expansion": { "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", @@ -233,6 +518,174 @@ "concat-map": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" } }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browser-resolve": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz", + "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=", + "dev": true, + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } + } + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "1.0.3", + "cipher-base": "1.0.4", + "create-hash": "1.2.0", + "evp_bytestokey": "1.0.3", + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "1.2.0", + "browserify-des": "1.0.1", + "evp_bytestokey": "1.0.3" + } + }, + "browserify-des": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.1.tgz", + "integrity": "sha512-zy0Cobe3hhgpiOM32Tj7KQ3Vl91m0njwsjzZQK1L+JDf11dzP9qIvjreVinsvXrgfjhStXwUWAEpB9D7Gwmayw==", + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "des.js": "1.0.0", + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "randombytes": "2.0.6" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.2.0", + "create-hmac": "1.1.7", + "elliptic": "6.4.0", + "inherits": "2.0.3", + "parse-asn1": "5.1.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "1.0.6" + } + }, + "buffer": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.1.0.tgz", + "integrity": "sha512-YkIRgwsZwJWTnyQrsBTWefizHh+8GYj3kbL1BTiAQ/9pwpino0G7B2gp5tx/FUBqUlvtxV85KNR3mwfAtv15Yw==", + "dev": true, + "requires": { + "base64-js": "1.2.3", + "ieee754": "1.1.11" + } + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", + "dev": true + }, "camelcase": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", @@ -240,6 +693,24 @@ "dev": true, "optional": true }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "2.1.1", + "map-obj": "1.0.1" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + } + } + }, "center-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", @@ -251,6 +722,68 @@ "lazy-cache": "1.0.4" } }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "1.3.2", + "async-each": "1.0.1", + "fsevents": "1.1.3", + "glob-parent": "2.0.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "2.0.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + } + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, "cliui": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", @@ -272,23 +805,548 @@ } } }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + }, + "combine-lists": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/combine-lists/-/combine-lists-1.0.1.tgz", + "integrity": "sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y=", + "dev": true, + "requires": { + "lodash": "4.17.4" + } + }, + "combine-source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", + "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=", + "dev": true, + "requires": { + "convert-source-map": "1.1.3", + "inline-source-map": "0.6.2", + "lodash.memoize": "3.0.4", + "source-map": "0.5.7" + }, + "dependencies": { + "convert-source-map": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "compare-versions": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.1.0.tgz", + "integrity": "sha512-4hAxDSBypT/yp2ySFD346So6Ragw5xmBn/e/agIGl3bZr6DLUqnoRZPusxKrXdYRZpgexO9daejmIenlq/wrIQ==", + "dev": true + }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", + "dev": true + }, + "component-emitter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz", + "integrity": "sha1-KWWU8nU9qmOZbSrwjRWpURbJrsM=", + "dev": true + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", + "dev": true + }, "concat-map": { "version": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "connect": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", + "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=", + "dev": true, + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.0", + "parseurl": "1.3.2", + "utils-merge": "1.0.1" + } + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "0.1.4" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "convert-source-map": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", + "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", + "dev": true + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "dev": true + }, "core-js": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", - "integrity": "sha1-TekR5mew6ukSTjQlS1OupvxhjT4=", + "integrity": "sha1-TekR5mew6ukSTjQlS1OupvxhjT4=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, + "create-ecdh": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.1.tgz", + "integrity": "sha512-iZvCCg8XqHQZ1ioNBTzXS/cQSkqkqcPs8xSX4upNB+DAk9Ht3uzQf2J32uAHNCne8LDmKr29AgZrEs4oIrwLuQ==", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "elliptic": "6.4.0" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "inherits": "2.0.3", + "md5.js": "1.3.4", + "ripemd160": "2.0.1", + "sha.js": "2.4.11" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "create-hash": "1.2.0", + "inherits": "2.0.3", + "ripemd160": "2.0.1", + "safe-buffer": "5.1.1", + "sha.js": "2.4.11" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "1.0.1", + "browserify-sign": "4.0.4", + "create-ecdh": "4.0.1", + "create-hash": "1.2.0", + "create-hmac": "1.1.7", + "diffie-hellman": "5.0.3", + "inherits": "2.0.3", + "pbkdf2": "3.0.14", + "public-encrypt": "4.0.2", + "randombytes": "2.0.6", + "randomfill": "1.0.4" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "1.0.2" + } + }, + "custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", + "dev": true + }, + "date-format": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-0.0.0.tgz", + "integrity": "sha1-CSBoY6sHDrRZrOpVQsvYVrEZZrM=", + "dev": true + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "dateformat": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", + "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", + "dev": true, + "requires": { + "get-stdin": "4.0.1", + "meow": "3.7.0" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "default-require-extensions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", + "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=", "dev": true, - "optional": true + "requires": { + "strip-bom": "2.0.0" + } + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "1.0.4" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", + "dev": true + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "miller-rabin": "4.0.1", + "randombytes": "2.0.6" + } + }, + "dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", + "dev": true, + "requires": { + "custom-event": "1.0.1", + "ent": "2.2.0", + "extend": "3.0.1", + "void-elements": "2.0.1" + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "elliptic": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", + "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0", + "hash.js": "1.1.3", + "hmac-drbg": "1.0.1", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1", + "minimalistic-crypto-utils": "1.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "engine.io": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.8.3.tgz", + "integrity": "sha1-jef5eJXSDTm4X4ju7nd7K9QrE9Q=", + "dev": true, + "requires": { + "accepts": "1.3.3", + "base64id": "1.0.0", + "cookie": "0.3.1", + "debug": "2.3.3", + "engine.io-parser": "1.3.2", + "ws": "1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "engine.io-client": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.8.3.tgz", + "integrity": "sha1-F5jtk0USRkU9TG9jXXogH+lA1as=", + "dev": true, + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "2.3.3", + "engine.io-parser": "1.3.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parsejson": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "1.1.2", + "xmlhttprequest-ssl": "1.5.3", + "yeast": "0.1.2" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "engine.io-parser": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.3.2.tgz", + "integrity": "sha1-k3sHnwAH0Ik+xW1GyyILjLQ1Igo=", + "dev": true, + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "0.0.6", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.4", + "has-binary": "0.1.7", + "wtf-8": "1.0.0" + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "dev": true + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "2.7.3", + "estraverse": "1.9.3", + "esutils": "2.0.2", + "optionator": "0.8.2", + "source-map": "0.2.0" + }, + "dependencies": { + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": "1.0.1" + } + } + } }, "esprima": { "version": "4.0.0", @@ -296,15 +1354,314 @@ "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", "dev": true }, - "fs-extra": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", - "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "eventemitter3": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", + "integrity": "sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg=", + "dev": true + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "dev": true + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", "dev": true, "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "4.0.0", - "universalify": "0.1.1" + "md5.js": "1.3.4", + "safe-buffer": "5.1.1" + } + }, + "expand-braces": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/expand-braces/-/expand-braces-0.1.2.tgz", + "integrity": "sha1-SIsdHSRRyz06axks/AMPRMWFX+o=", + "dev": true, + "requires": { + "array-slice": "0.2.3", + "array-unique": "0.2.1", + "braces": "0.1.5" + }, + "dependencies": { + "braces": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz", + "integrity": "sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY=", + "dev": true, + "requires": { + "expand-range": "0.1.1" + } + }, + "expand-range": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz", + "integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=", + "dev": true, + "requires": { + "is-number": "0.1.1", + "repeat-string": "0.2.2" + } + }, + "is-number": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz", + "integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=", + "dev": true + }, + "repeat-string": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-0.2.2.tgz", + "integrity": "sha1-x6jTI2BoNiBZp+RlH8aITosftK4=", + "dev": true + } + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "2.2.3" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, + "extend-shallow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", + "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=", + "dev": true, + "requires": { + "kind-of": "1.1.0" + }, + "dependencies": { + "kind-of": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", + "dev": true + } + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fileset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", + "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", + "dev": true, + "requires": { + "glob": "7.1.2", + "minimatch": "3.0.4" + }, + "dependencies": { + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } + }, + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "dev": true, + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.7", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "finalhandler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", + "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "statuses": "1.3.1", + "unpipe": "1.0.0" + }, + "dependencies": { + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", + "dev": true + } + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, + "fs-access": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz", + "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=", + "dev": true, + "requires": { + "null-check": "1.0.0" } }, "fs.realpath": { @@ -312,6 +1669,916 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, + "fsevents": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", + "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", + "dev": true, + "optional": true, + "requires": { + "nan": "2.10.0", + "node-pre-gyp": "0.6.39" + }, + "dependencies": { + "abbrev": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "ajv": { + "version": "4.11.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.2.9" + } + }, + "asn1": { + "version": "0.2.3", + "bundled": true, + "dev": true, + "optional": true + }, + "assert-plus": { + "version": "0.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true, + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "aws4": { + "version": "1.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "balanced-match": { + "version": "0.4.2", + "bundled": true, + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "block-stream": { + "version": "0.0.9", + "bundled": true, + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "boom": { + "version": "2.10.1", + "bundled": true, + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.7", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "0.4.2", + "concat-map": "0.0.1" + } + }, + "buffer-shims": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "caseless": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true + }, + "co": { + "version": "4.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "combined-stream": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "cryptiles": { + "version": "2.0.5", + "bundled": true, + "dev": true, + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "debug": { + "version": "2.6.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "bundled": true, + "dev": true, + "optional": true + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "extsprintf": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true, + "dev": true, + "optional": true + }, + "form-data": { + "version": "2.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.15" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "fstream": { + "version": "1.0.11", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.1" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "1.1.1", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "getpass": { + "version": "0.1.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true, + "dev": true + }, + "har-schema": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "har-validator": { + "version": "4.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "hawk": { + "version": "3.1.3", + "bundled": true, + "dev": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "bundled": true, + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.0", + "sshpk": "1.13.0" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.4", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "jodid25519": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "jsbn": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true, + "dev": true, + "optional": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "jsonify": { + "version": "0.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "jsprim": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.0.2", + "json-schema": "0.2.3", + "verror": "1.3.6" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "mime-db": { + "version": "1.27.0", + "bundled": true, + "dev": true + }, + "mime-types": { + "version": "2.1.15", + "bundled": true, + "dev": true, + "requires": { + "mime-db": "1.27.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "node-pre-gyp": { + "version": "0.6.39", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "1.0.2", + "hawk": "3.1.3", + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.0", + "rc": "1.2.1", + "request": "2.81.0", + "rimraf": "2.6.1", + "semver": "5.3.0", + "tar": "2.2.1", + "tar-pack": "3.4.0" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1.1.0", + "osenv": "0.1.4" + } + }, + "npmlog": { + "version": "4.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "performance-now": { + "version": "0.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "1.0.7", + "bundled": true, + "dev": true + }, + "punycode": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true + }, + "qs": { + "version": "6.4.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.2.9", + "bundled": true, + "dev": true, + "requires": { + "buffer-shims": "1.0.0", + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "1.0.1", + "util-deprecate": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.15", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.0.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.2", + "tunnel-agent": "0.6.0", + "uuid": "3.0.1" + } + }, + "rimraf": { + "version": "2.6.1", + "bundled": true, + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.0.1", + "bundled": true, + "dev": true + }, + "semver": { + "version": "5.3.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sntp": { + "version": "1.0.9", + "bundled": true, + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "sshpk": { + "version": "1.13.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jodid25519": "1.0.2", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "stringstream": { + "version": "0.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "2.2.1", + "bundled": true, + "dev": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-pack": { + "version": "3.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "2.6.8", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.2.9", + "rimraf": "2.6.1", + "tar": "2.2.1", + "uid-number": "0.0.6" + } + }, + "tough-cookie": { + "version": "2.3.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "bundled": true, + "dev": true, + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "uuid": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "verror": { + "version": "1.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "extsprintf": "1.0.2" + } + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + } + } + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, "glob": { "version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", @@ -325,6 +2592,31 @@ "path-is-absolute": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" } }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "2.0.1" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", @@ -343,10 +2635,158 @@ "uglify-js": "2.8.29" } }, - "highlight.js": { - "version": "9.12.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz", - "integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4=", + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-binary": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.7.tgz", + "integrity": "sha1-aOYesWIQyVRaClzOBqhzkS/h5ow=", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", + "dev": true + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "hash.js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", + "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "1.1.3", + "minimalistic-assert": "1.0.1", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "hosted-git-info": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", + "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": "1.5.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "http-proxy": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.16.2.tgz", + "integrity": "sha1-Bt/ykpUr9k2+hHH6nfcwZtTzd0I=", + "dev": true, + "requires": { + "eventemitter3": "1.2.0", + "requires-port": "1.0.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", + "dev": true + }, + "ieee754": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.11.tgz", + "integrity": "sha512-VhDzCKN7K8ufStx/CLj5/PDTMgph+qwN5Pkd5i0sGnVwk56zJ0lkT8Qzi1xqWLS0Wp29DgDtNeS7v8/wMoZeHg==", + "dev": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", "dev": true }, "inflight": { @@ -363,18 +2803,466 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true }, - "interpret": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", - "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "inline-source-map": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", + "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", + "dev": true, + "requires": { + "source-map": "0.5.7" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "1.3.1" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "1.11.0" + } + }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isbinaryfile": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.2.tgz", + "integrity": "sha1-Sj6XTsDLqQBNP8bN5yCeppNopiE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "dev": true, + "requires": { + "abbrev": "1.0.9", + "async": "1.5.2", + "escodegen": "1.8.1", + "esprima": "2.7.3", + "glob": "5.0.15", + "handlebars": "4.0.11", + "js-yaml": "3.10.0", + "mkdirp": "0.5.1", + "nopt": "3.0.6", + "once": "1.4.0", + "resolve": "1.1.7", + "supports-color": "3.2.3", + "which": "1.3.0", + "wordwrap": "1.0.0" + }, + "dependencies": { + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } + }, + "istanbul-api": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.1.tgz", + "integrity": "sha512-duj6AlLcsWNwUpfyfHt0nWIeRiZpuShnP40YTxOGQgtaN8fd6JYSxsvxUphTDy8V5MfDXo4s/xVCIIvVCO808g==", + "dev": true, + "requires": { + "async": "2.6.0", + "compare-versions": "3.1.0", + "fileset": "2.0.3", + "istanbul-lib-coverage": "1.2.0", + "istanbul-lib-hook": "1.2.0", + "istanbul-lib-instrument": "1.10.1", + "istanbul-lib-report": "1.1.4", + "istanbul-lib-source-maps": "1.2.4", + "istanbul-reports": "1.3.0", + "js-yaml": "3.10.0", + "mkdirp": "0.5.1", + "once": "1.4.0" + }, + "dependencies": { + "async": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", + "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "dev": true, + "requires": { + "lodash": "4.17.4" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } + }, + "istanbul-lib-coverage": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz", + "integrity": "sha512-GvgM/uXRwm+gLlvkWHTjDAvwynZkL9ns15calTrmhGgowlwJBbWMYzWbKqE2DT6JDP1AFXKa+Zi0EkqNCUqY0A==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.0.tgz", + "integrity": "sha512-p3En6/oGkFQV55Up8ZPC2oLxvgSxD8CzA0yBrhRZSh3pfv3OFj9aSGVC0yoerAi/O4u7jUVnOGVX1eVFM+0tmQ==", + "dev": true, + "requires": { + "append-transform": "0.4.0" + } + }, + "istanbul-lib-instrument": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz", + "integrity": "sha512-1dYuzkOCbuR5GRJqySuZdsmsNKPL3PTuyPevQfoCXJePT9C8y1ga75neU+Tuy9+yS3G/dgx8wgOmp2KLpgdoeQ==", + "dev": true, + "requires": { + "babel-generator": "6.26.1", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "istanbul-lib-coverage": "1.2.0", + "semver": "5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.4.tgz", + "integrity": "sha512-Azqvq5tT0U09nrncK3q82e/Zjkxa4tkFZv7E6VcqP0QCPn6oNljDPfrZEC/umNXds2t7b8sRJfs6Kmpzt8m2kA==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "1.2.0", + "mkdirp": "0.5.1", + "path-parse": "1.0.5", + "supports-color": "3.2.3" + }, + "dependencies": { + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.4.tgz", + "integrity": "sha512-UzuK0g1wyQijiaYQxj/CdNycFhAd2TLtO2obKQMTZrZ1jzEMRY3rvpASEKkaxbRR6brvdovfA03znPa/pXcejg==", + "dev": true, + "requires": { + "debug": "3.1.0", + "istanbul-lib-coverage": "1.2.0", + "mkdirp": "0.5.1", + "rimraf": "2.6.2", + "source-map": "0.5.7" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.3.0.tgz", + "integrity": "sha512-y2Z2IMqE1gefWUaVjrBm0mSKvUkaBy9Vqz8iwr/r40Y9hBbIteH5wqHG/9DLTfJ9xUnUT2j7A3+VVJ6EaYBllA==", + "dev": true, + "requires": { + "handlebars": "4.0.11" + } + }, + "jasmine-core": { + "version": "2.99.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.99.1.tgz", + "integrity": "sha1-5kAN8ea1bhMLYcS80JPap/boyhU=", + "dev": true + }, + "jasmine-spec-reporter": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz", + "integrity": "sha512-FZBoZu7VE5nR7Nilzy+Np8KuVIOxF4oXDPDknehCYBDE080EnlPu0afdZNmpGDBRCUBv3mj5qgqCRmk6W/K8vg==", + "dev": true, + "requires": { + "colors": "1.1.2" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, "js-yaml": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", @@ -385,13 +3273,490 @@ "esprima": "4.0.0" } }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "karma": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/karma/-/karma-1.7.1.tgz", + "integrity": "sha512-k5pBjHDhmkdaUccnC7gE3mBzZjcxyxYsYVaqiL2G5AqlfLyBO5nw2VdNK+O16cveEPd/gIOWULH7gkiYYwVNHg==", "dev": true, "requires": { - "graceful-fs": "4.1.11" + "bluebird": "3.5.1", + "body-parser": "1.18.2", + "chokidar": "1.7.0", + "colors": "1.1.2", + "combine-lists": "1.0.1", + "connect": "3.6.6", + "core-js": "2.4.1", + "di": "0.0.1", + "dom-serialize": "2.2.1", + "expand-braces": "0.1.2", + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "http-proxy": "1.16.2", + "isbinaryfile": "3.0.2", + "lodash": "3.10.1", + "log4js": "0.6.38", + "mime": "1.6.0", + "minimatch": "3.0.4", + "optimist": "0.6.1", + "qjobs": "1.2.0", + "range-parser": "1.2.0", + "rimraf": "2.6.2", + "safe-buffer": "5.1.1", + "socket.io": "1.7.3", + "source-map": "0.5.7", + "tmp": "0.0.31", + "useragent": "2.3.0" + }, + "dependencies": { + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } + }, + "karma-chrome-launcher": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz", + "integrity": "sha512-uf/ZVpAabDBPvdPdveyk1EPgbnloPvFFGgmRhYLTDH7gEB4nZdSBk8yTU47w1g/drLSx5uMOkjKk7IWKfWg/+w==", + "dev": true, + "requires": { + "fs-access": "1.0.1", + "which": "1.3.0" + } + }, + "karma-cli": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/karma-cli/-/karma-cli-1.0.1.tgz", + "integrity": "sha1-rmw8WKMTodALRRZMRVubhs4X+WA=", + "dev": true, + "requires": { + "resolve": "1.7.1" + }, + "dependencies": { + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "resolve": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", + "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + } + } + }, + "karma-coverage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-1.1.1.tgz", + "integrity": "sha1-Wv+LOc9plNwi3kyENix2ABtjfPY=", + "dev": true, + "requires": { + "dateformat": "1.0.12", + "istanbul": "0.4.5", + "lodash": "3.10.1", + "minimatch": "3.0.4", + "source-map": "0.5.7" + }, + "dependencies": { + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "karma-coverage-istanbul-reporter": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-1.4.2.tgz", + "integrity": "sha512-sQHexslLF+QHzaKfK8+onTYMyvSwv+p5cDayVxhpEELGa3z0QuB+l0IMsicIkkBNMOJKQaqueiRoW7iuo7lsog==", + "dev": true, + "requires": { + "istanbul-api": "1.3.1", + "minimatch": "3.0.4" + }, + "dependencies": { + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + } + } + }, + "karma-jasmine": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.1.1.tgz", + "integrity": "sha1-b+hA51oRYAydkehLM8RY4cRqNSk=", + "dev": true + }, + "karma-jasmine-html-reporter": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-0.2.2.tgz", + "integrity": "sha1-SKjl7xiAdhfuK14zwRlMNbQ5Ukw=", + "dev": true, + "requires": { + "karma-jasmine": "1.1.1" + } + }, + "karma-typescript": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/karma-typescript/-/karma-typescript-3.0.12.tgz", + "integrity": "sha1-qiy90RFEKgnG28uq6vSEmWVMaRM=", + "dev": true, + "requires": { + "acorn": "4.0.13", + "assert": "1.4.1", + "async": "2.6.0", + "browser-resolve": "1.11.2", + "browserify-zlib": "0.2.0", + "buffer": "5.1.0", + "combine-source-map": "0.8.0", + "console-browserify": "1.1.0", + "constants-browserify": "1.0.0", + "convert-source-map": "1.5.1", + "crypto-browserify": "3.12.0", + "diff": "3.5.0", + "domain-browser": "1.2.0", + "events": "1.1.1", + "glob": "7.1.2", + "https-browserify": "1.0.0", + "istanbul": "0.4.5", + "json-stringify-safe": "5.0.1", + "karma-coverage": "1.1.1", + "lodash": "4.17.4", + "log4js": "1.1.1", + "minimatch": "3.0.4", + "os-browserify": "0.3.0", + "pad": "2.0.3", + "path-browserify": "0.0.0", + "process": "0.11.10", + "punycode": "1.4.1", + "querystring-es3": "0.2.1", + "readable-stream": "2.3.6", + "remap-istanbul": "0.10.1", + "source-map": "0.6.1", + "stream-browserify": "2.0.1", + "stream-http": "2.8.1", + "string_decoder": "1.1.1", + "timers-browserify": "2.0.8", + "tmp": "0.0.29", + "tty-browserify": "0.0.0", + "url": "0.11.0", + "util": "0.10.3", + "vm-browserify": "0.0.4" + }, + "dependencies": { + "async": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", + "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "dev": true, + "requires": { + "lodash": "4.17.4" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "log4js": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-1.1.1.tgz", + "integrity": "sha1-wh0px2BAieTyVYM+f5SzRh3h/0M=", + "dev": true, + "requires": { + "debug": "2.6.9", + "semver": "5.5.0", + "streamroller": "0.4.1" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "tmp": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz", + "integrity": "sha1-8lEl/w3Z2jzLDC3Tce4SiLuRKMA=", + "dev": true, + "requires": { + "os-tmpdir": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } } }, "kind-of": { @@ -410,28 +3775,241 @@ "dev": true, "optional": true }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2", + "type-check": "0.3.2" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, "lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", "dev": true }, + "lodash.memoize": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", + "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=", + "dev": true + }, + "log4js": { + "version": "0.6.38", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-0.6.38.tgz", + "integrity": "sha1-LElBFmldb7JUgJQ9P8hy5mKlIv0=", + "dev": true, + "requires": { + "readable-stream": "1.0.34", + "semver": "4.3.6" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", "dev": true }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "dev": true, + "requires": { + "js-tokens": "3.0.2" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "0.4.1", + "signal-exit": "3.0.2" + } + }, + "lru-cache": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.2.tgz", + "integrity": "sha512-wgeVXhrDwAWnIF/yZARsFnMBtdFXOg1b8RIrhilp+0iDYN4mdQcNZElDZ0e4B64BhaxeQ5zN7PMyvu7we1kPeQ==", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, "lunr": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.1.6.tgz", - "integrity": "sha512-ydJpB8CX8cZ/VE+KMaYaFcZ6+o2LruM6NG76VXdflYTgluvVemz1lW4anE+pyBbLvxJHZdvD1Jy/fOqdzAEJog==", + "integrity": "sha512-ydJpB8CX8cZ/VE+KMaYaFcZ6+o2LruM6NG76VXdflYTgluvVemz1lW4anE+pyBbLvxJHZdvD1Jy/fOqdzAEJog==" + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", "dev": true }, - "marked": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.9.tgz", - "integrity": "sha512-nW5u0dxpXxHfkHzzrveY45gCbi+R4PaO4WRZYqZNl+vB0hVGeqlFn0aOg1c8AKL63TrNFn9Bm2UP4AdiZ9TPLw==", + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "dev": true, + "requires": { + "hash-base": "3.0.4", + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "2.1.0", + "decamelize": "1.2.0", + "loud-rejection": "1.6.0", + "map-obj": "1.0.1", + "minimist": "1.2.0", + "normalize-package-data": "2.4.0", + "object-assign": "4.1.0", + "read-pkg-up": "1.0.1", + "redent": "1.0.0", + "trim-newlines": "1.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "requires": { + "mime-db": "1.33.0" + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", "dev": true }, "minimatch": { @@ -448,12 +4026,120 @@ "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", "dev": true }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "dev": true, + "optional": true + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", + "dev": true + }, "node-forge": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz", - "integrity": "sha1-naYR6giYL0uUIGs760zJZl8gwwA=", + "integrity": "sha1-naYR6giYL0uUIGs760zJZl8gwwA=" + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1.0.9" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "requires": { + "hosted-git-info": "2.6.0", + "is-builtin-module": "1.0.0", + "semver": "4.3.6", + "validate-npm-package-license": "3.0.3" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, + "null-check": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz", + "integrity": "sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=", "dev": true }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", + "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=", + "dev": true + }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", + "dev": true + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, "once": { "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", @@ -472,45 +4158,601 @@ "wordwrap": "0.0.3" } }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "0.1.3", + "fast-levenshtein": "2.0.6", + "levn": "0.3.0", + "prelude-ls": "1.1.2", + "type-check": "0.3.2", + "wordwrap": "1.0.0" + }, + "dependencies": { + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } + } + }, + "options": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", + "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=", + "dev": true + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "pad": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pad/-/pad-2.0.3.tgz", + "integrity": "sha512-YVlBmpDQilhUl69RY/0Ku9Il0sNPLgVOVePhCJUfN8qDZKrq1zIY+ZwtCLtaWFtJzuJswwAcZHxf4a5RliV2pQ==", + "dev": true, + "requires": { + "wcwidth": "1.0.1" + } + }, + "pako": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", + "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", + "dev": true + }, + "parse-asn1": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", + "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", + "dev": true, + "requires": { + "asn1.js": "4.10.1", + "browserify-aes": "1.2.0", + "create-hash": "1.2.0", + "evp_bytestokey": "1.0.3", + "pbkdf2": "3.0.14" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, + "parsejson": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz", + "integrity": "sha1-q343WfIJ7OmUN5c/fQ8fZK4OZKs=", + "dev": true, + "requires": { + "better-assert": "1.0.2" + } + }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "dev": true, + "requires": { + "better-assert": "1.0.2" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "dev": true, + "requires": { + "better-assert": "1.0.2" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", + "dev": true + }, + "path-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", + "dev": true + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, "path-is-absolute": { "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, - "path-parse": { - "version": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", - "dev": true - }, - "progress": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", - "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", - "dev": true - }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", "dev": true, "requires": { - "resolve": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz" + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" } }, + "pbkdf2": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.14.tgz", + "integrity": "sha512-gjsZW9O34fm0R7PaLHRJmLLVfSoesxztjPjE9o6R+qtVJij90ltg1joIovN9GKrRW3t1PzhDDG3UMEMFfZ+1wA==", + "dev": true, + "requires": { + "create-hash": "1.2.0", + "create-hmac": "1.1.7", + "ripemd160": "2.0.1", + "safe-buffer": "5.1.1", + "sha.js": "2.4.11" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "plugin-error": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", + "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=", + "dev": true, + "requires": { + "ansi-cyan": "0.1.1", + "ansi-red": "0.1.1", + "arr-diff": "1.1.0", + "arr-union": "2.1.0", + "extend-shallow": "1.1.4" + }, + "dependencies": { + "arr-diff": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", + "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0", + "array-slice": "0.2.3" + } + } + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "public-encrypt": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", + "integrity": "sha512-4kJ5Esocg8X3h8YgJsKAuoesBgB7mqH3eowiDzMUPKiRDDE7E/BqqZD1hnTByIaAFiwAw246YEltSq7tdrOH0Q==", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.2.0", + "parse-asn1": "5.1.1", + "randombytes": "2.0.6" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "dev": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "randombytes": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", + "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "2.0.6", + "safe-buffer": "5.1.1" + } + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", + "dev": true + }, + "raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "dev": true, + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + }, + "dependencies": { + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", + "dev": true + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "dev": true, + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": "1.5.0" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", + "dev": true + } + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "readable-stream": "2.3.6", + "set-immediate-shim": "1.0.1" + }, + "dependencies": { + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + } + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "2.1.0", + "strip-indent": "1.0.1" + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "0.1.3" + } + }, + "remap-istanbul": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/remap-istanbul/-/remap-istanbul-0.10.1.tgz", + "integrity": "sha512-gsNQXs5kJLhErICSyYhzVZ++C8LBW8dgwr874Y2QvzAUS75zBlD/juZgXs39nbYJ09fZDlX2AVLVJAY2jbFJoQ==", + "dev": true, + "requires": { + "amdefine": "1.0.1", + "istanbul": "0.4.5", + "minimatch": "3.0.4", + "plugin-error": "0.1.2", + "source-map": "0.6.1", + "through2": "2.0.1" + }, + "dependencies": { + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", "dev": true }, - "resolve": { - "version": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", - "integrity": "sha1-HwmsznlsmnYlefMbLBzEw83fnzY=", + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "dev": true, "requires": { - "path-parse": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz" + "is-finite": "1.0.2" } }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, "right-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", @@ -530,24 +4772,229 @@ "glob": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz" } }, + "ripemd160": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=", + "dev": true, + "requires": { + "hash-base": "2.0.2", + "inherits": "2.0.3" + }, + "dependencies": { + "hash-base": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", + "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, "rxjs": { "version": "5.5.6", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.6.tgz", "integrity": "sha512-v4Q5HDC0FHAQ7zcBX7T2IL6O5ltl1a2GX4ENjPXg6SjDY69Cmx9v4113C99a4wGF16ClPv5Z8mghuYorVkg/kg==", - "dev": true, "requires": { "symbol-observable": "1.0.1" } }, - "shelljs": { - "version": "0.7.8", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", - "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "dev": true + }, + "semver": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", + "dev": true + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dev": true, "requires": { - "glob": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "interpret": "1.1.0", - "rechoir": "0.6.2" + "inherits": "2.0.3", + "safe-buffer": "5.1.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "socket.io": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.7.3.tgz", + "integrity": "sha1-uK+cq6AJSeVo42nxMn6pvp6iRhs=", + "dev": true, + "requires": { + "debug": "2.3.3", + "engine.io": "1.8.3", + "has-binary": "0.1.7", + "object-assign": "4.1.0", + "socket.io-adapter": "0.5.0", + "socket.io-client": "1.7.3", + "socket.io-parser": "2.3.1" + }, + "dependencies": { + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "socket.io-adapter": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz", + "integrity": "sha1-y21LuL7IHhB4uZZ3+c7QBGBmu4s=", + "dev": true, + "requires": { + "debug": "2.3.3", + "socket.io-parser": "2.3.1" + }, + "dependencies": { + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "socket.io-client": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.7.3.tgz", + "integrity": "sha1-sw6GqhDV7zVGYBwJzeR2Xjgdo3c=", + "dev": true, + "requires": { + "backo2": "1.0.2", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "2.3.3", + "engine.io-client": "1.8.3", + "has-binary": "0.1.7", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseuri": "0.0.5", + "socket.io-parser": "2.3.1", + "to-array": "0.1.4" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "debug": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", + "dev": true, + "requires": { + "ms": "0.7.2" + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "socket.io-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.3.1.tgz", + "integrity": "sha1-3VMgJRA85Clpcya+/WQAX8/ltKA=", + "dev": true, + "requires": { + "component-emitter": "1.1.2", + "debug": "2.2.0", + "isarray": "0.0.1", + "json3": "3.3.2" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + } } }, "source-map": { @@ -559,23 +5006,282 @@ "amdefine": "1.0.1" } }, + "spdx-correct": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", + "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "dev": true, + "requires": { + "spdx-expression-parse": "3.0.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "2.1.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", + "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", + "dev": true + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "stream-browserify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.6" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "stream-http": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.1.tgz", + "integrity": "sha512-cQ0jo17BLca2r0GfRdZKYAGLU6JRoIWxqSOakUMuKOT6MOK7AAlE856L33QuDmAy/eeOrhLee3dZKX0Uadu93A==", + "dev": true, + "requires": { + "builtin-status-codes": "3.0.0", + "inherits": "2.0.3", + "readable-stream": "2.3.6", + "to-arraybuffer": "1.0.1", + "xtend": "4.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "streamroller": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-0.4.1.tgz", + "integrity": "sha1-1DW9WXQ3Or2b2QaDWVEwhRBswF8=", + "dev": true, + "requires": { + "date-format": "0.0.0", + "debug": "0.7.4", + "mkdirp": "0.5.1", + "readable-stream": "1.1.14" + }, + "dependencies": { + "debug": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", + "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=", + "dev": true + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "4.0.1" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, "symbol-observable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", - "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=", + "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" + }, + "through2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.1.tgz", + "integrity": "sha1-OE51MU1J8y3hLuu4E2uOtrXVnak=", + "dev": true, + "requires": { + "readable-stream": "2.0.6", + "xtend": "4.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "0.10.31", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "timers-browserify": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.8.tgz", + "integrity": "sha512-a+QofyerK/mw3zl0nOlU33TP0vnbaF8pDKevVI5hT5LMUFEUQPtbhSohuvRZWB8UW1bP9Bhfqqw6KA3gsbP4Uw==", + "dev": true, + "requires": { + "setimmediate": "1.0.5" + } + }, + "tmp": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz", + "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=", + "dev": true, + "requires": { + "os-tmpdir": "1.0.2" + } + }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", + "dev": true + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "dev": true }, "tslib": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", - "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==", - "dev": true + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==" }, "tslint": { "version": "5.9.1", @@ -873,44 +5579,30 @@ } } }, - "typedoc": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.9.0.tgz", - "integrity": "sha512-numP0CtcUK4I1Vssw6E1N/FjyJWpWqhLT4Zb7Gw3i7ca3ElnYh6z41Y/tcUhMsMYn6L8b67E/Fu4XYYKkNaLbA==", + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", "dev": true, "requires": { - "@types/fs-extra": "4.0.0", - "@types/handlebars": "4.0.31", - "@types/highlight.js": "9.1.8", - "@types/lodash": "4.14.74", - "@types/marked": "0.3.0", - "@types/minimatch": "2.0.29", - "@types/shelljs": "0.7.0", - "fs-extra": "4.0.3", - "handlebars": "4.0.11", - "highlight.js": "9.12.0", - "lodash": "4.17.4", - "marked": "0.3.9", - "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "progress": "2.0.0", - "shelljs": "0.7.8", - "typedoc-default-themes": "0.5.0", - "typescript": "2.4.1" - }, - "dependencies": { - "typescript": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.4.1.tgz", - "integrity": "sha1-w8yxbdqgsjFN4DHn5v7onlujRrw=", - "dev": true - } + "prelude-ls": "1.1.2" } }, - "typedoc-default-themes": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.5.0.tgz", - "integrity": "sha1-bcJDPnjti+qOiHo6zeLzF4W9Yic=", - "dev": true + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.18" + } }, "typescript": { "version": "2.7.1", @@ -946,12 +5638,118 @@ "dev": true, "optional": true }, - "universalify": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", - "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=", + "ultron": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", + "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=", "dev": true }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "useragent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", + "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==", + "dev": true, + "requires": { + "lru-cache": "4.1.2", + "tmp": "0.0.31" + } + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", + "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", + "dev": true, + "requires": { + "spdx-correct": "3.0.0", + "spdx-expression-parse": "3.0.0" + } + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "dev": true, + "requires": { + "indexof": "0.0.1" + } + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "dev": true + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, + "requires": { + "defaults": "1.0.3" + } + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, "window-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", @@ -970,6 +5768,40 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "ws": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.2.tgz", + "integrity": "sha1-iiRPoFJAHgjJiGz0SoUYnh/UBn8=", + "dev": true, + "requires": { + "options": "0.0.6", + "ultron": "1.0.2" + } + }, + "wtf-8": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wtf-8/-/wtf-8-1.0.0.tgz", + "integrity": "sha1-OS2LotDxw00e4tYw8V0O+2jhBIo=", + "dev": true + }, + "xmlhttprequest-ssl": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz", + "integrity": "sha1-GFqIjATspGw+QHDZn3tJ3jUomS0=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, "yargs": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", @@ -983,11 +5815,16 @@ "window-size": "0.1.0" } }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", + "dev": true + }, "zone.js": { "version": "0.8.19", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.19.tgz", - "integrity": "sha512-l9rofaOs6a4y1W8zt4pDmnCUCnYG377dG+5SZlXNWrTWYUuXFqcJZiOarhYiRVR0NI9sH/8ooPJiz4uprB/Mkg==", - "dev": true + "integrity": "sha512-l9rofaOs6a4y1W8zt4pDmnCUCnYG377dG+5SZlXNWrTWYUuXFqcJZiOarhYiRVR0NI9sH/8ooPJiz4uprB/Mkg==" } } } diff --git a/package.json b/package.json index 5d4f7e55dc..ee44acb61f 100644 --- a/package.json +++ b/package.json @@ -12,21 +12,36 @@ "url": "https://github.com/bitwarden/jslib" }, "license": "GPL-3.0", - "module": "src/index.ts", - "typings": "src/index.ts", - "files": [ - "src", - "dist" - ], "scripts": { "prebuild": "rimraf dist/**/*", - "build": "tsc && typedoc --out dist/docs --target es6 --theme minimal --mode file src", + "build": "tsc", "start": "tsc -watch", "lint": "tslint src/**/*.ts || true", "lint:fix": "tslint src/**/*.ts --fix", - "pub": "npm run build && npm publish --access public" + "test": "karma start --single-run", + "test:watch": "karma start" }, "devDependencies": { + "@types/jasmine": "^2.8.2", + "@types/lunr": "2.1.5", + "@types/node": "8.0.19", + "@types/node-forge": "0.7.1", + "@types/papaparse": "4.1.31", + "@types/webcrypto": "0.0.28", + "jasmine-core": "^2.8.0", + "jasmine-spec-reporter": "^4.2.1", + "karma": "^1.7.1", + "karma-chrome-launcher": "^2.2.0", + "karma-cli": "^1.0.1", + "karma-coverage-istanbul-reporter": "^1.3.0", + "karma-jasmine": "^1.1.0", + "karma-jasmine-html-reporter": "^0.2.2", + "karma-typescript": "^3.0.8", + "rimraf": "^2.6.2", + "tslint": "^5.8.0", + "typescript": "^2.7.1" + }, + "dependencies": { "@angular/animations": "5.2.0", "@angular/common": "5.2.0", "@angular/compiler": "5.2.0", @@ -37,21 +52,12 @@ "@angular/platform-browser-dynamic": "5.2.0", "@angular/router": "5.2.0", "@angular/upgrade": "5.2.0", - "@types/lunr": "2.1.5", - "@types/node": "8.0.19", - "@types/node-forge": "0.7.1", - "@types/papaparse": "4.1.31", - "@types/webcrypto": "0.0.28", "angular2-toaster": "4.0.2", "angulartics2": "5.0.1", "core-js": "2.4.1", "lunr": "2.1.6", "node-forge": "0.7.1", - "rimraf": "^2.6.2", "rxjs": "5.5.6", - "tslint": "^5.8.0", - "typedoc": "^0.9.0", - "typescript": "^2.7.1", "zone.js": "0.8.19" } } diff --git a/src/services/utils.service.spec.ts b/src/services/utils.service.spec.ts new file mode 100644 index 0000000000..26dfa8bd60 --- /dev/null +++ b/src/services/utils.service.spec.ts @@ -0,0 +1,34 @@ +import { UtilsService } from './utils.service'; + +describe('Utils Service', () => { + describe('getHostname', () => { + it('should fail for invalid urls', () => { + expect(UtilsService.getHostname(null)).toBeNull(); + expect(UtilsService.getHostname(undefined)).toBeNull(); + expect(UtilsService.getHostname(' ')).toBeNull(); + expect(UtilsService.getHostname('https://bit!:"_&ward.com')).toBeNull(); + expect(UtilsService.getHostname('bitwarden')).toBeNull(); + }); + + it('should handle valid urls', () => { + expect(UtilsService.getHostname('bitwarden.com')).toBe('bitwarden.com'); + expect(UtilsService.getHostname('https://bitwarden.com')).toBe('bitwarden.com'); + expect(UtilsService.getHostname('http://bitwarden.com')).toBe('bitwarden.com'); + expect(UtilsService.getHostname('http://vault.bitwarden.com')).toBe('vault.bitwarden.com'); + expect(UtilsService.getHostname('https://user:password@bitwarden.com:8080/password/sites?and&query#hash')) + .toBe('bitwarden.com'); + }); + + it('should support localhost and IP', () => { + expect(UtilsService.getHostname('https://localhost')).toBe('localhost'); + expect(UtilsService.getHostname('https://192.168.1.1')).toBe('192.168.1.1'); + }); + }); + + describe('newGuid', () => { + it('should create a valid guid', () => { + const validGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + expect(UtilsService.newGuid()).toMatch(validGuid); + }); + }); +}); From 291250653f6059da8ca4c05404b1fc08f8ec1042 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 17 Apr 2018 16:25:01 -0400 Subject: [PATCH 0171/1626] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8ed823597e..d376564923 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![appveyor build](https://ci.appveyor.com/api/projects/status/github/bitwarden/jslib?branch=master&svg=true)](https://ci.appveyor.com/project/bitwarden/jslib) + # Bitwarden JavaScript Library Common code referenced across Bitwarden JavaScript projects. From 9ac6f5db1fb6f3b5e6472e85e40a9f8d58621cbb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 17 Apr 2018 16:17:04 -0400 Subject: [PATCH 0172/1626] build:watch --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ee44acb61f..4f64f45cab 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "scripts": { "prebuild": "rimraf dist/**/*", "build": "tsc", - "start": "tsc -watch", + "build:watch": "tsc -watch", "lint": "tslint src/**/*.ts || true", "lint:fix": "tslint src/**/*.ts --fix", "test": "karma start --single-run", From 5ed0edf8674dd9b6391c0f8b880ebeef4f30ea82 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 17 Apr 2018 17:56:41 -0400 Subject: [PATCH 0173/1626] test web crypto pbkdf2 --- karma.conf.js | 3 +- .../webCryptoFunction.service.spec.ts | 95 +++++++++++++++++++ src/services/webCryptoFunction.service.ts | 4 +- 3 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 src/services/webCryptoFunction.service.spec.ts diff --git a/karma.conf.js b/karma.conf.js index 726902707d..d4793738c1 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -58,7 +58,8 @@ module.exports = function(config) { module: 'CommonJS' }, bundlerOptions: { - entrypoints: /\.spec\.ts$/ + entrypoints: /\.spec\.ts$/, + sourceMap: true } }, }) diff --git a/src/services/webCryptoFunction.service.spec.ts b/src/services/webCryptoFunction.service.spec.ts new file mode 100644 index 0000000000..3cf1facf71 --- /dev/null +++ b/src/services/webCryptoFunction.service.spec.ts @@ -0,0 +1,95 @@ +import { DeviceType } from '../enums/deviceType'; + +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; + +import { WebCryptoFunctionService } from './webCryptoFunction.service'; + +import { UtilsService } from './utils.service'; + +describe('WebCrypto Function Service', () => { + describe('pbkdf2', () => { + const regular256Key = 'pj9prw/OHPleXI6bRdmlaD+saJS4awrMiQsQiDjeu2I='; + const utf8256Key = 'yqvoFXgMRmHR3QPYr5pyR4uVuoHkltv9aHUP63p8n7I='; + const unicode256Key = 'ZdeOata6xoRpB4DLp8zHhXz5kLmkWtX5pd+TdRH8w8w='; + + const regular512Key = 'liTi/Ke8LPU1Qv+Vl7NGEVt/XMbsBVJ2kQxtVG/Z1/I='; + const utf8512Key = 'df0KdvIBeCzD/kyXptwQohaqUa4e7IyFUyhFQjXCANs='; + const unicode512Key = 'FE+AnUJaxv8jh+zUDtZz4mjjcYk0/PZDZm+SLJe3Xtw='; + + testPbkdf2ValidKey(false, 'sha256', regular256Key, utf8256Key, unicode256Key); + testPbkdf2ValidKey(false, 'sha512', regular512Key, utf8512Key, unicode512Key); + testPbkdf2ValidKey(true, 'sha256', regular256Key, utf8256Key, unicode256Key); + testPbkdf2ValidKey(true, 'sha512', regular512Key, utf8512Key, unicode512Key); + }); +}); + +function testPbkdf2ValidKey(edge: boolean, algorithm: 'sha256' | 'sha512', regularKey: string, + utf8Key: string, unicodeKey: string) { + const forEdge = edge ? ' for edge' : ''; + const regularEmail = 'user@example.com'; + const utf8Email = 'üser@example.com'; + + const regularPassword = 'password'; + const utf8Password = 'pǻssword'; + const unicodePassword = '😀password🙏'; + + it('should create valid ' + algorithm + ' key from regular input' + forEdge, async () => { + const webCryptoFunctionService = getWebCryptoFunctionService(edge); + const key = await webCryptoFunctionService.pbkdf2(regularPassword, regularEmail, algorithm, 5000, 256); + expect(UtilsService.fromBufferToB64(key)).toBe(regularKey); + }); + + it('should create valid ' + algorithm + ' key from utf8 input' + forEdge, async () => { + const webCryptoFunctionService = getWebCryptoFunctionService(edge); + const key = await webCryptoFunctionService.pbkdf2(utf8Password, utf8Email, algorithm, 5000, 256); + expect(UtilsService.fromBufferToB64(key)).toBe(utf8Key); + }); + + it('should create valid ' + algorithm + ' key from unicode input' + forEdge, async () => { + const webCryptoFunctionService = getWebCryptoFunctionService(edge); + const key = await webCryptoFunctionService.pbkdf2(UtilsService.fromUtf8ToArray(unicodePassword).buffer, + UtilsService.fromUtf8ToArray(regularEmail).buffer, algorithm, 5000, 256); + expect(UtilsService.fromBufferToB64(key)).toBe(unicodeKey); + }); + + it('should create valid ' + algorithm + ' key from array buffer input' + forEdge, async () => { + const webCryptoFunctionService = getWebCryptoFunctionService(edge); + const key = await webCryptoFunctionService.pbkdf2(UtilsService.fromUtf8ToArray(regularPassword).buffer, + UtilsService.fromUtf8ToArray(regularEmail).buffer, algorithm, 5000, 256); + expect(UtilsService.fromBufferToB64(key)).toBe(regularKey); + }); +} + +function getWebCryptoFunctionService(edge = false) { + const platformUtilsService = new BrowserPlatformUtilsService(edge); + return new WebCryptoFunctionService(window, platformUtilsService); +} + +class BrowserPlatformUtilsService implements PlatformUtilsService { + constructor(private edge: boolean) { } + + isEdge() { + return this.edge; + } + + identityClientId: string; + getDevice: () => DeviceType; + getDeviceString: () => string; + isFirefox: () => boolean; + isChrome: () => boolean; + isOpera: () => boolean; + isVivaldi: () => boolean; + isSafari: () => boolean; + isMacAppStore: () => boolean; + analyticsId: () => string; + getDomain: (uriString: string) => string; + isViewOpen: () => boolean; + launchUri: (uri: string, options?: any) => void; + saveFile: (win: Window, blobData: any, blobOptions: any, fileName: string) => void; + getApplicationVersion: () => string; + supportsU2f: (win: Window) => boolean; + showDialog: (text: string, title?: string, confirmText?: string, cancelText?: string, + type?: string) => Promise; + isDev: () => boolean; + copyToClipboard: (text: string, options?: any) => void; +} diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index 2eb4e68a03..a4aa901eb4 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -83,13 +83,13 @@ export class WebCryptoFunctionService implements CryptoFunctionService { bytes = forge.util.encodeUtf8(value); } else { const value64 = UtilsService.fromBufferToB64(value); - bytes = forge.util.encode64(value64); + bytes = forge.util.decode64(value64); } return bytes; } private fromForgeBytesToBuf(byteString: string): ArrayBuffer { - const b64 = forge.util.decode64(byteString); + const b64 = forge.util.encode64(byteString); return UtilsService.fromB64ToArray(b64).buffer; } } From da473ee1a6c7448f1a28d1ae59a04d542f4349cc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 17 Apr 2018 18:26:02 -0400 Subject: [PATCH 0174/1626] test web crypto hashing --- .../webCryptoFunction.service.spec.ts | 61 ++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/src/services/webCryptoFunction.service.spec.ts b/src/services/webCryptoFunction.service.spec.ts index 3cf1facf71..8d48e72aed 100644 --- a/src/services/webCryptoFunction.service.spec.ts +++ b/src/services/webCryptoFunction.service.spec.ts @@ -18,9 +18,35 @@ describe('WebCrypto Function Service', () => { testPbkdf2ValidKey(false, 'sha256', regular256Key, utf8256Key, unicode256Key); testPbkdf2ValidKey(false, 'sha512', regular512Key, utf8512Key, unicode512Key); + testPbkdf2ValidKey(true, 'sha256', regular256Key, utf8256Key, unicode256Key); testPbkdf2ValidKey(true, 'sha512', regular512Key, utf8512Key, unicode512Key); }); + + describe('hash', () => { + const regular1Hash = '2a241604fb921fad12bf877282457268e1dccb70'; + const utf81Hash = '85672798dc5831e96d6c48655d3d39365a9c88b6'; + const unicode1Hash = '39c975935054a3efc805a9709b60763a823a6ad4'; + + const regular256Hash = '2b8e96031d352a8655d733d7a930b5ffbea69dc25cf65c7bca7dd946278908b2'; + const utf8256Hash = '25fe8440f5b01ed113b0a0e38e721b126d2f3f77a67518c4a04fcde4e33eeb9d'; + const unicode256Hash = 'adc1c0c2afd6e92cefdf703f9b6eb2c38e0d6d1a040c83f8505c561fea58852e'; + + const regular512Hash = 'c15cf11d43bde333647e3f559ec4193bb2edeaa0e8b902772f514cdf3f785a3f49a6e02a4b87b3' + + 'b47523271ad45b7e0aebb5cdcc1bc54815d256eb5dcb80da9d'; + const utf8512Hash = '035c31a877a291af09ed2d3a1a293e69c3e079ea2cecc00211f35e6bce10474ca3ad6e30b59e26118' + + '37463f20969c5bc95282965a051a88f8cdf2e166549fcdd'; + const unicode512Hash = '2b16a5561af8ad6fe414cc103fc8036492e1fc6d9aabe1b655497054f760fe0e34c5d100ac773d' + + '9f3030438284f22dbfa20cb2e9b019f2c98dfe38ce1ef41bae'; + + testHash(false, 'sha1', regular1Hash, utf81Hash, unicode1Hash); + testHash(false, 'sha256', regular256Hash, utf8256Hash, unicode256Hash); + testHash(false, 'sha512', regular512Hash, utf8512Hash, unicode512Hash); + + testHash(true, 'sha1', regular1Hash, utf81Hash, unicode1Hash); + testHash(true, 'sha256', regular256Hash, utf8256Hash, unicode256Hash); + testHash(true, 'sha512', regular512Hash, utf8512Hash, unicode512Hash); + }); }); function testPbkdf2ValidKey(edge: boolean, algorithm: 'sha256' | 'sha512', regularKey: string, @@ -47,8 +73,7 @@ function testPbkdf2ValidKey(edge: boolean, algorithm: 'sha256' | 'sha512', regul it('should create valid ' + algorithm + ' key from unicode input' + forEdge, async () => { const webCryptoFunctionService = getWebCryptoFunctionService(edge); - const key = await webCryptoFunctionService.pbkdf2(UtilsService.fromUtf8ToArray(unicodePassword).buffer, - UtilsService.fromUtf8ToArray(regularEmail).buffer, algorithm, 5000, 256); + const key = await webCryptoFunctionService.pbkdf2(unicodePassword, regularEmail, algorithm, 5000, 256); expect(UtilsService.fromBufferToB64(key)).toBe(unicodeKey); }); @@ -60,6 +85,38 @@ function testPbkdf2ValidKey(edge: boolean, algorithm: 'sha256' | 'sha512', regul }); } +function testHash(edge: boolean, algorithm: 'sha1' | 'sha256' | 'sha512', regularHash: string, + utf8Hash: string, unicodeHash: string) { + const forEdge = edge ? ' for edge' : ''; + const regularValue = 'HashMe!!'; + const utf8Value = 'HǻshMe!!'; + const unicodeValue = '😀HashMe!!!🙏'; + + it('should create valid ' + algorithm + ' hash from regular input' + forEdge, async () => { + const webCryptoFunctionService = getWebCryptoFunctionService(edge); + const hash = await webCryptoFunctionService.hash(regularValue, algorithm); + expect(UtilsService.fromBufferToHex(hash)).toBe(regularHash); + }); + + it('should create valid ' + algorithm + ' hash from utf8 input' + forEdge, async () => { + const webCryptoFunctionService = getWebCryptoFunctionService(edge); + const hash = await webCryptoFunctionService.hash(utf8Value, algorithm); + expect(UtilsService.fromBufferToHex(hash)).toBe(utf8Hash); + }); + + it('should create valid ' + algorithm + ' hash from unicode input' + forEdge, async () => { + const webCryptoFunctionService = getWebCryptoFunctionService(edge); + const hash = await webCryptoFunctionService.hash(unicodeValue, algorithm); + expect(UtilsService.fromBufferToHex(hash)).toBe(unicodeHash); + }); + + it('should create valid ' + algorithm + ' hash from array buffer input' + forEdge, async () => { + const webCryptoFunctionService = getWebCryptoFunctionService(edge); + const hash = await webCryptoFunctionService.hash(UtilsService.fromUtf8ToArray(regularValue).buffer, algorithm); + expect(UtilsService.fromBufferToHex(hash)).toBe(regularHash); + }); +} + function getWebCryptoFunctionService(edge = false) { const platformUtilsService = new BrowserPlatformUtilsService(edge); return new WebCryptoFunctionService(window, platformUtilsService); From 719c8aa70cc6415ef57bd273e810337d13cc9bfc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 17 Apr 2018 18:32:08 -0400 Subject: [PATCH 0175/1626] this.isEdge member --- src/services/webCryptoFunction.service.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index a4aa901eb4..72bd719136 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -8,15 +8,17 @@ import { UtilsService } from '../services/utils.service'; export class WebCryptoFunctionService implements CryptoFunctionService { private crypto: Crypto; private subtle: SubtleCrypto; + private isEdge: boolean; constructor(private win: Window, private platformUtilsService: PlatformUtilsService) { this.crypto = win.crypto; this.subtle = win.crypto.subtle; + this.isEdge = platformUtilsService.isEdge(); } async pbkdf2(password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', iterations: number, length: number): Promise { - if (this.platformUtilsService.isEdge()) { + if (this.isEdge) { const passwordBytes = this.toForgeBytes(password); const saltBytes = this.toForgeBytes(salt); const derivedKeyBytes = (forge as any).pbkdf2(passwordBytes, saltBytes, iterations, length / 8, algorithm); @@ -46,7 +48,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { } async hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { - if (this.platformUtilsService.isEdge()) { + if (this.isEdge) { let md: forge.md.MessageDigest; if (algorithm === 'sha1') { md = forge.md.sha1.create(); From 81f7bd7b76061f1526a2922cfcf52fadbbef903a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 17 Apr 2018 19:02:58 -0400 Subject: [PATCH 0176/1626] hmac implementation for web crypto --- src/abstractions/cryptoFunction.service.ts | 1 + src/services/nodeCryptoFunction.service.ts | 4 +++ .../webCryptoFunction.service.spec.ts | 24 +++++++++++++++++ src/services/webCryptoFunction.service.ts | 27 +++++++++++++++++-- 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/abstractions/cryptoFunction.service.ts b/src/abstractions/cryptoFunction.service.ts index a992da8379..1012090bb0 100644 --- a/src/abstractions/cryptoFunction.service.ts +++ b/src/abstractions/cryptoFunction.service.ts @@ -2,4 +2,5 @@ export abstract class CryptoFunctionService { pbkdf2: (password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', iterations: number, length: number) => Promise; hash: (value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise; + hmac: (value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise; } diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index 0c0d4aaf00..d9a54774e7 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -25,6 +25,10 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { return hash.digest().buffer; } + async hmac(value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { + return new Uint8Array([]).buffer; + } + private toNodeValue(value: string | ArrayBuffer): string | Buffer { let nodeValue: string | Buffer; if (typeof (value) === 'string') { diff --git a/src/services/webCryptoFunction.service.spec.ts b/src/services/webCryptoFunction.service.spec.ts index 8d48e72aed..4da6ae5865 100644 --- a/src/services/webCryptoFunction.service.spec.ts +++ b/src/services/webCryptoFunction.service.spec.ts @@ -47,6 +47,21 @@ describe('WebCrypto Function Service', () => { testHash(true, 'sha256', regular256Hash, utf8256Hash, unicode256Hash); testHash(true, 'sha512', regular512Hash, utf8512Hash, unicode512Hash); }); + + describe('hmac', () => { + const sha1Mac = '4d4c223f95dc577b665ec4ccbcb680b80a397038'; + const sha256Mac = '6be3caa84922e12aaaaa2f16c40d44433bb081ef323db584eb616333ab4e874f'; + const sha512Mac = '21910e341fa12106ca35758a2285374509326c9fbe0bd64e7b99c898f841dc948c58ce66d3504d8883c' + + '5ea7817a0b7c5d4d9b00364ccd214669131fc17fe4aca'; + + testHmac(false, 'sha1', sha1Mac); + testHmac(false, 'sha256', sha256Mac); + testHmac(false, 'sha512', sha512Mac); + + testHmac(true, 'sha1', sha1Mac); + testHmac(true, 'sha256', sha256Mac); + testHmac(true, 'sha512', sha512Mac); + }); }); function testPbkdf2ValidKey(edge: boolean, algorithm: 'sha256' | 'sha512', regularKey: string, @@ -117,6 +132,15 @@ function testHash(edge: boolean, algorithm: 'sha1' | 'sha256' | 'sha512', regula }); } +function testHmac(edge: boolean, algorithm: 'sha1' | 'sha256' | 'sha512', mac: string) { + it('should create valid ' + algorithm + ' hmac' + (edge ? ' for edge' : ''), async () => { + const webCryptoFunctionService = getWebCryptoFunctionService(edge); + const computedMac = await webCryptoFunctionService.hmac(UtilsService.fromUtf8ToArray('SignMe!!').buffer, + UtilsService.fromUtf8ToArray('secretkey').buffer, algorithm); + expect(UtilsService.fromBufferToHex(computedMac)).toBe(mac); + }); +} + function getWebCryptoFunctionService(edge = false) { const platformUtilsService = new BrowserPlatformUtilsService(edge); return new WebCryptoFunctionService(window, platformUtilsService); diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index 72bd719136..639a191f6d 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -35,7 +35,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { name: 'PBKDF2', salt: saltBuf, iterations: iterations, - hash: { name: algorithm === 'sha256' ? 'SHA-256' : 'SHA-512' }, + hash: { name: this.toWebCryptoAlgorithm(algorithm) }, }; const keyType: AesDerivedKeyParams = { @@ -65,10 +65,29 @@ export class WebCryptoFunctionService implements CryptoFunctionService { const valueBuf = this.toBuf(value); return await this.subtle.digest({ - name: algorithm === 'sha1' ? 'SHA-1' : algorithm === 'sha256' ? 'SHA-256' : 'SHA-512' + name: this.toWebCryptoAlgorithm(algorithm) }, valueBuf); } + async hmac(value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { + if (this.isEdge) { + const valueBytes = this.toForgeBytes(value); + const keyBytes = this.toForgeBytes(key); + const hmac = (forge as any).hmac.create(); + hmac.start(algorithm, keyBytes); + hmac.update(valueBytes); + return this.fromForgeBytesToBuf(hmac.digest().getBytes()); + } + + const signingAlgorithm = { + name: 'HMAC', + hash: { name: this.toWebCryptoAlgorithm(algorithm) }, + }; + + const importedKey = await this.subtle.importKey('raw', key, signingAlgorithm, false, ['sign']); + return await this.subtle.sign(signingAlgorithm, importedKey, value); + } + private toBuf(value: string | ArrayBuffer): ArrayBuffer { let buf: ArrayBuffer; if (typeof (value) === 'string') { @@ -94,4 +113,8 @@ export class WebCryptoFunctionService implements CryptoFunctionService { const b64 = forge.util.encode64(byteString); return UtilsService.fromB64ToArray(b64).buffer; } + + private toWebCryptoAlgorithm(algorithm: 'sha1' | 'sha256' | 'sha512'): string { + return algorithm === 'sha1' ? 'SHA-1' : algorithm === 'sha256' ? 'SHA-256' : 'SHA-512'; + } } From 3ca8716fc627b00e911a240295c905e1ba066887 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 17 Apr 2018 21:18:47 -0400 Subject: [PATCH 0177/1626] determine length based on alg. fix 512 wc pbkdf2 --- src/abstractions/cryptoFunction.service.ts | 2 +- src/index.ts | 4 +- src/services/nodeCryptoFunction.service.ts | 3 +- .../webCryptoFunction.service.spec.ts | 39 ++++++++++--------- src/services/webCryptoFunction.service.ts | 26 +++++-------- 5 files changed, 35 insertions(+), 39 deletions(-) diff --git a/src/abstractions/cryptoFunction.service.ts b/src/abstractions/cryptoFunction.service.ts index 1012090bb0..cb755573ba 100644 --- a/src/abstractions/cryptoFunction.service.ts +++ b/src/abstractions/cryptoFunction.service.ts @@ -1,6 +1,6 @@ export abstract class CryptoFunctionService { pbkdf2: (password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', - iterations: number, length: number) => Promise; + iterations: number) => Promise; hash: (value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise; hmac: (value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise; } diff --git a/src/index.ts b/src/index.ts index 5d4c096cc3..36346148da 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,11 @@ import * as Abstractions from './abstractions'; import * as Enums from './enums'; +import * as Misc from './misc'; import * as Data from './models/data'; import * as Domain from './models/domain'; -import * as Misc from './misc'; import * as Request from './models/request'; import * as Response from './models/response'; -import * as Services from './services'; import * as View from './models/view'; +import * as Services from './services'; export { Abstractions, Enums, Data, Domain, Misc, Request, Response, Services, View }; diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index d9a54774e7..7c6e6e11f0 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -4,7 +4,8 @@ import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; export class NodeCryptoFunctionService implements CryptoFunctionService { async pbkdf2(password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', - iterations: number, length: number): Promise { + iterations: number): Promise { + const len = algorithm === 'sha256' ? 256 : 512; const nodePassword = this.toNodeValue(password); const nodeSalt = this.toNodeValue(salt); return new Promise((resolve, reject) => { diff --git a/src/services/webCryptoFunction.service.spec.ts b/src/services/webCryptoFunction.service.spec.ts index 4da6ae5865..498aa17b0f 100644 --- a/src/services/webCryptoFunction.service.spec.ts +++ b/src/services/webCryptoFunction.service.spec.ts @@ -12,15 +12,18 @@ describe('WebCrypto Function Service', () => { const utf8256Key = 'yqvoFXgMRmHR3QPYr5pyR4uVuoHkltv9aHUP63p8n7I='; const unicode256Key = 'ZdeOata6xoRpB4DLp8zHhXz5kLmkWtX5pd+TdRH8w8w='; - const regular512Key = 'liTi/Ke8LPU1Qv+Vl7NGEVt/XMbsBVJ2kQxtVG/Z1/I='; - const utf8512Key = 'df0KdvIBeCzD/kyXptwQohaqUa4e7IyFUyhFQjXCANs='; - const unicode512Key = 'FE+AnUJaxv8jh+zUDtZz4mjjcYk0/PZDZm+SLJe3Xtw='; + const regular512Key = 'liTi/Ke8LPU1Qv+Vl7NGEVt/XMbsBVJ2kQxtVG/Z1/JFHFKQW3ZkI81qVlwTiCpb+cFXzs+57' + + 'eyhhx5wfKo5Cg=='; + const utf8512Key = 'df0KdvIBeCzD/kyXptwQohaqUa4e7IyFUyhFQjXCANu5T+scq55hCcE4dG4T/MhAk2exw8j7ixRN' + + 'zXANiVZpnw=='; + const unicode512Key = 'FE+AnUJaxv8jh+zUDtZz4mjjcYk0/PZDZm+SLJe3XtxtnpdqqpblX6JjuMZt/dYYNMOrb2+mD' + + 'L3FiQDTROh1lg=='; - testPbkdf2ValidKey(false, 'sha256', regular256Key, utf8256Key, unicode256Key); - testPbkdf2ValidKey(false, 'sha512', regular512Key, utf8512Key, unicode512Key); + testPbkdf2(false, 'sha256', regular256Key, utf8256Key, unicode256Key); + testPbkdf2(false, 'sha512', regular512Key, utf8512Key, unicode512Key); - testPbkdf2ValidKey(true, 'sha256', regular256Key, utf8256Key, unicode256Key); - testPbkdf2ValidKey(true, 'sha512', regular512Key, utf8512Key, unicode512Key); + testPbkdf2(true, 'sha256', regular256Key, utf8256Key, unicode256Key); + testPbkdf2(true, 'sha512', regular512Key, utf8512Key, unicode512Key); }); describe('hash', () => { @@ -64,7 +67,7 @@ describe('WebCrypto Function Service', () => { }); }); -function testPbkdf2ValidKey(edge: boolean, algorithm: 'sha256' | 'sha512', regularKey: string, +function testPbkdf2(edge: boolean, algorithm: 'sha256' | 'sha512', regularKey: string, utf8Key: string, unicodeKey: string) { const forEdge = edge ? ' for edge' : ''; const regularEmail = 'user@example.com'; @@ -76,26 +79,26 @@ function testPbkdf2ValidKey(edge: boolean, algorithm: 'sha256' | 'sha512', regul it('should create valid ' + algorithm + ' key from regular input' + forEdge, async () => { const webCryptoFunctionService = getWebCryptoFunctionService(edge); - const key = await webCryptoFunctionService.pbkdf2(regularPassword, regularEmail, algorithm, 5000, 256); + const key = await webCryptoFunctionService.pbkdf2(regularPassword, regularEmail, algorithm, 5000); expect(UtilsService.fromBufferToB64(key)).toBe(regularKey); }); it('should create valid ' + algorithm + ' key from utf8 input' + forEdge, async () => { const webCryptoFunctionService = getWebCryptoFunctionService(edge); - const key = await webCryptoFunctionService.pbkdf2(utf8Password, utf8Email, algorithm, 5000, 256); + const key = await webCryptoFunctionService.pbkdf2(utf8Password, utf8Email, algorithm, 5000); expect(UtilsService.fromBufferToB64(key)).toBe(utf8Key); }); it('should create valid ' + algorithm + ' key from unicode input' + forEdge, async () => { const webCryptoFunctionService = getWebCryptoFunctionService(edge); - const key = await webCryptoFunctionService.pbkdf2(unicodePassword, regularEmail, algorithm, 5000, 256); + const key = await webCryptoFunctionService.pbkdf2(unicodePassword, regularEmail, algorithm, 5000); expect(UtilsService.fromBufferToB64(key)).toBe(unicodeKey); }); it('should create valid ' + algorithm + ' key from array buffer input' + forEdge, async () => { const webCryptoFunctionService = getWebCryptoFunctionService(edge); const key = await webCryptoFunctionService.pbkdf2(UtilsService.fromUtf8ToArray(regularPassword).buffer, - UtilsService.fromUtf8ToArray(regularEmail).buffer, algorithm, 5000, 256); + UtilsService.fromUtf8ToArray(regularEmail).buffer, algorithm, 5000); expect(UtilsService.fromBufferToB64(key)).toBe(regularKey); }); } @@ -147,12 +150,6 @@ function getWebCryptoFunctionService(edge = false) { } class BrowserPlatformUtilsService implements PlatformUtilsService { - constructor(private edge: boolean) { } - - isEdge() { - return this.edge; - } - identityClientId: string; getDevice: () => DeviceType; getDeviceString: () => string; @@ -173,4 +170,10 @@ class BrowserPlatformUtilsService implements PlatformUtilsService { type?: string) => Promise; isDev: () => boolean; copyToClipboard: (text: string, options?: any) => void; + + constructor(private edge: boolean) { } + + isEdge() { + return this.edge; + } } diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index 639a191f6d..41b2447986 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -17,20 +17,19 @@ export class WebCryptoFunctionService implements CryptoFunctionService { } async pbkdf2(password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', - iterations: number, length: number): Promise { + iterations: number): Promise { if (this.isEdge) { + const len = algorithm === 'sha256' ? 32 : 64; const passwordBytes = this.toForgeBytes(password); const saltBytes = this.toForgeBytes(salt); - const derivedKeyBytes = (forge as any).pbkdf2(passwordBytes, saltBytes, iterations, length / 8, algorithm); + const derivedKeyBytes = (forge as any).pbkdf2(passwordBytes, saltBytes, iterations, len, algorithm); return this.fromForgeBytesToBuf(derivedKeyBytes); } + const len = algorithm === 'sha256' ? 256 : 512; const passwordBuf = this.toBuf(password); const saltBuf = this.toBuf(salt); - const importedKey = await this.subtle.importKey('raw', passwordBuf, { name: 'PBKDF2' }, - false, ['deriveKey', 'deriveBits']); - const alg: Pbkdf2Params = { name: 'PBKDF2', salt: saltBuf, @@ -38,13 +37,8 @@ export class WebCryptoFunctionService implements CryptoFunctionService { hash: { name: this.toWebCryptoAlgorithm(algorithm) }, }; - const keyType: AesDerivedKeyParams = { - name: 'AES-CBC', - length: length, - }; - - const derivedKey = await this.subtle.deriveKey(alg, importedKey, keyType, true, ['encrypt', 'decrypt']); - return await this.subtle.exportKey('raw', derivedKey); + const impKey = await this.subtle.importKey('raw', passwordBuf, { name: 'PBKDF2' }, false, ['deriveBits']); + return await window.crypto.subtle.deriveBits(alg, impKey, len); } async hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { @@ -64,9 +58,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { } const valueBuf = this.toBuf(value); - return await this.subtle.digest({ - name: this.toWebCryptoAlgorithm(algorithm) - }, valueBuf); + return await this.subtle.digest({ name: this.toWebCryptoAlgorithm(algorithm) }, valueBuf); } async hmac(value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { @@ -84,8 +76,8 @@ export class WebCryptoFunctionService implements CryptoFunctionService { hash: { name: this.toWebCryptoAlgorithm(algorithm) }, }; - const importedKey = await this.subtle.importKey('raw', key, signingAlgorithm, false, ['sign']); - return await this.subtle.sign(signingAlgorithm, importedKey, value); + const impKey = await this.subtle.importKey('raw', key, signingAlgorithm, false, ['sign']); + return await this.subtle.sign(signingAlgorithm, impKey, value); } private toBuf(value: string | ArrayBuffer): ArrayBuffer { From 2e583147594610c60a06e75e8e6498bfe60390f2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 17 Apr 2018 22:35:53 -0400 Subject: [PATCH 0178/1626] byteString conversion without b64 --- src/services/webCryptoFunction.service.ts | 30 ++++++++++++----------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index 41b2447986..4278e0647a 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -20,10 +20,10 @@ export class WebCryptoFunctionService implements CryptoFunctionService { iterations: number): Promise { if (this.isEdge) { const len = algorithm === 'sha256' ? 32 : 64; - const passwordBytes = this.toForgeBytes(password); - const saltBytes = this.toForgeBytes(salt); + const passwordBytes = this.toByteString(password); + const saltBytes = this.toByteString(salt); const derivedKeyBytes = (forge as any).pbkdf2(passwordBytes, saltBytes, iterations, len, algorithm); - return this.fromForgeBytesToBuf(derivedKeyBytes); + return this.fromByteStringToBuf(derivedKeyBytes); } const len = algorithm === 'sha256' ? 256 : 512; @@ -52,9 +52,9 @@ export class WebCryptoFunctionService implements CryptoFunctionService { md = (forge as any).md.sha512.create(); } - const valueBytes = this.toForgeBytes(value); + const valueBytes = this.toByteString(value); md.update(valueBytes, 'raw'); - return this.fromForgeBytesToBuf(md.digest().data); + return this.fromByteStringToBuf(md.digest().data); } const valueBuf = this.toBuf(value); @@ -63,12 +63,12 @@ export class WebCryptoFunctionService implements CryptoFunctionService { async hmac(value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { if (this.isEdge) { - const valueBytes = this.toForgeBytes(value); - const keyBytes = this.toForgeBytes(key); + const valueBytes = this.toByteString(value); + const keyBytes = this.toByteString(key); const hmac = (forge as any).hmac.create(); hmac.start(algorithm, keyBytes); hmac.update(valueBytes); - return this.fromForgeBytesToBuf(hmac.digest().getBytes()); + return this.fromByteStringToBuf(hmac.digest().getBytes()); } const signingAlgorithm = { @@ -90,20 +90,22 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return buf; } - private toForgeBytes(value: string | ArrayBuffer): string { + private toByteString(value: string | ArrayBuffer): string { let bytes: string; if (typeof (value) === 'string') { bytes = forge.util.encodeUtf8(value); } else { - const value64 = UtilsService.fromBufferToB64(value); - bytes = forge.util.decode64(value64); + bytes = String.fromCharCode.apply(null, new Uint8Array(value)); } return bytes; } - private fromForgeBytesToBuf(byteString: string): ArrayBuffer { - const b64 = forge.util.encode64(byteString); - return UtilsService.fromB64ToArray(b64).buffer; + private fromByteStringToBuf(byteString: string): ArrayBuffer { + const arr = new Uint8Array(byteString.length); + for (let i = 0; i < byteString.length; i++) { + arr[i] = byteString.charCodeAt(i); + } + return arr.buffer; } private toWebCryptoAlgorithm(algorithm: 'sha1' | 'sha256' | 'sha512'): string { From 680fbff3069019fb55d93417b14ad569fcfedbea Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 18 Apr 2018 00:24:17 -0400 Subject: [PATCH 0179/1626] web crypto aes encryption and random bytes --- .../webCryptoFunction.service.spec.ts | 56 +++++++++++++++++++ src/services/webCryptoFunction.service.ts | 28 ++++++++++ 2 files changed, 84 insertions(+) diff --git a/src/services/webCryptoFunction.service.spec.ts b/src/services/webCryptoFunction.service.spec.ts index 498aa17b0f..c5c9f70e91 100644 --- a/src/services/webCryptoFunction.service.spec.ts +++ b/src/services/webCryptoFunction.service.spec.ts @@ -65,6 +65,54 @@ describe('WebCrypto Function Service', () => { testHmac(true, 'sha256', sha256Mac); testHmac(true, 'sha512', sha512Mac); }); + + describe('aesEncrypt', () => { + it('should successfully aes 256 encrypt data', async () => { + const webCryptoFunctionService = getWebCryptoFunctionService(); + const iv = makeStaticByteArray(16); + const key = makeStaticByteArray(32); + const data = UtilsService.fromUtf8ToArray('EncryptMe!'); + const encValue = await webCryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); + expect(UtilsService.fromBufferToB64(encValue)).toBe('ByUF8vhyX4ddU9gcooznwA=='); + }); + }); + + describe('aesDecryptSmall', () => { + it('should successfully aes 256 decrypt data', async () => { + const webCryptoFunctionService = getWebCryptoFunctionService(); + const iv = makeStaticByteArray(16); + const key = makeStaticByteArray(32); + const data = UtilsService.fromB64ToArray('ByUF8vhyX4ddU9gcooznwA=='); + const decValue = await webCryptoFunctionService.aesDecryptSmall(data.buffer, iv.buffer, key.buffer); + expect(UtilsService.fromBufferToUtf8(decValue)).toBe('EncryptMe!'); + }); + }); + + describe('aesDecryptLarge', () => { + it('should successfully aes 256 decrypt data', async () => { + const webCryptoFunctionService = getWebCryptoFunctionService(); + const iv = makeStaticByteArray(16); + const key = makeStaticByteArray(32); + const data = UtilsService.fromB64ToArray('ByUF8vhyX4ddU9gcooznwA=='); + const decValue = await webCryptoFunctionService.aesDecryptLarge(data.buffer, iv.buffer, key.buffer); + expect(UtilsService.fromBufferToUtf8(decValue)).toBe('EncryptMe!'); + }); + }); + + describe('randomBytes', () => { + it('should make a value of the correct length', async () => { + const webCryptoFunctionService = getWebCryptoFunctionService(); + const randomData = await webCryptoFunctionService.randomBytes(16); + expect(randomData.byteLength).toBe(16); + }); + + it('should not make the same value twice', async () => { + const webCryptoFunctionService = getWebCryptoFunctionService(); + const randomData = await webCryptoFunctionService.randomBytes(16); + const randomData2 = await webCryptoFunctionService.randomBytes(16); + expect(randomData.byteLength === randomData2.byteLength && randomData !== randomData2).toBeTruthy(); + }); + }); }); function testPbkdf2(edge: boolean, algorithm: 'sha256' | 'sha512', regularKey: string, @@ -149,6 +197,14 @@ function getWebCryptoFunctionService(edge = false) { return new WebCryptoFunctionService(window, platformUtilsService); } +function makeStaticByteArray(length: number) { + const arr = new Uint8Array(length); + for (let i = 0; i < length; i++) { + arr[i] = i; + } + return arr; +} + class BrowserPlatformUtilsService implements PlatformUtilsService { identityClientId: string; getDevice: () => DeviceType; diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index 4278e0647a..dbba69ad1f 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -80,6 +80,34 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return await this.subtle.sign(signingAlgorithm, impKey, value); } + async aesEncrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { + const impKey = await this.subtle.importKey('raw', key, { name: 'AES-CBC' }, false, ['encrypt']); + return await this.subtle.encrypt({ name: 'AES-CBC', iv: iv }, impKey, data); + } + + async aesDecryptSmall(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { + const dataBytes = this.toByteString(data); + const ivBytes = this.toByteString(iv); + const keyBytes = this.toByteString(key); + const dataBuffer = (forge as any).util.createBuffer(dataBytes); + const decipher = (forge as any).cipher.createDecipher('AES-CBC', keyBytes); + decipher.start({ iv: ivBytes }); + decipher.update(dataBuffer); + decipher.finish(); + return this.fromByteStringToBuf(decipher.output.getBytes()); + } + + async aesDecryptLarge(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { + const impKey = await this.subtle.importKey('raw', key, { name: 'AES-CBC' }, false, ['decrypt']); + return await this.subtle.decrypt({ name: 'AES-CBC', iv: iv }, impKey, data); + } + + randomBytes(length: number): ArrayBuffer { + const arr = new Uint8Array(length); + this.crypto.getRandomValues(arr); + return arr.buffer; + } + private toBuf(value: string | ArrayBuffer): ArrayBuffer { let buf: ArrayBuffer; if (typeof (value) === 'string') { From 866de1278f8600e01080790d2e720f3ea862304e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 18 Apr 2018 00:25:16 -0400 Subject: [PATCH 0180/1626] remove 256 from expectation --- src/services/webCryptoFunction.service.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/webCryptoFunction.service.spec.ts b/src/services/webCryptoFunction.service.spec.ts index c5c9f70e91..68a8a56090 100644 --- a/src/services/webCryptoFunction.service.spec.ts +++ b/src/services/webCryptoFunction.service.spec.ts @@ -67,7 +67,7 @@ describe('WebCrypto Function Service', () => { }); describe('aesEncrypt', () => { - it('should successfully aes 256 encrypt data', async () => { + it('should successfully aes encrypt data', async () => { const webCryptoFunctionService = getWebCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); @@ -78,7 +78,7 @@ describe('WebCrypto Function Service', () => { }); describe('aesDecryptSmall', () => { - it('should successfully aes 256 decrypt data', async () => { + it('should successfully aes decrypt data', async () => { const webCryptoFunctionService = getWebCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); @@ -89,7 +89,7 @@ describe('WebCrypto Function Service', () => { }); describe('aesDecryptLarge', () => { - it('should successfully aes 256 decrypt data', async () => { + it('should successfully aes decrypt data', async () => { const webCryptoFunctionService = getWebCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); From 172f2de4ffdeb524a58160e312509bbc029681aa Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 18 Apr 2018 08:50:02 -0400 Subject: [PATCH 0181/1626] node crypto aes implementations --- src/services/nodeCryptoFunction.service.ts | 54 +++++++++++++++++++--- src/services/webCryptoFunction.service.ts | 4 +- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index 7c6e6e11f0..6f5bb9499d 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -3,7 +3,7 @@ import * as crypto from 'crypto'; import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; export class NodeCryptoFunctionService implements CryptoFunctionService { - async pbkdf2(password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', + pbkdf2(password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', iterations: number): Promise { const len = algorithm === 'sha256' ? 256 : 512; const nodePassword = this.toNodeValue(password); @@ -19,15 +19,53 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { }); } - async hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { + hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { const nodeValue = this.toNodeValue(value); const hash = crypto.createHash(algorithm); hash.update(nodeValue); - return hash.digest().buffer; + return Promise.resolve(hash.digest().buffer); } - async hmac(value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { - return new Uint8Array([]).buffer; + hmac(value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { + const nodeValue = this.toNodeBuffer(value); + const nodeKey = this.toNodeBuffer(value); + const hmac = crypto.createHmac(algorithm, nodeKey); + hmac.update(nodeValue); + return Promise.resolve(hmac.digest().buffer); + } + + aesEncrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { + const nodeData = this.toNodeBuffer(data); + const nodeIv = this.toNodeBuffer(iv); + const nodeKey = this.toNodeBuffer(key); + const cipher = crypto.createCipheriv('aes-256-cbc', nodeKey, nodeIv); + const encBuf = Buffer.concat([cipher.update(nodeData), cipher.final()]); + return Promise.resolve(encBuf.buffer); + } + + aesDecryptSmall(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { + return this.aesDecryptLarge(data, iv, key); + } + + aesDecryptLarge(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { + const nodeData = this.toNodeBuffer(data); + const nodeIv = this.toNodeBuffer(iv); + const nodeKey = this.toNodeBuffer(key); + const decipher = crypto.createDecipheriv('aes-256-cbc', nodeKey, nodeIv); + const decBuf = Buffer.concat([decipher.update(nodeData), decipher.final()]); + return Promise.resolve(decBuf.buffer); + } + + randomBytes(length: number): Promise { + return new Promise((resolve, reject) => { + crypto.randomBytes(length, (error, bytes) => { + if (error != null) { + reject(error); + } else { + resolve(bytes.buffer); + } + }); + }); } private toNodeValue(value: string | ArrayBuffer): string | Buffer { @@ -35,8 +73,12 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { if (typeof (value) === 'string') { nodeValue = value; } else { - nodeValue = Buffer.from(new Uint8Array(value) as any); + nodeValue = this.toNodeBuffer(value); } return nodeValue; } + + private toNodeBuffer(value: ArrayBuffer): Buffer { + return Buffer.from(new Uint8Array(value) as any);; + } } diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index dbba69ad1f..04de7af331 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -102,10 +102,10 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return await this.subtle.decrypt({ name: 'AES-CBC', iv: iv }, impKey, data); } - randomBytes(length: number): ArrayBuffer { + randomBytes(length: number): Promise { const arr = new Uint8Array(length); this.crypto.getRandomValues(arr); - return arr.buffer; + return Promise.resolve(arr.buffer); } private toBuf(value: string | ArrayBuffer): ArrayBuffer { From 9b4069cb19a803f02c2787d2611c944049c0c610 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 18 Apr 2018 12:59:48 -0400 Subject: [PATCH 0182/1626] node vs web testing with jasmine --- package-lock.json | 2346 +++++++++++++++++ package.json | 9 +- spec/helpers.ts | 3 + .../nodeCryptoFunction.service.spec.ts | 62 + spec/support/jasmine.json | 12 + karma.conf.js => spec/support/karma.conf.js | 7 +- .../web}/services/utils.service.spec.ts | 2 +- .../webCryptoFunction.service.spec.ts | 8 +- tsconfig.json | 9 +- 9 files changed, 2443 insertions(+), 15 deletions(-) create mode 100644 spec/helpers.ts create mode 100644 spec/node/services/nodeCryptoFunction.service.spec.ts create mode 100644 spec/support/jasmine.json rename karma.conf.js => spec/support/karma.conf.js (95%) rename {src => spec/web}/services/utils.service.spec.ts (95%) rename {src => spec/web}/services/webCryptoFunction.service.spec.ts (97%) diff --git a/package-lock.json b/package-lock.json index 5f37db06ba..bbe75e6d22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -178,6 +178,15 @@ "tslib": "1.9.0" } }, + "ansi-align": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", + "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "dev": true, + "requires": { + "string-width": "2.1.1" + } + }, "ansi-cyan": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", @@ -315,6 +324,12 @@ "util": "0.10.3" } }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, "async": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", @@ -327,6 +342,12 @@ "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", "dev": true }, + "atob": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.0.tgz", + "integrity": "sha512-SuiKH8vbsOyCALjA/+EINmt/Kdl+TQPrtFgW7XZZcwtryFu9e5kQoX3bjCW6mIvGH1fbeAZZuvwGR5IlBRznGw==", + "dev": true + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -440,6 +461,79 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "1.0.1", + "class-utils": "0.3.6", + "component-emitter": "1.2.1", + "define-property": "1.0.0", + "isobject": "3.0.1", + "mixin-deep": "1.3.1", + "pascalcase": "0.1.1" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, "base64-arraybuffer": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", @@ -509,6 +603,64 @@ "type-is": "1.6.16" } }, + "boxen": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", + "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", + "dev": true, + "requires": { + "ansi-align": "2.0.0", + "camelcase": "4.1.0", + "chalk": "2.4.0", + "cli-boxes": "1.0.0", + "string-width": "2.1.1", + "term-size": "1.2.0", + "widest-line": "2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "chalk": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.0.tgz", + "integrity": "sha512-Wr/w0f4o9LuE7K53cD0qmbAMM+2XNLzR29vFn5hqko4sxGlUsyy363NvmyGIyk5tpe9cjTr9SJYbysEyPkRnFw==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, "brace-expansion": { "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", @@ -680,6 +832,37 @@ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", "dev": true }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "1.0.0", + "component-emitter": "1.2.1", + "get-value": "2.0.6", + "has-value": "1.0.0", + "isobject": "3.0.1", + "set-value": "2.0.0", + "to-object-path": "0.3.0", + "union-value": "1.0.0", + "unset-value": "1.0.0" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, "callsite": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", @@ -711,6 +894,12 @@ } } }, + "capture-stack-trace": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", + "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", + "dev": true + }, "center-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", @@ -766,6 +955,12 @@ } } }, + "ci-info": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.3.tgz", + "integrity": "sha512-SK/846h/Rcy8q9Z9CAwGBLfCJ6EkjJWdpelWDufQpqVDYq2Wnnv8zlSO6AMQap02jvhVruKKpEtQOufo3pFhLg==", + "dev": true + }, "cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", @@ -784,6 +979,47 @@ } } }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "3.1.0", + "define-property": "0.2.5", + "isobject": "3.0.1", + "static-extend": "0.1.2" + }, + "dependencies": { + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "cli-boxes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", + "dev": true + }, "cliui": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", @@ -811,6 +1047,31 @@ "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", "dev": true }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "1.0.0", + "object-visit": "1.0.1" + } + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, "colors": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", @@ -881,6 +1142,20 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "configstore": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", + "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", + "dev": true, + "requires": { + "dot-prop": "4.2.0", + "graceful-fs": "4.1.11", + "make-dir": "1.2.0", + "unique-string": "1.0.0", + "write-file-atomic": "2.3.0", + "xdg-basedir": "3.0.0" + } + }, "connect": { "version": "3.6.6", "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", @@ -926,6 +1201,12 @@ "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", "dev": true }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, "core-js": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", @@ -947,6 +1228,15 @@ "elliptic": "6.4.0" } }, + "create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "dev": true, + "requires": { + "capture-stack-trace": "1.0.0" + } + }, "create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -990,6 +1280,17 @@ } } }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "4.1.2", + "shebang-command": "1.2.0", + "which": "1.3.0" + } + }, "crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", @@ -1017,6 +1318,12 @@ } } }, + "crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", + "dev": true + }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", @@ -1069,6 +1376,18 @@ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-extend": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", + "dev": true + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -1093,6 +1412,59 @@ "clone": "1.0.4" } }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "1.0.2", + "isobject": "3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -1167,6 +1539,27 @@ "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", "dev": true }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "dev": true, + "requires": { + "is-obj": "1.0.1" + } + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1305,6 +1698,15 @@ "is-arrayish": "0.2.1" } }, + "error-stack-parser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.1.tgz", + "integrity": "sha1-oyArj7AxFKqbQKDjZp5IsrZaAQo=", + "dev": true, + "requires": { + "stackframe": "1.0.4" + } + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1366,6 +1768,21 @@ "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", "dev": true }, + "event-stream": { + "version": "3.3.4", + "resolved": "http://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "dev": true, + "requires": { + "duplexer": "0.1.1", + "from": "0.1.7", + "map-stream": "0.1.0", + "pause-stream": "0.0.11", + "split": "0.3.3", + "stream-combiner": "0.0.4", + "through": "2.3.8" + } + }, "eventemitter3": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", @@ -1388,6 +1805,21 @@ "safe-buffer": "5.1.1" } }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + } + }, "expand-braces": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/expand-braces/-/expand-braces-0.1.2.tgz", @@ -1655,6 +2087,21 @@ "for-in": "1.0.2" } }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "0.2.2" + } + }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", + "dev": true + }, "fs-access": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz", @@ -2579,6 +3026,18 @@ "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", "dev": true }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, "glob": { "version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", @@ -2611,12 +3070,40 @@ "is-glob": "2.0.1" } }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "dev": true, + "requires": { + "ini": "1.3.5" + } + }, "globals": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", "dev": true }, + "got": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "dev": true, + "requires": { + "create-error-class": "3.0.2", + "duplexer3": "0.1.4", + "get-stream": "3.0.0", + "is-redirect": "1.0.0", + "is-retry-allowed": "1.1.0", + "is-stream": "1.1.0", + "lowercase-keys": "1.0.1", + "safe-buffer": "5.1.1", + "timed-out": "4.0.1", + "unzip-response": "2.0.1", + "url-parse-lax": "1.0.0" + } + }, "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", @@ -2673,6 +3160,66 @@ "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", "dev": true }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "2.0.6", + "has-values": "1.0.0", + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, "hash-base": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", @@ -2774,6 +3321,24 @@ "integrity": "sha512-VhDzCKN7K8ufStx/CLj5/PDTMgph+qwN5Pkd5i0sGnVwk56zJ0lkT8Qzi1xqWLS0Wp29DgDtNeS7v8/wMoZeHg==", "dev": true }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", + "dev": true + }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, "indent-string": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", @@ -2803,6 +3368,12 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, "inline-source-map": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", @@ -2829,6 +3400,15 @@ "loose-envify": "1.3.1" } }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -2859,6 +3439,43 @@ "builtin-modules": "1.1.1" } }, + "is-ci": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.1.0.tgz", + "integrity": "sha512-c7TnwxLePuqIlxHgr7xtxzycJPegNHFuIrBkwbf8hc58//+Op1CqFkyS+xnIMkwn9UsJIwc174BIjkyBmSpjKg==", + "dev": true, + "requires": { + "ci-info": "1.1.3" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, "is-dotfile": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", @@ -2895,6 +3512,12 @@ "number-is-nan": "1.0.1" } }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, "is-glob": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", @@ -2904,6 +3527,22 @@ "is-extglob": "1.0.0" } }, + "is-installed-globally": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", + "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "dev": true, + "requires": { + "global-dirs": "0.1.1", + "is-path-inside": "1.0.1" + } + }, + "is-npm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", + "dev": true + }, "is-number": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", @@ -2913,6 +3552,55 @@ "kind-of": "3.2.2" } }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-odd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", + "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", + "dev": true, + "requires": { + "is-number": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + } + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "1.0.2" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, "is-posix-bracket": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", @@ -2925,12 +3613,36 @@ "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", "dev": true }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", + "dev": true + }, + "is-retry-allowed": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", + "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", "dev": true }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -3242,6 +3954,112 @@ "handlebars": "4.0.11" } }, + "jasmine": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.1.0.tgz", + "integrity": "sha1-K9Wf1+xuwOistk4J9Fpo7SrRlSo=", + "dev": true, + "requires": { + "glob": "7.1.2", + "jasmine-core": "3.1.0" + }, + "dependencies": { + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "jasmine-core": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.1.0.tgz", + "integrity": "sha1-pHheE11d9lAk38kiSVPfWFvSdmw=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } + }, "jasmine-core": { "version": "2.99.1", "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.99.1.tgz", @@ -3257,6 +4075,57 @@ "colors": "1.1.2" } }, + "jasmine-ts-console-reporter": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/jasmine-ts-console-reporter/-/jasmine-ts-console-reporter-3.1.1.tgz", + "integrity": "sha512-sKR3Dj2VkPbNBxN65KEYIKL5O+/6sG+j4zyhvUUUMuN4CjsuDiUhoneaj4xsIY8XvyTD4l/b9jtJ1uwSb/mZPg==", + "dev": true, + "requires": { + "error-stack-parser": "2.0.1", + "minimatch": "3.0.4", + "source-map": "0.5.7", + "source-map-resolve": "0.5.1" + }, + "dependencies": { + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", @@ -3768,6 +4637,15 @@ "is-buffer": "1.1.6" } }, + "latest-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", + "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "dev": true, + "requires": { + "package-json": "4.0.1" + } + }, "lazy-cache": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", @@ -3877,6 +4755,12 @@ "signal-exit": "3.0.2" } }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true + }, "lru-cache": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.2.tgz", @@ -3892,12 +4776,50 @@ "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.1.6.tgz", "integrity": "sha512-ydJpB8CX8cZ/VE+KMaYaFcZ6+o2LruM6NG76VXdflYTgluvVemz1lW4anE+pyBbLvxJHZdvD1Jy/fOqdzAEJog==" }, + "make-dir": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.2.0.tgz", + "integrity": "sha512-aNUAa4UMg/UougV25bbrU4ZaaKNjJ/3/xnvg/twpmKROPdKZPZ9wGgI0opdZzO8q/zUFawoUuixuOv33eZ61Iw==", + "dev": true, + "requires": { + "pify": "3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, "map-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", "dev": true }, + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "1.0.1" + } + }, "md5.js": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", @@ -4026,6 +4948,27 @@ "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", "dev": true }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "dev": true, + "requires": { + "for-in": "1.0.2", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", @@ -4056,6 +4999,65 @@ "dev": true, "optional": true }, + "nanomatch": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", + "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", + "dev": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "fragment-cache": "0.2.1", + "is-odd": "2.0.0", + "is-windows": "1.0.2", + "kind-of": "6.0.2", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, "negotiator": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", @@ -4067,6 +5069,469 @@ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz", "integrity": "sha1-naYR6giYL0uUIGs760zJZl8gwwA=" }, + "nodemon": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.17.3.tgz", + "integrity": "sha512-8AtS+wA5u6qoE12LONjqOzUzxAI5ObzSw6U5LgqpaO/0y6wwId4l5dN0ZulYyYdpLZD1MbkBp7GjG1hqaoRqYg==", + "dev": true, + "requires": { + "chokidar": "2.0.3", + "debug": "3.1.0", + "ignore-by-default": "1.0.1", + "minimatch": "3.0.4", + "pstree.remy": "1.1.0", + "semver": "5.5.0", + "supports-color": "5.4.0", + "touch": "3.1.0", + "undefsafe": "2.0.2", + "update-notifier": "2.5.0" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "3.1.10", + "normalize-path": "2.1.1" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "repeat-element": "1.1.2", + "snapdragon": "0.8.2", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "chokidar": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", + "integrity": "sha512-zW8iXYZtXMx4kux/nuZVXjkLP+CyIK5Al5FHnj1OgTKGZfp4Oy6/ymtMSKFv3GD8DviEmUPmJg9eFdJ/JzudMg==", + "dev": true, + "requires": { + "anymatch": "2.0.0", + "async-each": "1.0.1", + "braces": "2.3.2", + "fsevents": "1.1.3", + "glob-parent": "3.1.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "4.0.0", + "normalize-path": "2.1.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0", + "upath": "1.0.4" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "3.1.0", + "path-dirname": "1.0.2" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "2.1.1" + } + } + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "dev": true, + "requires": { + "is-extglob": "2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.9", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, "nopt": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", @@ -4097,6 +5562,15 @@ "remove-trailing-separator": "1.1.0" } }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "2.0.1" + } + }, "null-check": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz", @@ -4121,6 +5595,45 @@ "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", "dev": true }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "0.1.1", + "define-property": "0.2.5", + "kind-of": "3.2.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, "object.omit": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", @@ -4131,6 +5644,23 @@ "is-extendable": "0.1.1" } }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -4198,6 +5728,32 @@ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "package-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "dev": true, + "requires": { + "got": "6.7.1", + "registry-auth-token": "3.3.2", + "registry-url": "3.1.0", + "semver": "5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + } + } + }, "pad": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pad/-/pad-2.0.3.tgz", @@ -4280,12 +5836,24 @@ "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", "dev": true }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, "path-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", "dev": true }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, "path-exists": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", @@ -4300,6 +5868,18 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, "path-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", @@ -4311,6 +5891,15 @@ "pinkie-promise": "2.0.1" } }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "dev": true, + "requires": { + "through": "2.3.8" + } + }, "pbkdf2": { "version": "3.0.14", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.14.tgz", @@ -4370,12 +5959,24 @@ } } }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, "preserve": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", @@ -4394,12 +5995,30 @@ "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true }, + "ps-tree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.1.0.tgz", + "integrity": "sha1-tCGyQUDWID8e08dplrRCewjowBQ=", + "dev": true, + "requires": { + "event-stream": "3.3.4" + } + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, + "pstree.remy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.0.tgz", + "integrity": "sha512-q5I5vLRMVtdWa8n/3UEzZX7Lfghzrg9eG2IKk2ENLSofKRCXVqMvMUHxCKgXNaqH/8ebhBxrqftHWnyTFweJ5Q==", + "dev": true, + "requires": { + "ps-tree": "1.1.0" + } + }, "public-encrypt": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", @@ -4553,6 +6172,26 @@ } } }, + "rc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.6.tgz", + "integrity": "sha1-6xiYnG1PTxYsOZ953dKfODVWgJI=", + "dev": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -4667,6 +6306,56 @@ "is-equal-shallow": "0.1.3" } }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "3.0.2", + "safe-regex": "1.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "dev": true, + "requires": { + "rc": "1.2.6", + "safe-buffer": "5.1.1" + } + }, + "registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "dev": true, + "requires": { + "rc": "1.2.6" + } + }, "remap-istanbul": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/remap-istanbul/-/remap-istanbul-0.10.1.tgz", @@ -4753,6 +6442,18 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, "right-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", @@ -4813,18 +6514,67 @@ "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", "dev": true }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "0.1.15" + } + }, "semver": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", "dev": true }, + "semver-diff": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "dev": true, + "requires": { + "semver": "5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + } + } + }, "set-immediate-shim": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", "dev": true }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "split-string": "3.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -4855,12 +6605,141 @@ } } }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "0.11.2", + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "map-cache": "0.2.2", + "source-map": "0.5.7", + "source-map-resolve": "0.5.1", + "use": "3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "1.0.0", + "isobject": "3.0.1", + "snapdragon-util": "3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, "socket.io": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.7.3.tgz", @@ -5006,6 +6885,25 @@ "amdefine": "1.0.1" } }, + "source-map-resolve": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.1.tgz", + "integrity": "sha512-0KW2wvzfxm8NCTb30z0LMNyPqWCdDGE2viwzUaucqJdkTRXtZiSY3I+2A6nVAjmdOy0I4gU8DwnVVGsk9jvP2A==", + "dev": true, + "requires": { + "atob": "2.1.0", + "decode-uri-component": "0.2.0", + "resolve-url": "0.2.1", + "source-map-url": "0.4.0", + "urix": "0.1.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, "spdx-correct": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", @@ -5038,12 +6936,78 @@ "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", "dev": true }, + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "dev": true, + "requires": { + "through": "2.3.8" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "stackframe": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.0.4.tgz", + "integrity": "sha512-to7oADIniaYwS3MhtCa/sQhrxidCCQiF/qp4/m5iN3ipf0Y7Xlri0f6eG29r08aL7JYl8n32AF3Q5GYBZ7K8vw==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "0.2.5", + "object-copy": "0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -5068,6 +7032,15 @@ } } }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "dev": true, + "requires": { + "duplexer": "0.1.1" + } + }, "stream-http": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.1.tgz", @@ -5139,6 +7112,33 @@ } } }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -5166,6 +7166,12 @@ "is-utf8": "0.2.1" } }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, "strip-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", @@ -5175,6 +7181,12 @@ "get-stdin": "4.0.1" } }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -5186,6 +7198,21 @@ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" }, + "term-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", + "dev": true, + "requires": { + "execa": "0.7.0" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, "through2": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.1.tgz", @@ -5230,6 +7257,12 @@ } } }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "dev": true + }, "timers-browserify": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.8.tgz", @@ -5266,6 +7299,89 @@ "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", "dev": true }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "regex-not": "1.0.2", + "safe-regex": "1.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "3.0.0", + "repeat-string": "1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + } + } + }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "1.0.10" + }, + "dependencies": { + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "1.0.9" + } + } + } + }, "trim-newlines": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", @@ -5644,12 +7760,190 @@ "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=", "dev": true }, + "undefsafe": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", + "integrity": "sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY=", + "dev": true, + "requires": { + "debug": "2.6.9" + } + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "requires": { + "arr-union": "3.1.0", + "get-value": "2.0.6", + "is-extendable": "0.1.1", + "set-value": "0.4.3" + }, + "dependencies": { + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "to-object-path": "0.3.0" + } + } + } + }, + "unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "dev": true, + "requires": { + "crypto-random-string": "1.0.0" + } + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", "dev": true }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "0.3.1", + "isobject": "3.0.1" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "2.0.6", + "has-values": "0.1.4", + "isobject": "2.1.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "unzip-response": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", + "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", + "dev": true + }, + "upath": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.0.4.tgz", + "integrity": "sha512-d4SJySNBXDaQp+DPrziv3xGS6w3d2Xt69FijJr86zMPBy23JEloMCEOUBBzuN7xCtjLCnmB9tI/z7SBCahHBOw==", + "dev": true + }, + "update-notifier": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", + "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "dev": true, + "requires": { + "boxen": "1.3.0", + "chalk": "2.4.0", + "configstore": "3.1.2", + "import-lazy": "2.1.0", + "is-ci": "1.1.0", + "is-installed-globally": "0.1.0", + "is-npm": "1.0.0", + "latest-version": "3.1.0", + "semver-diff": "2.1.0", + "xdg-basedir": "3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.0.tgz", + "integrity": "sha512-Wr/w0f4o9LuE7K53cD0qmbAMM+2XNLzR29vFn5hqko4sxGlUsyy363NvmyGIyk5tpe9cjTr9SJYbysEyPkRnFw==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, "url": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", @@ -5668,6 +7962,32 @@ } } }, + "url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "dev": true, + "requires": { + "prepend-http": "1.0.4" + } + }, + "use": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", + "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, "useragent": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", @@ -5750,6 +8070,15 @@ "isexe": "2.0.0" } }, + "widest-line": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.0.tgz", + "integrity": "sha1-AUKk6KJD+IgsAjOqDgKBqnYVInM=", + "dev": true, + "requires": { + "string-width": "2.1.1" + } + }, "window-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", @@ -5768,6 +8097,17 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "write-file-atomic": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", + "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "imurmurhash": "0.1.4", + "signal-exit": "3.0.2" + } + }, "ws": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.2.tgz", @@ -5784,6 +8124,12 @@ "integrity": "sha1-OS2LotDxw00e4tYw8V0O+2jhBIo=", "dev": true }, + "xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", + "dev": true + }, "xmlhttprequest-ssl": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz", diff --git a/package.json b/package.json index 4f64f45cab..8f2df30090 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,10 @@ "build:watch": "tsc -watch", "lint": "tslint src/**/*.ts || true", "lint:fix": "tslint src/**/*.ts --fix", - "test": "karma start --single-run", - "test:watch": "karma start" + "test": "karma start ./spec/support/karma.conf.js --single-run", + "test:watch": "karma start ./spec/support/karma.conf.js", + "test:node": "npm run build && jasmine", + "test:node:watch": "npm run build:watch & nodemon -w ./dist jasmine" }, "devDependencies": { "@types/jasmine": "^2.8.2", @@ -28,6 +30,8 @@ "@types/node-forge": "0.7.1", "@types/papaparse": "4.1.31", "@types/webcrypto": "0.0.28", + "jasmine": "^3.1.0", + "jasmine-ts-console-reporter": "^3.1.1", "jasmine-core": "^2.8.0", "jasmine-spec-reporter": "^4.2.1", "karma": "^1.7.1", @@ -37,6 +41,7 @@ "karma-jasmine": "^1.1.0", "karma-jasmine-html-reporter": "^0.2.2", "karma-typescript": "^3.0.8", + "nodemon": "^1.17.3", "rimraf": "^2.6.2", "tslint": "^5.8.0", "typescript": "^2.7.1" diff --git a/spec/helpers.ts b/spec/helpers.ts new file mode 100644 index 0000000000..a60a7a6673 --- /dev/null +++ b/spec/helpers.ts @@ -0,0 +1,3 @@ +const TSConsoleReporter = require('jasmine-ts-console-reporter'); +jasmine.getEnv().clearReporters(); // Clear default console reporter +jasmine.getEnv().addReporter(new TSConsoleReporter()); diff --git a/spec/node/services/nodeCryptoFunction.service.spec.ts b/spec/node/services/nodeCryptoFunction.service.spec.ts new file mode 100644 index 0000000000..22be44efee --- /dev/null +++ b/spec/node/services/nodeCryptoFunction.service.spec.ts @@ -0,0 +1,62 @@ +import { NodeCryptoFunctionService } from '../../../src/services/nodeCryptoFunction.service'; + +import { UtilsService } from '../../../src/services/utils.service'; + +describe('NodeCrypto Function Service', () => { + describe('aesEncrypt', () => { + it('should successfully aes encrypt data', async () => { + const nodeCryptoFunctionService = new NodeCryptoFunctionService(); + const iv = makeStaticByteArray(16); + const key = makeStaticByteArray(32); + const data = UtilsService.fromUtf8ToArray('EncryptMe!'); + const encValue = await nodeCryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); + expect(UtilsService.fromBufferToB64(encValue)).toBe('ByUF8vhyX4ddU9gcooznwA=='); + }); + }); + + describe('aesDecryptSmall', () => { + it('should successfully aes decrypt data', async () => { + const nodeCryptoFunctionService = new NodeCryptoFunctionService(); + const iv = makeStaticByteArray(16); + const key = makeStaticByteArray(32); + const data = UtilsService.fromB64ToArray('ByUF8vhyX4ddU9gcooznwA=='); + const decValue = await nodeCryptoFunctionService.aesDecryptLarge(data.buffer, iv.buffer, key.buffer); + expect(UtilsService.fromBufferToUtf8(decValue)).toBe('EncryptMe!'); + }); + }); + + describe('aesDecryptLarge', () => { + it('should successfully aes decrypt data', async () => { + const nodeCryptoFunctionService = new NodeCryptoFunctionService(); + const iv = makeStaticByteArray(16); + const key = makeStaticByteArray(32); + const data = UtilsService.fromB64ToArray('ByUF8vhyX4ddU9gcooznwA=='); + const decValue = await nodeCryptoFunctionService.aesDecryptLarge(data.buffer, iv.buffer, key.buffer); + expect(UtilsService.fromBufferToUtf8(decValue)).toBe('EncryptMe!'); + }); + }); + + describe('randomBytes', () => { + it('should make a value of the correct length', async () => { + const nodeCryptoFunctionService = new NodeCryptoFunctionService(); + const randomData = await nodeCryptoFunctionService.randomBytes(16); + expect(randomData.byteLength).toBe(16); + }); + + it('should not make the same value twice', async () => { + const nodeCryptoFunctionService = new NodeCryptoFunctionService(); + const randomData = await nodeCryptoFunctionService.randomBytes(16); + const randomData2 = await nodeCryptoFunctionService.randomBytes(16); + expect(randomData.byteLength === randomData2.byteLength && randomData !== randomData2).toBeTruthy(); + }); + }); +}); + +function makeStaticByteArray(length: number) { + const arr = new Uint8Array(length); + for (let i = 0; i < length; i++) { + arr[i] = i; + } + return arr; +} + diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json new file mode 100644 index 0000000000..19ce1e1633 --- /dev/null +++ b/spec/support/jasmine.json @@ -0,0 +1,12 @@ +{ + "spec_dir": "dist/spec", + "spec_files": [ + "common/**/*[sS]pec.js", + "node/**/*[sS]pec.js" + ], + "helpers": [ + "helpers.js" + ], + "stopSpecOnExpectationFailure": false, + "random": true +} diff --git a/karma.conf.js b/spec/support/karma.conf.js similarity index 95% rename from karma.conf.js rename to spec/support/karma.conf.js index d4793738c1..0168b89698 100644 --- a/karma.conf.js +++ b/spec/support/karma.conf.js @@ -1,7 +1,7 @@ module.exports = function(config) { config.set({ // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: '', + basePath: '../../', // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter @@ -9,6 +9,8 @@ module.exports = function(config) { // list of files / patterns to load in the browser files: [ + 'spec/common/**/*.ts', + 'spec/web/**/*.ts', 'src/abstractions/**/*.ts', 'src/enums/**/*.ts', 'src/models/**/*.ts', @@ -54,9 +56,6 @@ module.exports = function(config) { karmaTypescriptConfig: { tsconfig: './tsconfig.json', - compilerOptions: { - module: 'CommonJS' - }, bundlerOptions: { entrypoints: /\.spec\.ts$/, sourceMap: true diff --git a/src/services/utils.service.spec.ts b/spec/web/services/utils.service.spec.ts similarity index 95% rename from src/services/utils.service.spec.ts rename to spec/web/services/utils.service.spec.ts index 26dfa8bd60..1241674bc1 100644 --- a/src/services/utils.service.spec.ts +++ b/spec/web/services/utils.service.spec.ts @@ -1,4 +1,4 @@ -import { UtilsService } from './utils.service'; +import { UtilsService } from '../../../src/services/utils.service'; describe('Utils Service', () => { describe('getHostname', () => { diff --git a/src/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts similarity index 97% rename from src/services/webCryptoFunction.service.spec.ts rename to spec/web/services/webCryptoFunction.service.spec.ts index 68a8a56090..3922fc24fa 100644 --- a/src/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -1,10 +1,10 @@ -import { DeviceType } from '../enums/deviceType'; +import { DeviceType } from '../../../src/enums/deviceType'; -import { PlatformUtilsService } from '../abstractions/platformUtils.service'; +import { PlatformUtilsService } from '../../../src/abstractions/platformUtils.service'; -import { WebCryptoFunctionService } from './webCryptoFunction.service'; +import { WebCryptoFunctionService } from '../../../src/services/webCryptoFunction.service'; -import { UtilsService } from './utils.service'; +import { UtilsService } from '../../../src/services/utils.service'; describe('WebCrypto Function Service', () => { describe('pbkdf2', () => { diff --git a/tsconfig.json b/tsconfig.json index e647a0bd85..45449f582f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,21 @@ { "compilerOptions": { "moduleResolution": "node", - "target": "ES2016", - "module": "es6", + "target": "ES6", + "module": "commonjs", "sourceMap": true, "declaration": true, "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "declarationDir": "dist/types", - "outDir": "dist/es", + "outDir": "dist", "typeRoots": [ "node_modules/@types" ] }, "include": [ - "src" + "src", + "spec" ] } From 2f76550d3e571fded07eb079ebf554293b1f6947 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 18 Apr 2018 13:38:57 -0400 Subject: [PATCH 0183/1626] concurrently compile ts and run jasmine tests --- package-lock.json | 108 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index bbe75e6d22..7b80b28d2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1113,6 +1113,12 @@ } } }, + "commander": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", + "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=", + "dev": true + }, "compare-versions": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.1.0.tgz", @@ -1142,6 +1148,84 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "concurrently": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-3.5.1.tgz", + "integrity": "sha512-689HrwGw8Rbk1xtV9C4dY6TPJAvIYZbRbnKSAtfJ7tHqICFGoZ0PCWYjxfmerRyxBG0o3sbG3pe7N8vqPwIHuQ==", + "dev": true, + "requires": { + "chalk": "0.5.1", + "commander": "2.6.0", + "date-fns": "1.29.0", + "lodash": "4.17.4", + "rx": "2.3.24", + "spawn-command": "0.0.2-1", + "supports-color": "3.2.3", + "tree-kill": "1.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", + "dev": true + }, + "ansi-styles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", + "dev": true + }, + "chalk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", + "dev": true, + "requires": { + "ansi-styles": "1.1.0", + "escape-string-regexp": "1.0.5", + "has-ansi": "0.1.0", + "strip-ansi": "0.3.0", + "supports-color": "0.2.0" + }, + "dependencies": { + "supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "dev": true + } + } + }, + "has-ansi": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", + "dev": true, + "requires": { + "ansi-regex": "0.2.1" + } + }, + "strip-ansi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "dev": true, + "requires": { + "ansi-regex": "0.2.1" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, "configstore": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", @@ -1339,6 +1423,12 @@ "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", "dev": true }, + "date-fns": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.29.0.tgz", + "integrity": "sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw==", + "dev": true + }, "date-format": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/date-format/-/date-format-0.0.0.tgz", @@ -6500,6 +6590,12 @@ } } }, + "rx": { + "version": "2.3.24", + "resolved": "https://registry.npmjs.org/rx/-/rx-2.3.24.tgz", + "integrity": "sha1-FPlQpCF9fjXapxu8vljv9o6ksrc=", + "dev": true + }, "rxjs": { "version": "5.5.6", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.6.tgz", @@ -6904,6 +7000,12 @@ "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", "dev": true }, + "spawn-command": { + "version": "0.0.2-1", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", + "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=", + "dev": true + }, "spdx-correct": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", @@ -7382,6 +7484,12 @@ } } }, + "tree-kill": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.0.tgz", + "integrity": "sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg==", + "dev": true + }, "trim-newlines": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", diff --git a/package.json b/package.json index 8f2df30090..70467a5948 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test": "karma start ./spec/support/karma.conf.js --single-run", "test:watch": "karma start ./spec/support/karma.conf.js", "test:node": "npm run build && jasmine", - "test:node:watch": "npm run build:watch & nodemon -w ./dist jasmine" + "test:node:watch": "concurrently -k -n \"TSC,Node\" -c \"yellow,cyan\" \"npm run build:watch\" \"nodemon -w ./dist --delay 500ms --exec jasmine\"" }, "devDependencies": { "@types/jasmine": "^2.8.2", @@ -30,6 +30,7 @@ "@types/node-forge": "0.7.1", "@types/papaparse": "4.1.31", "@types/webcrypto": "0.0.28", + "concurrently": "3.5.1", "jasmine": "^3.1.0", "jasmine-ts-console-reporter": "^3.1.1", "jasmine-core": "^2.8.0", From e8824c2c8ba6c16d82136de43fa00d636d8f73b1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 18 Apr 2018 13:43:42 -0400 Subject: [PATCH 0184/1626] lint spec dir --- package.json | 4 ++-- spec/helpers.ts | 1 + spec/node/services/nodeCryptoFunction.service.spec.ts | 1 - src/services/nodeCryptoFunction.service.ts | 2 +- src/services/webCryptoFunction.service.ts | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 70467a5948..4253d44df9 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "prebuild": "rimraf dist/**/*", "build": "tsc", "build:watch": "tsc -watch", - "lint": "tslint src/**/*.ts || true", - "lint:fix": "tslint src/**/*.ts --fix", + "lint": "tslint src/**/*.ts spec/**/*.ts || true", + "lint:fix": "tslint src/**/*.ts spec/**/*.ts --fix", "test": "karma start ./spec/support/karma.conf.js --single-run", "test:watch": "karma start ./spec/support/karma.conf.js", "test:node": "npm run build && jasmine", diff --git a/spec/helpers.ts b/spec/helpers.ts index a60a7a6673..29367d7b99 100644 --- a/spec/helpers.ts +++ b/spec/helpers.ts @@ -1,3 +1,4 @@ +// tslint:disable-next-line const TSConsoleReporter = require('jasmine-ts-console-reporter'); jasmine.getEnv().clearReporters(); // Clear default console reporter jasmine.getEnv().addReporter(new TSConsoleReporter()); diff --git a/spec/node/services/nodeCryptoFunction.service.spec.ts b/spec/node/services/nodeCryptoFunction.service.spec.ts index 22be44efee..e860be7126 100644 --- a/spec/node/services/nodeCryptoFunction.service.spec.ts +++ b/spec/node/services/nodeCryptoFunction.service.spec.ts @@ -59,4 +59,3 @@ function makeStaticByteArray(length: number) { } return arr; } - diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index 6f5bb9499d..9847ff198d 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -79,6 +79,6 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { } private toNodeBuffer(value: ArrayBuffer): Buffer { - return Buffer.from(new Uint8Array(value) as any);; + return Buffer.from(new Uint8Array(value) as any); } } diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index 04de7af331..02351016c5 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -19,14 +19,14 @@ export class WebCryptoFunctionService implements CryptoFunctionService { async pbkdf2(password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', iterations: number): Promise { if (this.isEdge) { - const len = algorithm === 'sha256' ? 32 : 64; + const forgeLen = algorithm === 'sha256' ? 32 : 64; const passwordBytes = this.toByteString(password); const saltBytes = this.toByteString(salt); - const derivedKeyBytes = (forge as any).pbkdf2(passwordBytes, saltBytes, iterations, len, algorithm); + const derivedKeyBytes = (forge as any).pbkdf2(passwordBytes, saltBytes, iterations, forgeLen, algorithm); return this.fromByteStringToBuf(derivedKeyBytes); } - const len = algorithm === 'sha256' ? 256 : 512; + const wcLen = algorithm === 'sha256' ? 256 : 512; const passwordBuf = this.toBuf(password); const saltBuf = this.toBuf(salt); @@ -38,7 +38,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { }; const impKey = await this.subtle.importKey('raw', passwordBuf, { name: 'PBKDF2' }, false, ['deriveBits']); - return await window.crypto.subtle.deriveBits(alg, impKey, len); + return await window.crypto.subtle.deriveBits(alg, impKey, wcLen); } async hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { From e20e878b8b83b9eec33ceff6216e9c5e6885c8cd Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 18 Apr 2018 13:45:31 -0400 Subject: [PATCH 0185/1626] clean on build and build:watch --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 4253d44df9..856670e1ac 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ }, "license": "GPL-3.0", "scripts": { - "prebuild": "rimraf dist/**/*", - "build": "tsc", - "build:watch": "tsc -watch", + "clean": "rimraf dist/**/*", + "build": "npm run clean && tsc", + "build:watch": "npm run clean && tsc -watch", "lint": "tslint src/**/*.ts spec/**/*.ts || true", "lint:fix": "tslint src/**/*.ts spec/**/*.ts --fix", "test": "karma start ./spec/support/karma.conf.js --single-run", From 97fe01e131c6af906188a29d266d626e0a189090 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 19 Apr 2018 00:00:53 -0400 Subject: [PATCH 0186/1626] rsaDecrypt implementation --- src/abstractions/cryptoFunction.service.ts | 1 + src/services/nodeCryptoFunction.service.ts | 20 ++++++++++++++++++++ src/services/webCryptoFunction.service.ts | 15 +++++++++++++-- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/abstractions/cryptoFunction.service.ts b/src/abstractions/cryptoFunction.service.ts index cb755573ba..34ea7d7978 100644 --- a/src/abstractions/cryptoFunction.service.ts +++ b/src/abstractions/cryptoFunction.service.ts @@ -3,4 +3,5 @@ export abstract class CryptoFunctionService { iterations: number) => Promise; hash: (value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise; hmac: (value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise; + rsaDecrypt: (data: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256') => Promise; } diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index 9847ff198d..533647ca8c 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -1,4 +1,5 @@ import * as crypto from 'crypto'; +import * as constants from 'constants'; import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; @@ -56,6 +57,20 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { return Promise.resolve(decBuf.buffer); } + rsaDecrypt(data: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256'): Promise { + if (algorithm !== 'sha1') { + throw new Error('only sha1 is supported on this platform.'); + } + + const nodeData = this.toNodeBuffer(data); + const rsaKey: crypto.RsaPrivateKey = { + key: this.toPem(key), + padding: constants.RSA_PKCS1_OAEP_PADDING, + }; + const decBuf = crypto.publicDecrypt(rsaKey, nodeData); + return Promise.resolve(decBuf.buffer); + } + randomBytes(length: number): Promise { return new Promise((resolve, reject) => { crypto.randomBytes(length, (error, bytes) => { @@ -81,4 +96,9 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { private toNodeBuffer(value: ArrayBuffer): Buffer { return Buffer.from(new Uint8Array(value) as any); } + + private toPem(key: ArrayBuffer): string { + const b64Key = ''; // TODO: key to b84 + return '-----BEGIN PRIVATE KEY-----\n' + b64Key + '\n-----END PRIVATE KEY-----'; + } } diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index 02351016c5..f83e36efc6 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -30,7 +30,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { const passwordBuf = this.toBuf(password); const saltBuf = this.toBuf(salt); - const alg: Pbkdf2Params = { + const pbkdf2Params: Pbkdf2Params = { name: 'PBKDF2', salt: saltBuf, iterations: iterations, @@ -38,7 +38,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { }; const impKey = await this.subtle.importKey('raw', passwordBuf, { name: 'PBKDF2' }, false, ['deriveBits']); - return await window.crypto.subtle.deriveBits(alg, impKey, wcLen); + return await window.crypto.subtle.deriveBits(pbkdf2Params, impKey, wcLen); } async hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { @@ -102,6 +102,17 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return await this.subtle.decrypt({ name: 'AES-CBC', iv: iv }, impKey, data); } + async rsaDecrypt(data: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256'): Promise { + // Note: Edge browser requires that we specify name and hash for both key import and decrypt. + // We cannot use the proper types here. + const rsaParams = { + name: 'RSA-OAEP', + hash: { name: this.toWebCryptoAlgorithm(algorithm) }, + }; + const impKey = await this.subtle.importKey('pkcs8', key, rsaParams, false, ['decrypt']); + return await this.subtle.decrypt(rsaParams, impKey, data); + } + randomBytes(length: number): Promise { const arr = new Uint8Array(length); this.crypto.getRandomValues(arr); From 23917010a778e8464ba8b5f080d3306fc28a489e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 19 Apr 2018 08:00:54 -0400 Subject: [PATCH 0187/1626] psss doc to copy function --- .../components/password-generator-history.component.ts | 5 +++-- src/angular/components/password-generator.component.ts | 5 +++-- src/angular/components/view.component.ts | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/angular/components/password-generator-history.component.ts b/src/angular/components/password-generator-history.component.ts index fd2a7dc372..a94f780a06 100644 --- a/src/angular/components/password-generator-history.component.ts +++ b/src/angular/components/password-generator-history.component.ts @@ -14,7 +14,7 @@ export class PasswordGeneratorHistoryComponent implements OnInit { constructor(protected passwordGenerationService: PasswordGenerationService, protected analytics: Angulartics2, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, - protected toasterService: ToasterService) { } + protected toasterService: ToasterService, private win: Window) { } async ngOnInit() { this.history = await this.passwordGenerationService.getHistory(); @@ -27,7 +27,8 @@ export class PasswordGeneratorHistoryComponent implements OnInit { copy(password: string) { this.analytics.eventTrack.next({ action: 'Copied Historical Password' }); - this.platformUtilsService.copyToClipboard(password); + const copyOptions = this.win != null ? { doc: this.win.document } : null; + this.platformUtilsService.copyToClipboard(password, copyOptions); this.toasterService.popAsync('info', null, this.i18nService.t('valueCopied', this.i18nService.t('password'))); } } diff --git a/src/angular/components/password-generator.component.ts b/src/angular/components/password-generator.component.ts index 7ca52070e0..e4a35c8445 100644 --- a/src/angular/components/password-generator.component.ts +++ b/src/angular/components/password-generator.component.ts @@ -23,7 +23,7 @@ export class PasswordGeneratorComponent implements OnInit { constructor(protected passwordGenerationService: PasswordGenerationService, protected analytics: Angulartics2, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, - protected toasterService: ToasterService) { } + protected toasterService: ToasterService, private win: Window) { } async ngOnInit() { this.options = await this.passwordGenerationService.getOptions(); @@ -61,7 +61,8 @@ export class PasswordGeneratorComponent implements OnInit { copy() { this.analytics.eventTrack.next({ action: 'Copied Generated Password' }); - this.platformUtilsService.copyToClipboard(this.password); + const copyOptions = this.win != null ? { doc: this.win.document } : null; + this.platformUtilsService.copyToClipboard(this.password, copyOptions); this.toasterService.popAsync('info', null, this.i18nService.t('valueCopied', this.i18nService.t('password'))); } diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index da63d5c69f..4eb02cde78 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -116,7 +116,8 @@ export class ViewComponent implements OnDestroy { } this.analytics.eventTrack.next({ action: 'Copied ' + aType }); - this.platformUtilsService.copyToClipboard(value); + const copyOptions = this.win != null ? { doc: this.win.document } : null; + this.platformUtilsService.copyToClipboard(value, copyOptions); this.toasterService.popAsync('info', null, this.i18nService.t('valueCopied', this.i18nService.t(typeI18nKey))); } From fae453056465667ff770a11c1566e9bfc075265f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 19 Apr 2018 14:07:11 -0400 Subject: [PATCH 0188/1626] use TwoFactorProviderType enum for remember me --- src/models/request/tokenRequest.ts | 6 ++++-- src/services/auth.service.ts | 7 ++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/models/request/tokenRequest.ts b/src/models/request/tokenRequest.ts index 4588903241..85d0477b8c 100644 --- a/src/models/request/tokenRequest.ts +++ b/src/models/request/tokenRequest.ts @@ -1,14 +1,16 @@ +import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; + import { DeviceRequest } from './deviceRequest'; export class TokenRequest { email: string; masterPasswordHash: string; token: string; - provider: number; + provider: TwoFactorProviderType; remember: boolean; device?: DeviceRequest; - constructor(email: string, masterPasswordHash: string, provider: number, + constructor(email: string, masterPasswordHash: string, provider: TwoFactorProviderType, token: string, remember: boolean, device?: DeviceRequest) { this.email = email; this.masterPasswordHash = masterPasswordHash; diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 348c2c1d84..b3b9fbc4a0 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -9,8 +9,6 @@ import { TokenRequest } from '../models/request/tokenRequest'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; -import { ConstantsService } from '../services/constants.service'; - import { ApiService } from '../abstractions/api.service'; import { AppIdService } from '../abstractions/appId.service'; import { CryptoService } from '../abstractions/crypto.service'; @@ -69,8 +67,7 @@ export class AuthService { constructor(private cryptoService: CryptoService, private apiService: ApiService, private userService: UserService, private tokenService: TokenService, private appIdService: AppIdService, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, private constantsService: ConstantsService, - private messagingService: MessagingService) { + private platformUtilsService: PlatformUtilsService, private messagingService: MessagingService) { } init() { @@ -152,7 +149,7 @@ export class AuthService { request = new TokenRequest(email, hashedPassword, twoFactorProvider, twoFactorToken, remember, deviceRequest); } else if (storedTwoFactorToken != null) { - request = new TokenRequest(email, hashedPassword, this.constantsService.twoFactorProvider.remember, + request = new TokenRequest(email, hashedPassword, TwoFactorProviderType.Remember, storedTwoFactorToken, false, deviceRequest); } else { request = new TokenRequest(email, hashedPassword, null, null, false, deviceRequest); From 8cf3a6b0f0c0ac5d8ce33d32e0b66a28f90a787b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 19 Apr 2018 14:40:42 -0400 Subject: [PATCH 0189/1626] hostname or uri --- src/models/view/loginUriView.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/models/view/loginUriView.ts b/src/models/view/loginUriView.ts index 6222fef96e..6e221e02d3 100644 --- a/src/models/view/loginUriView.ts +++ b/src/models/view/loginUriView.ts @@ -6,12 +6,15 @@ import { LoginUri } from '../domain/loginUri'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { UtilsService } from '../../services/utils.service'; + export class LoginUriView implements View { match: UriMatchType = null; // tslint:disable private _uri: string; private _domain: string; + private _hostname: string; // tslint:enable constructor(u?: LoginUri) { @@ -47,8 +50,19 @@ export class LoginUriView implements View { return this._domain; } - get domainOrUri(): string { - return this.domain != null ? this.domain : this.uri; + get hostname(): string { + if (this._hostname == null && this.uri != null) { + this._hostname = UtilsService.getHostname(this.uri); + if (this._hostname === '') { + this._hostname = null; + } + } + + return this._hostname; + } + + get hostnameOrUri(): string { + return this.hostname != null ? this.hostname : this.uri; } get isWebsite(): boolean { From 2c2128a871dc488cd845008cf7fe8725e41b4801 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 19 Apr 2018 23:12:06 -0400 Subject: [PATCH 0190/1626] lint error --- src/services/nodeCryptoFunction.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index 533647ca8c..6a8607f4a0 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -1,5 +1,5 @@ -import * as crypto from 'crypto'; import * as constants from 'constants'; +import * as crypto from 'crypto'; import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; From 171fbb078536d4c678447405833a9a9a22a4a430 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 20 Apr 2018 10:59:35 -0400 Subject: [PATCH 0191/1626] vs code debugging node --- .vscode/launch.json | 18 ++++++++++++++++++ .vscode/tasks.json | 12 ++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..d9b0460f86 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Jasmine Individual Test", + "program": "${workspaceRoot}\\node_modules\\jasmine\\bin\\jasmine.js", + "preLaunchTask": "npm run build", + "args": [ + "${workspaceFolder}/dist\\spec\\node\\services\\nodeCryptoFunction.service.spec.js" + ] + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000000..db998c99fc --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,12 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "npm run build", + "type": "shell", + "command": "npm run build" + } + ] +} \ No newline at end of file From 3b2b48dd8d908a74466f6293e8590240e49b9eaa Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 20 Apr 2018 10:59:55 -0400 Subject: [PATCH 0192/1626] misc utils for browser and node --- .../nodeCryptoFunction.service.spec.ts | 16 ++-- .../webCryptoFunction.service.spec.ts | 42 +++++----- src/misc/utils.ts | 76 +++++++++++++++++++ src/services/nodeCryptoFunction.service.ts | 18 +++-- src/services/webCryptoFunction.service.ts | 4 +- 5 files changed, 118 insertions(+), 38 deletions(-) create mode 100644 src/misc/utils.ts diff --git a/spec/node/services/nodeCryptoFunction.service.spec.ts b/spec/node/services/nodeCryptoFunction.service.spec.ts index e860be7126..bb70e8e89b 100644 --- a/spec/node/services/nodeCryptoFunction.service.spec.ts +++ b/spec/node/services/nodeCryptoFunction.service.spec.ts @@ -1,6 +1,6 @@ import { NodeCryptoFunctionService } from '../../../src/services/nodeCryptoFunction.service'; -import { UtilsService } from '../../../src/services/utils.service'; +import { Utils } from '../../../src/misc/utils'; describe('NodeCrypto Function Service', () => { describe('aesEncrypt', () => { @@ -8,9 +8,9 @@ describe('NodeCrypto Function Service', () => { const nodeCryptoFunctionService = new NodeCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); - const data = UtilsService.fromUtf8ToArray('EncryptMe!'); + const data = Utils.fromUtf8ToArray('EncryptMe!'); const encValue = await nodeCryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); - expect(UtilsService.fromBufferToB64(encValue)).toBe('ByUF8vhyX4ddU9gcooznwA=='); + expect(Utils.fromBufferToB64(encValue)).toBe('ByUF8vhyX4ddU9gcooznwA=='); }); }); @@ -19,9 +19,9 @@ describe('NodeCrypto Function Service', () => { const nodeCryptoFunctionService = new NodeCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); - const data = UtilsService.fromB64ToArray('ByUF8vhyX4ddU9gcooznwA=='); - const decValue = await nodeCryptoFunctionService.aesDecryptLarge(data.buffer, iv.buffer, key.buffer); - expect(UtilsService.fromBufferToUtf8(decValue)).toBe('EncryptMe!'); + const data = Utils.fromB64ToArray('ByUF8vhyX4ddU9gcooznwA=='); + const decValue = await nodeCryptoFunctionService.aesDecryptSmall(data.buffer, iv.buffer, key.buffer); + expect(Utils.fromBufferToUtf8(decValue)).toBe('EncryptMe!'); }); }); @@ -30,9 +30,9 @@ describe('NodeCrypto Function Service', () => { const nodeCryptoFunctionService = new NodeCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); - const data = UtilsService.fromB64ToArray('ByUF8vhyX4ddU9gcooznwA=='); + const data = Utils.fromB64ToArray('ByUF8vhyX4ddU9gcooznwA=='); const decValue = await nodeCryptoFunctionService.aesDecryptLarge(data.buffer, iv.buffer, key.buffer); - expect(UtilsService.fromBufferToUtf8(decValue)).toBe('EncryptMe!'); + expect(Utils.fromBufferToUtf8(decValue)).toBe('EncryptMe!'); }); }); diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index 3922fc24fa..8c6b5776fe 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -4,7 +4,7 @@ import { PlatformUtilsService } from '../../../src/abstractions/platformUtils.se import { WebCryptoFunctionService } from '../../../src/services/webCryptoFunction.service'; -import { UtilsService } from '../../../src/services/utils.service'; +import { Utils } from '../../../src/misc/utils'; describe('WebCrypto Function Service', () => { describe('pbkdf2', () => { @@ -71,9 +71,9 @@ describe('WebCrypto Function Service', () => { const webCryptoFunctionService = getWebCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); - const data = UtilsService.fromUtf8ToArray('EncryptMe!'); + const data = Utils.fromUtf8ToArray('EncryptMe!'); const encValue = await webCryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); - expect(UtilsService.fromBufferToB64(encValue)).toBe('ByUF8vhyX4ddU9gcooznwA=='); + expect(Utils.fromBufferToB64(encValue)).toBe('ByUF8vhyX4ddU9gcooznwA=='); }); }); @@ -82,9 +82,9 @@ describe('WebCrypto Function Service', () => { const webCryptoFunctionService = getWebCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); - const data = UtilsService.fromB64ToArray('ByUF8vhyX4ddU9gcooznwA=='); + const data = Utils.fromB64ToArray('ByUF8vhyX4ddU9gcooznwA=='); const decValue = await webCryptoFunctionService.aesDecryptSmall(data.buffer, iv.buffer, key.buffer); - expect(UtilsService.fromBufferToUtf8(decValue)).toBe('EncryptMe!'); + expect(Utils.fromBufferToUtf8(decValue)).toBe('EncryptMe!'); }); }); @@ -93,9 +93,9 @@ describe('WebCrypto Function Service', () => { const webCryptoFunctionService = getWebCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); - const data = UtilsService.fromB64ToArray('ByUF8vhyX4ddU9gcooznwA=='); + const data = Utils.fromB64ToArray('ByUF8vhyX4ddU9gcooznwA=='); const decValue = await webCryptoFunctionService.aesDecryptLarge(data.buffer, iv.buffer, key.buffer); - expect(UtilsService.fromBufferToUtf8(decValue)).toBe('EncryptMe!'); + expect(Utils.fromBufferToUtf8(decValue)).toBe('EncryptMe!'); }); }); @@ -128,26 +128,26 @@ function testPbkdf2(edge: boolean, algorithm: 'sha256' | 'sha512', regularKey: s it('should create valid ' + algorithm + ' key from regular input' + forEdge, async () => { const webCryptoFunctionService = getWebCryptoFunctionService(edge); const key = await webCryptoFunctionService.pbkdf2(regularPassword, regularEmail, algorithm, 5000); - expect(UtilsService.fromBufferToB64(key)).toBe(regularKey); + expect(Utils.fromBufferToB64(key)).toBe(regularKey); }); it('should create valid ' + algorithm + ' key from utf8 input' + forEdge, async () => { const webCryptoFunctionService = getWebCryptoFunctionService(edge); const key = await webCryptoFunctionService.pbkdf2(utf8Password, utf8Email, algorithm, 5000); - expect(UtilsService.fromBufferToB64(key)).toBe(utf8Key); + expect(Utils.fromBufferToB64(key)).toBe(utf8Key); }); it('should create valid ' + algorithm + ' key from unicode input' + forEdge, async () => { const webCryptoFunctionService = getWebCryptoFunctionService(edge); const key = await webCryptoFunctionService.pbkdf2(unicodePassword, regularEmail, algorithm, 5000); - expect(UtilsService.fromBufferToB64(key)).toBe(unicodeKey); + expect(Utils.fromBufferToB64(key)).toBe(unicodeKey); }); it('should create valid ' + algorithm + ' key from array buffer input' + forEdge, async () => { const webCryptoFunctionService = getWebCryptoFunctionService(edge); - const key = await webCryptoFunctionService.pbkdf2(UtilsService.fromUtf8ToArray(regularPassword).buffer, - UtilsService.fromUtf8ToArray(regularEmail).buffer, algorithm, 5000); - expect(UtilsService.fromBufferToB64(key)).toBe(regularKey); + const key = await webCryptoFunctionService.pbkdf2(Utils.fromUtf8ToArray(regularPassword).buffer, + Utils.fromUtf8ToArray(regularEmail).buffer, algorithm, 5000); + expect(Utils.fromBufferToB64(key)).toBe(regularKey); }); } @@ -161,34 +161,34 @@ function testHash(edge: boolean, algorithm: 'sha1' | 'sha256' | 'sha512', regula it('should create valid ' + algorithm + ' hash from regular input' + forEdge, async () => { const webCryptoFunctionService = getWebCryptoFunctionService(edge); const hash = await webCryptoFunctionService.hash(regularValue, algorithm); - expect(UtilsService.fromBufferToHex(hash)).toBe(regularHash); + expect(Utils.fromBufferToHex(hash)).toBe(regularHash); }); it('should create valid ' + algorithm + ' hash from utf8 input' + forEdge, async () => { const webCryptoFunctionService = getWebCryptoFunctionService(edge); const hash = await webCryptoFunctionService.hash(utf8Value, algorithm); - expect(UtilsService.fromBufferToHex(hash)).toBe(utf8Hash); + expect(Utils.fromBufferToHex(hash)).toBe(utf8Hash); }); it('should create valid ' + algorithm + ' hash from unicode input' + forEdge, async () => { const webCryptoFunctionService = getWebCryptoFunctionService(edge); const hash = await webCryptoFunctionService.hash(unicodeValue, algorithm); - expect(UtilsService.fromBufferToHex(hash)).toBe(unicodeHash); + expect(Utils.fromBufferToHex(hash)).toBe(unicodeHash); }); it('should create valid ' + algorithm + ' hash from array buffer input' + forEdge, async () => { const webCryptoFunctionService = getWebCryptoFunctionService(edge); - const hash = await webCryptoFunctionService.hash(UtilsService.fromUtf8ToArray(regularValue).buffer, algorithm); - expect(UtilsService.fromBufferToHex(hash)).toBe(regularHash); + const hash = await webCryptoFunctionService.hash(Utils.fromUtf8ToArray(regularValue).buffer, algorithm); + expect(Utils.fromBufferToHex(hash)).toBe(regularHash); }); } function testHmac(edge: boolean, algorithm: 'sha1' | 'sha256' | 'sha512', mac: string) { it('should create valid ' + algorithm + ' hmac' + (edge ? ' for edge' : ''), async () => { const webCryptoFunctionService = getWebCryptoFunctionService(edge); - const computedMac = await webCryptoFunctionService.hmac(UtilsService.fromUtf8ToArray('SignMe!!').buffer, - UtilsService.fromUtf8ToArray('secretkey').buffer, algorithm); - expect(UtilsService.fromBufferToHex(computedMac)).toBe(mac); + const computedMac = await webCryptoFunctionService.hmac(Utils.fromUtf8ToArray('SignMe!!').buffer, + Utils.fromUtf8ToArray('secretkey').buffer, algorithm); + expect(Utils.fromBufferToHex(computedMac)).toBe(mac); }); } diff --git a/src/misc/utils.ts b/src/misc/utils.ts new file mode 100644 index 0000000000..2b2c323ab2 --- /dev/null +++ b/src/misc/utils.ts @@ -0,0 +1,76 @@ +export class Utils { + static inited = false; + static isNode = false; + static isBrowser = true; + + static init() { + if (Utils.inited) { + return; + } + + Utils.inited = true; + Utils.isNode = typeof window === 'undefined'; + Utils.isBrowser = !Utils.isNode; + } + + static fromB64ToArray(str: string): Uint8Array { + if (Utils.isNode) { + return new Uint8Array(Buffer.from(str, 'base64')); + } else { + const binaryString = window.atob(str); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; + } + } + + static fromUtf8ToArray(str: string): Uint8Array { + if (Utils.isNode) { + return new Uint8Array(Buffer.from(str, 'utf8')); + } else { + const strUtf8 = unescape(encodeURIComponent(str)); + const arr = new Uint8Array(strUtf8.length); + for (let i = 0; i < strUtf8.length; i++) { + arr[i] = strUtf8.charCodeAt(i); + } + return arr; + } + } + + static fromBufferToB64(buffer: ArrayBuffer): string { + if (Utils.isNode) { + return new Buffer(buffer).toString('base64'); + } else { + let binary = ''; + const bytes = new Uint8Array(buffer); + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + return window.btoa(binary); + } + } + + static fromBufferToUtf8(buffer: ArrayBuffer): string { + if (Utils.isNode) { + return new Buffer(buffer).toString('utf8'); + } else { + const bytes = new Uint8Array(buffer); + const encodedString = String.fromCharCode.apply(null, bytes); + return decodeURIComponent(escape(encodedString)); + } + } + + // ref: https://stackoverflow.com/a/40031979/1090359 + static fromBufferToHex(buffer: ArrayBuffer): string { + if (Utils.isNode) { + return new Buffer(buffer).toString('hex'); + } else { + const bytes = new Uint8Array(buffer); + return Array.prototype.map.call(bytes, (x: number) => ('00' + x.toString(16)).slice(-2)).join(''); + } + } +} + +Utils.init(); diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index 6a8607f4a0..8a495b4d60 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -14,7 +14,7 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { if (error != null) { reject(error); } else { - resolve(key.buffer); + resolve(this.toArrayBuffer(key)); } }); }); @@ -24,7 +24,7 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { const nodeValue = this.toNodeValue(value); const hash = crypto.createHash(algorithm); hash.update(nodeValue); - return Promise.resolve(hash.digest().buffer); + return Promise.resolve(this.toArrayBuffer(hash.digest())); } hmac(value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { @@ -32,7 +32,7 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { const nodeKey = this.toNodeBuffer(value); const hmac = crypto.createHmac(algorithm, nodeKey); hmac.update(nodeValue); - return Promise.resolve(hmac.digest().buffer); + return Promise.resolve(this.toArrayBuffer(hmac.digest())); } aesEncrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { @@ -41,7 +41,7 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { const nodeKey = this.toNodeBuffer(key); const cipher = crypto.createCipheriv('aes-256-cbc', nodeKey, nodeIv); const encBuf = Buffer.concat([cipher.update(nodeData), cipher.final()]); - return Promise.resolve(encBuf.buffer); + return Promise.resolve(this.toArrayBuffer(encBuf)); } aesDecryptSmall(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { @@ -54,7 +54,7 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { const nodeKey = this.toNodeBuffer(key); const decipher = crypto.createDecipheriv('aes-256-cbc', nodeKey, nodeIv); const decBuf = Buffer.concat([decipher.update(nodeData), decipher.final()]); - return Promise.resolve(decBuf.buffer); + return Promise.resolve(this.toArrayBuffer(decBuf)); } rsaDecrypt(data: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256'): Promise { @@ -68,7 +68,7 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { padding: constants.RSA_PKCS1_OAEP_PADDING, }; const decBuf = crypto.publicDecrypt(rsaKey, nodeData); - return Promise.resolve(decBuf.buffer); + return Promise.resolve(this.toArrayBuffer(decBuf)); } randomBytes(length: number): Promise { @@ -77,7 +77,7 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { if (error != null) { reject(error); } else { - resolve(bytes.buffer); + resolve(this.toArrayBuffer(bytes)); } }); }); @@ -97,6 +97,10 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { return Buffer.from(new Uint8Array(value) as any); } + private toArrayBuffer(buf: Buffer): ArrayBuffer { + return new Uint8Array(buf).buffer; + } + private toPem(key: ArrayBuffer): string { const b64Key = ''; // TODO: key to b84 return '-----BEGIN PRIVATE KEY-----\n' + b64Key + '\n-----END PRIVATE KEY-----'; diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index f83e36efc6..2b32078688 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -3,7 +3,7 @@ import * as forge from 'node-forge'; import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; -import { UtilsService } from '../services/utils.service'; +import { Utils } from '../misc/utils'; export class WebCryptoFunctionService implements CryptoFunctionService { private crypto: Crypto; @@ -122,7 +122,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { private toBuf(value: string | ArrayBuffer): ArrayBuffer { let buf: ArrayBuffer; if (typeof (value) === 'string') { - buf = UtilsService.fromUtf8ToArray(value).buffer; + buf = Utils.fromUtf8ToArray(value).buffer; } else { buf = value; } From 44f5d1c2673fd9af1b505f0be3032a6073279d64 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 20 Apr 2018 11:02:46 -0400 Subject: [PATCH 0193/1626] include misc in karma --- spec/support/karma.conf.js | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/support/karma.conf.js b/spec/support/karma.conf.js index 0168b89698..d2e6d219ae 100644 --- a/spec/support/karma.conf.js +++ b/spec/support/karma.conf.js @@ -14,6 +14,7 @@ module.exports = function(config) { 'src/abstractions/**/*.ts', 'src/enums/**/*.ts', 'src/models/**/*.ts', + 'src/misc/**/*.ts', 'src/services/**/*.ts' ], From d73012efc8f822059436844f4b808f00bb2bfcb0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 20 Apr 2018 15:32:25 -0400 Subject: [PATCH 0194/1626] rsa encrypt and decrypt tests --- .../nodeCryptoFunction.service.spec.ts | 67 +++++++++++++++- .../webCryptoFunction.service.spec.ts | 78 ++++++++++++++++++- src/misc/utils.ts | 12 +++ src/services/nodeCryptoFunction.service.ts | 50 ++++++++---- src/services/webCryptoFunction.service.ts | 33 ++++---- 5 files changed, 206 insertions(+), 34 deletions(-) diff --git a/spec/node/services/nodeCryptoFunction.service.spec.ts b/spec/node/services/nodeCryptoFunction.service.spec.ts index bb70e8e89b..746a0563b4 100644 --- a/spec/node/services/nodeCryptoFunction.service.spec.ts +++ b/spec/node/services/nodeCryptoFunction.service.spec.ts @@ -2,9 +2,33 @@ import { NodeCryptoFunctionService } from '../../../src/services/nodeCryptoFunct import { Utils } from '../../../src/misc/utils'; +const RsaPublicKey = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl0Vawl/toXzkEvB82FEtqHP' + + '4xlU2ab/v0crqIfXfIoWF/XXdHGIdrZeilnRXPPJT1B9dTsasttEZNnua/0Rek/cjNDHtzT52irfoZYS7X6HNIfOi54Q+egP' + + 'RQ1H7iNHVZz3K8Db9GCSKPeC8MbW6gVCzb15esCe1gGzg6wkMuWYDFYPoh/oBqcIqrGah7firqB1nDedzEjw32heP2DAffVN' + + '084iTDjiWrJNUxBJ2pDD5Z9dT3MzQ2s09ew1yMWK2z37rT3YerC7OgEDmo3WYo3xL3qYJznu3EO2nmrYjiRa40wKSjxsTlUc' + + 'xDF+F0uMW8oR9EMUHgepdepfAtLsSAQIDAQAB'; +const RsaPrivateKey = 'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS8Hz' + + 'YUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86L' + + 'nhD56A9FDUfuI0dVnPcrwNv0YJIo94LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfaF4/' + + 'YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6AQOajdZijfEvepgnOe7cQ7aeatiOJFrjTApK' + + 'PGxOVRzEMX4XS4xbyhH0QxQeB6l16l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq92q' + + 'Buwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tPdr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapj' + + 'WpxEF+11x7r+wM+0xRZQ8sNFYG46aPfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLXUIh' + + '5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTRbuDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk' + + '1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxucvOU' + + 'BeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjAhCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIf' + + 'TFKC/hDk6FKZlgwvupWYJyU9RkyfstPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQYUcU' + + 'q4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vszv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVv' + + 'q1UTXIeQcQnoY5lGHJl3K8mbS3TnXE6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEPjNX' + + '5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBezMRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1' + + 'eLLGd7YV0H+J3fgNc7gGWK51hOrF9JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXgAoE' + + 'Z18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGpIs3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8' + + '+tPVgppLcG0+tMdLjigFQiDUQk2y3WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEzXKZ' + + 'BokBGnjFnTnKcs7nv/O8='; + describe('NodeCrypto Function Service', () => { describe('aesEncrypt', () => { - it('should successfully aes encrypt data', async () => { + it('should successfully encrypt data', async () => { const nodeCryptoFunctionService = new NodeCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); @@ -12,10 +36,21 @@ describe('NodeCrypto Function Service', () => { const encValue = await nodeCryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); expect(Utils.fromBufferToB64(encValue)).toBe('ByUF8vhyX4ddU9gcooznwA=='); }); + + it('should successfully encrypt and then decrypt data', async () => { + const nodeCryptoFunctionService = new NodeCryptoFunctionService(); + const iv = makeStaticByteArray(16); + const key = makeStaticByteArray(32); + const value = 'EncryptMe!'; + const data = Utils.fromUtf8ToArray(value); + const encValue = await nodeCryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); + const decValue = await nodeCryptoFunctionService.aesDecryptSmall(encValue, iv.buffer, key.buffer); + expect(Utils.fromBufferToUtf8(decValue)).toBe(value); + }); }); describe('aesDecryptSmall', () => { - it('should successfully aes decrypt data', async () => { + it('should successfully decrypt data', async () => { const nodeCryptoFunctionService = new NodeCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); @@ -26,7 +61,7 @@ describe('NodeCrypto Function Service', () => { }); describe('aesDecryptLarge', () => { - it('should successfully aes decrypt data', async () => { + it('should successfully decrypt data', async () => { const nodeCryptoFunctionService = new NodeCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); @@ -36,6 +71,32 @@ describe('NodeCrypto Function Service', () => { }); }); + describe('rsaEncrypt', () => { + it('should successfully encrypt and then decrypt data', async () => { + const nodeCryptoFunctionService = new NodeCryptoFunctionService(); + const pubKey = Utils.fromB64ToArray(RsaPublicKey); + const privKey = Utils.fromB64ToArray(RsaPrivateKey); + const value = 'EncryptMe!'; + const data = Utils.fromUtf8ToArray(value); + const encValue = await nodeCryptoFunctionService.rsaEncrypt(data.buffer, pubKey.buffer, 'sha1'); + const decValue = await nodeCryptoFunctionService.rsaDecrypt(encValue, privKey.buffer, 'sha1'); + expect(Utils.fromBufferToUtf8(decValue)).toBe(value); + }); + }); + + describe('rsaDecrypt', () => { + it('should successfully decrypt data', async () => { + const nodeCryptoFunctionService = new NodeCryptoFunctionService(); + const privKey = Utils.fromB64ToArray(RsaPrivateKey); + const data = Utils.fromB64ToArray('A1/p8BQzN9UrbdYxUY2Va5+kPLyfZXF9JsZrjeEXcaclsnHurdxVAJcnbEqYMP3UXV' + + '4YAS/mpf+Rxe6/X0WS1boQdA0MAHSgx95hIlAraZYpiMLLiJRKeo2u8YivCdTM9V5vuAEJwf9Tof/qFsFci3sApdbATkorCT' + + 'zFOIEPF2S1zgperEP23M01mr4dWVdYN18B32YF67xdJHMbFhp5dkQwv9CmscoWq7OE5HIfOb+JAh7BEZb+CmKhM3yWJvoR/D' + + '/5jcercUtK2o+XrzNrL4UQ7yLZcFz6Bfwb/j6ICYvqd/YJwXNE6dwlL57OfwJyCdw2rRYf0/qI00t9u8Iitw=='); + const decValue = await nodeCryptoFunctionService.rsaDecrypt(data.buffer, privKey.buffer, 'sha1'); + expect(Utils.fromBufferToUtf8(decValue)).toBe('EncryptMe!'); + }); + }); + describe('randomBytes', () => { it('should make a value of the correct length', async () => { const nodeCryptoFunctionService = new NodeCryptoFunctionService(); diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index 8c6b5776fe..f41698ed58 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -6,6 +6,30 @@ import { WebCryptoFunctionService } from '../../../src/services/webCryptoFunctio import { Utils } from '../../../src/misc/utils'; +const RsaPublicKey = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl0Vawl/toXzkEvB82FEtqHP' + + '4xlU2ab/v0crqIfXfIoWF/XXdHGIdrZeilnRXPPJT1B9dTsasttEZNnua/0Rek/cjNDHtzT52irfoZYS7X6HNIfOi54Q+egP' + + 'RQ1H7iNHVZz3K8Db9GCSKPeC8MbW6gVCzb15esCe1gGzg6wkMuWYDFYPoh/oBqcIqrGah7firqB1nDedzEjw32heP2DAffVN' + + '084iTDjiWrJNUxBJ2pDD5Z9dT3MzQ2s09ew1yMWK2z37rT3YerC7OgEDmo3WYo3xL3qYJznu3EO2nmrYjiRa40wKSjxsTlUc' + + 'xDF+F0uMW8oR9EMUHgepdepfAtLsSAQIDAQAB'; +const RsaPrivateKey = 'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS8Hz' + + 'YUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86L' + + 'nhD56A9FDUfuI0dVnPcrwNv0YJIo94LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfaF4/' + + 'YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6AQOajdZijfEvepgnOe7cQ7aeatiOJFrjTApK' + + 'PGxOVRzEMX4XS4xbyhH0QxQeB6l16l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq92q' + + 'Buwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tPdr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapj' + + 'WpxEF+11x7r+wM+0xRZQ8sNFYG46aPfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLXUIh' + + '5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTRbuDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk' + + '1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxucvOU' + + 'BeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjAhCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIf' + + 'TFKC/hDk6FKZlgwvupWYJyU9RkyfstPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQYUcU' + + 'q4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vszv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVv' + + 'q1UTXIeQcQnoY5lGHJl3K8mbS3TnXE6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEPjNX' + + '5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBezMRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1' + + 'eLLGd7YV0H+J3fgNc7gGWK51hOrF9JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXgAoE' + + 'Z18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGpIs3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8' + + '+tPVgppLcG0+tMdLjigFQiDUQk2y3WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEzXKZ' + + 'BokBGnjFnTnKcs7nv/O8='; + describe('WebCrypto Function Service', () => { describe('pbkdf2', () => { const regular256Key = 'pj9prw/OHPleXI6bRdmlaD+saJS4awrMiQsQiDjeu2I='; @@ -67,7 +91,7 @@ describe('WebCrypto Function Service', () => { }); describe('aesEncrypt', () => { - it('should successfully aes encrypt data', async () => { + it('should successfully encrypt data', async () => { const webCryptoFunctionService = getWebCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); @@ -75,10 +99,32 @@ describe('WebCrypto Function Service', () => { const encValue = await webCryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); expect(Utils.fromBufferToB64(encValue)).toBe('ByUF8vhyX4ddU9gcooznwA=='); }); + + it('should successfully encrypt and then decrypt small data', async () => { + const webCryptoFunctionService = getWebCryptoFunctionService(); + const iv = makeStaticByteArray(16); + const key = makeStaticByteArray(32); + const value = 'EncryptMe!'; + const data = Utils.fromUtf8ToArray(value); + const encValue = await webCryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); + const decValue = await webCryptoFunctionService.aesDecryptSmall(encValue, iv.buffer, key.buffer); + expect(Utils.fromBufferToUtf8(decValue)).toBe(value); + }); + + it('should successfully encrypt and then decrypt large data', async () => { + const webCryptoFunctionService = getWebCryptoFunctionService(); + const iv = makeStaticByteArray(16); + const key = makeStaticByteArray(32); + const value = 'EncryptMe!'; + const data = Utils.fromUtf8ToArray(value); + const encValue = await webCryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); + const decValue = await webCryptoFunctionService.aesDecryptLarge(encValue, iv.buffer, key.buffer); + expect(Utils.fromBufferToUtf8(decValue)).toBe(value); + }); }); describe('aesDecryptSmall', () => { - it('should successfully aes decrypt data', async () => { + it('should successfully decrypt data', async () => { const webCryptoFunctionService = getWebCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); @@ -89,7 +135,7 @@ describe('WebCrypto Function Service', () => { }); describe('aesDecryptLarge', () => { - it('should successfully aes decrypt data', async () => { + it('should successfully decrypt data', async () => { const webCryptoFunctionService = getWebCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); @@ -99,6 +145,32 @@ describe('WebCrypto Function Service', () => { }); }); + describe('rsaEncrypt', () => { + it('should successfully encrypt and then decrypt data', async () => { + const webCryptoFunctionService = getWebCryptoFunctionService(); + const pubKey = Utils.fromB64ToArray(RsaPublicKey); + const privKey = Utils.fromB64ToArray(RsaPrivateKey); + const value = 'EncryptMe!'; + const data = Utils.fromUtf8ToArray(value); + const encValue = await webCryptoFunctionService.rsaEncrypt(data.buffer, pubKey.buffer, 'sha1'); + const decValue = await webCryptoFunctionService.rsaDecrypt(encValue, privKey.buffer, 'sha1'); + expect(Utils.fromBufferToUtf8(decValue)).toBe(value); + }); + }); + + describe('rsaDecrypt', () => { + it('should successfully decrypt data', async () => { + const webCryptoFunctionService = getWebCryptoFunctionService(); + const privKey = Utils.fromB64ToArray(RsaPrivateKey); + const data = Utils.fromB64ToArray('A1/p8BQzN9UrbdYxUY2Va5+kPLyfZXF9JsZrjeEXcaclsnHurdxVAJcnbEqYMP3UXV' + + '4YAS/mpf+Rxe6/X0WS1boQdA0MAHSgx95hIlAraZYpiMLLiJRKeo2u8YivCdTM9V5vuAEJwf9Tof/qFsFci3sApdbATkorCT' + + 'zFOIEPF2S1zgperEP23M01mr4dWVdYN18B32YF67xdJHMbFhp5dkQwv9CmscoWq7OE5HIfOb+JAh7BEZb+CmKhM3yWJvoR/D' + + '/5jcercUtK2o+XrzNrL4UQ7yLZcFz6Bfwb/j6ICYvqd/YJwXNE6dwlL57OfwJyCdw2rRYf0/qI00t9u8Iitw=='); + const decValue = await webCryptoFunctionService.rsaDecrypt(data.buffer, privKey.buffer, 'sha1'); + expect(Utils.fromBufferToUtf8(decValue)).toBe('EncryptMe!'); + }); + }); + describe('randomBytes', () => { it('should make a value of the correct length', async () => { const webCryptoFunctionService = getWebCryptoFunctionService(); diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 2b2c323ab2..e02bd66f3f 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -39,6 +39,14 @@ export class Utils { } } + static fromByteStringToArray(str: string): Uint8Array { + const arr = new Uint8Array(str.length); + for (let i = 0; i < str.length; i++) { + arr[i] = str.charCodeAt(i); + } + return arr; + } + static fromBufferToB64(buffer: ArrayBuffer): string { if (Utils.isNode) { return new Buffer(buffer).toString('base64'); @@ -62,6 +70,10 @@ export class Utils { } } + static fromBufferToByteString(buffer: ArrayBuffer): string { + return String.fromCharCode.apply(null, new Uint8Array(buffer)); + } + // ref: https://stackoverflow.com/a/40031979/1090359 static fromBufferToHex(buffer: ArrayBuffer): string { if (Utils.isNode) { diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index 8a495b4d60..3ab9db7a16 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -1,8 +1,11 @@ import * as constants from 'constants'; import * as crypto from 'crypto'; +import * as forge from 'node-forge'; import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; +import { Utils } from '../misc/utils'; + export class NodeCryptoFunctionService implements CryptoFunctionService { pbkdf2(password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', iterations: number): Promise { @@ -57,18 +60,32 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { return Promise.resolve(this.toArrayBuffer(decBuf)); } - rsaDecrypt(data: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256'): Promise { - if (algorithm !== 'sha1') { - throw new Error('only sha1 is supported on this platform.'); + rsaEncrypt(data: ArrayBuffer, publicKey: ArrayBuffer, algorithm: 'sha1' | 'sha256'): Promise { + let md: forge.md.MessageDigest; + if (algorithm === 'sha256') { + md = forge.md.sha256.create(); + } else { + md = forge.md.sha1.create(); } - const nodeData = this.toNodeBuffer(data); - const rsaKey: crypto.RsaPrivateKey = { - key: this.toPem(key), - padding: constants.RSA_PKCS1_OAEP_PADDING, - }; - const decBuf = crypto.publicDecrypt(rsaKey, nodeData); - return Promise.resolve(this.toArrayBuffer(decBuf)); + const dataBytes = Utils.fromBufferToByteString(data); + const key = this.toForgePublicKey(publicKey); + const decBytes: string = key.encrypt(dataBytes, 'RSA-OAEP', { md: md }); + return Promise.resolve(Utils.fromByteStringToArray(decBytes).buffer); + } + + rsaDecrypt(data: ArrayBuffer, privateKey: ArrayBuffer, algorithm: 'sha1' | 'sha256'): Promise { + let md: forge.md.MessageDigest; + if (algorithm === 'sha256') { + md = forge.md.sha256.create(); + } else { + md = forge.md.sha1.create(); + } + + const dataBytes = Utils.fromBufferToByteString(data); + const key = this.toForgePrivateKey(privateKey); + const decBytes: string = key.decrypt(dataBytes, 'RSA-OAEP', { md: md }); + return Promise.resolve(Utils.fromByteStringToArray(decBytes).buffer); } randomBytes(length: number): Promise { @@ -101,8 +118,15 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { return new Uint8Array(buf).buffer; } - private toPem(key: ArrayBuffer): string { - const b64Key = ''; // TODO: key to b84 - return '-----BEGIN PRIVATE KEY-----\n' + b64Key + '\n-----END PRIVATE KEY-----'; + private toForgePrivateKey(key: ArrayBuffer): any { + const byteString = Utils.fromBufferToByteString(key); + const asn1 = forge.asn1.fromDer(byteString); + return (forge as any).pki.privateKeyFromAsn1(asn1); + } + + private toForgePublicKey(key: ArrayBuffer): any { + const byteString = Utils.fromBufferToByteString(key); + const asn1 = forge.asn1.fromDer(byteString); + return (forge as any).pki.publicKeyFromAsn1(asn1); } } diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index 2b32078688..25efa06d0e 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -23,7 +23,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { const passwordBytes = this.toByteString(password); const saltBytes = this.toByteString(salt); const derivedKeyBytes = (forge as any).pbkdf2(passwordBytes, saltBytes, iterations, forgeLen, algorithm); - return this.fromByteStringToBuf(derivedKeyBytes); + return Utils.fromByteStringToArray(derivedKeyBytes).buffer; } const wcLen = algorithm === 'sha256' ? 256 : 512; @@ -54,7 +54,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { const valueBytes = this.toByteString(value); md.update(valueBytes, 'raw'); - return this.fromByteStringToBuf(md.digest().data); + return Utils.fromByteStringToArray(md.digest().data).buffer; } const valueBuf = this.toBuf(value); @@ -68,7 +68,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { const hmac = (forge as any).hmac.create(); hmac.start(algorithm, keyBytes); hmac.update(valueBytes); - return this.fromByteStringToBuf(hmac.digest().getBytes()); + return Utils.fromByteStringToArray(hmac.digest().getBytes()).buffer; } const signingAlgorithm = { @@ -94,7 +94,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { decipher.start({ iv: ivBytes }); decipher.update(dataBuffer); decipher.finish(); - return this.fromByteStringToBuf(decipher.output.getBytes()); + return Utils.fromByteStringToArray(decipher.output.getBytes()).buffer; } async aesDecryptLarge(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { @@ -102,14 +102,25 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return await this.subtle.decrypt({ name: 'AES-CBC', iv: iv }, impKey, data); } - async rsaDecrypt(data: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256'): Promise { + async rsaEncrypt(data: ArrayBuffer, publicKey: ArrayBuffer, algorithm: 'sha1' | 'sha256'): Promise { // Note: Edge browser requires that we specify name and hash for both key import and decrypt. // We cannot use the proper types here. const rsaParams = { name: 'RSA-OAEP', hash: { name: this.toWebCryptoAlgorithm(algorithm) }, }; - const impKey = await this.subtle.importKey('pkcs8', key, rsaParams, false, ['decrypt']); + const impKey = await this.subtle.importKey('spki', publicKey, rsaParams, false, ['encrypt']); + return await this.subtle.encrypt(rsaParams, impKey, data); + } + + async rsaDecrypt(data: ArrayBuffer, privateKey: ArrayBuffer, algorithm: 'sha1' | 'sha256'): Promise { + // Note: Edge browser requires that we specify name and hash for both key import and decrypt. + // We cannot use the proper types here. + const rsaParams = { + name: 'RSA-OAEP', + hash: { name: this.toWebCryptoAlgorithm(algorithm) }, + }; + const impKey = await this.subtle.importKey('pkcs8', privateKey, rsaParams, false, ['decrypt']); return await this.subtle.decrypt(rsaParams, impKey, data); } @@ -134,19 +145,11 @@ export class WebCryptoFunctionService implements CryptoFunctionService { if (typeof (value) === 'string') { bytes = forge.util.encodeUtf8(value); } else { - bytes = String.fromCharCode.apply(null, new Uint8Array(value)); + bytes = Utils.fromBufferToByteString(value); } return bytes; } - private fromByteStringToBuf(byteString: string): ArrayBuffer { - const arr = new Uint8Array(byteString.length); - for (let i = 0; i < byteString.length; i++) { - arr[i] = byteString.charCodeAt(i); - } - return arr.buffer; - } - private toWebCryptoAlgorithm(algorithm: 'sha1' | 'sha256' | 'sha512'): string { return algorithm === 'sha1' ? 'SHA-1' : algorithm === 'sha256' ? 'SHA-256' : 'SHA-512'; } From cda281419236425fda2c973739185767c9256487 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 21 Apr 2018 12:57:29 -0400 Subject: [PATCH 0195/1626] typemock lib for mocking services --- package-lock.json | 23 ++++++++++++ package.json | 1 + .../webCryptoFunction.service.spec.ts | 36 ++++--------------- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7b80b28d2e..0517016707 100644 --- a/package-lock.json +++ b/package-lock.json @@ -979,6 +979,12 @@ } } }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -6055,6 +6061,12 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, + "postinstall-build": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postinstall-build/-/postinstall-build-5.0.1.tgz", + "integrity": "sha1-uRepB5smF42aJK9aXNjLSpkdEbk=", + "dev": true + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -7828,6 +7840,17 @@ "mime-types": "2.1.18" } }, + "typemoq": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typemoq/-/typemoq-2.1.0.tgz", + "integrity": "sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw==", + "dev": true, + "requires": { + "circular-json": "0.3.3", + "lodash": "4.17.4", + "postinstall-build": "5.0.1" + } + }, "typescript": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.1.tgz", diff --git a/package.json b/package.json index 856670e1ac..ea2447ebee 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "nodemon": "^1.17.3", "rimraf": "^2.6.2", "tslint": "^5.8.0", + "typemoq": "^2.1.0", "typescript": "^2.7.1" }, "dependencies": { diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index f41698ed58..386a56aebd 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -1,3 +1,5 @@ +import * as TypeMoq from 'typemoq'; + import { DeviceType } from '../../../src/enums/deviceType'; import { PlatformUtilsService } from '../../../src/abstractions/platformUtils.service'; @@ -265,8 +267,9 @@ function testHmac(edge: boolean, algorithm: 'sha1' | 'sha256' | 'sha512', mac: s } function getWebCryptoFunctionService(edge = false) { - const platformUtilsService = new BrowserPlatformUtilsService(edge); - return new WebCryptoFunctionService(window, platformUtilsService); + const platformUtilsMock = TypeMoq.Mock.ofType(PlatformUtilsServiceMock); + platformUtilsMock.setup((x) => x.isEdge()).returns(() => edge); + return new WebCryptoFunctionService(window, platformUtilsMock.object); } function makeStaticByteArray(length: number) { @@ -277,31 +280,6 @@ function makeStaticByteArray(length: number) { return arr; } -class BrowserPlatformUtilsService implements PlatformUtilsService { - identityClientId: string; - getDevice: () => DeviceType; - getDeviceString: () => string; - isFirefox: () => boolean; - isChrome: () => boolean; - isOpera: () => boolean; - isVivaldi: () => boolean; - isSafari: () => boolean; - isMacAppStore: () => boolean; - analyticsId: () => string; - getDomain: (uriString: string) => string; - isViewOpen: () => boolean; - launchUri: (uri: string, options?: any) => void; - saveFile: (win: Window, blobData: any, blobOptions: any, fileName: string) => void; - getApplicationVersion: () => string; - supportsU2f: (win: Window) => boolean; - showDialog: (text: string, title?: string, confirmText?: string, cancelText?: string, - type?: string) => Promise; - isDev: () => boolean; - copyToClipboard: (text: string, options?: any) => void; - - constructor(private edge: boolean) { } - - isEdge() { - return this.edge; - } +class PlatformUtilsServiceMock extends PlatformUtilsService { + isEdge = () => false; } From fc1114a6bd66b62b7821958fca2a0c9260c08ddc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 21 Apr 2018 23:14:04 -0400 Subject: [PATCH 0196/1626] refactor crypto service to use crypto functions --- src/abstractions/crypto.service.ts | 9 +- src/abstractions/cryptoFunction.service.ts | 5 + src/angular/components/export.component.ts | 4 +- src/angular/components/lock.component.ts | 2 +- src/angular/components/register.component.ts | 2 +- src/models/domain/cipherString.ts | 16 +- src/models/domain/encryptedObject.ts | 6 +- src/models/domain/index.ts | 1 - src/models/domain/symmetricCryptoKey.ts | 77 +--- .../domain/symmetricCryptoKeyBuffers.ts | 9 - src/services/auth.service.ts | 2 +- src/services/crypto.service.ts | 402 +++++++----------- src/services/nodeCryptoFunction.service.ts | 2 +- src/services/passwordGeneration.service.ts | 2 +- tsconfig.json | 1 + 15 files changed, 211 insertions(+), 329 deletions(-) delete mode 100644 src/models/domain/symmetricCryptoKeyBuffers.ts diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 704171f2c8..473758ae6e 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -22,14 +22,13 @@ export abstract class CryptoService { clearOrgKeys: (memoryOnly?: boolean) => Promise; clearKeys: () => Promise; toggleKey: () => Promise; - makeKey: (password: string, salt: string) => SymmetricCryptoKey; + makeKey: (password: string, salt: string) => Promise; hashPassword: (password: string, key: SymmetricCryptoKey) => Promise; makeEncKey: (key: SymmetricCryptoKey) => Promise; - encrypt: (plainValue: string | Uint8Array, key?: SymmetricCryptoKey, - plainValueEncoding?: string) => Promise; + encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise; encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise; - decrypt: (cipherString: CipherString, key?: SymmetricCryptoKey, outputEncoding?: string) => Promise; + decrypt: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise; + decryptToUtf8: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise; decryptFromBytes: (encBuf: ArrayBuffer, key: SymmetricCryptoKey) => Promise; - rsaDecrypt: (encValue: string) => Promise; sha1: (password: string) => Promise; } diff --git a/src/abstractions/cryptoFunction.service.ts b/src/abstractions/cryptoFunction.service.ts index 34ea7d7978..c8f5bdf5bd 100644 --- a/src/abstractions/cryptoFunction.service.ts +++ b/src/abstractions/cryptoFunction.service.ts @@ -3,5 +3,10 @@ export abstract class CryptoFunctionService { iterations: number) => Promise; hash: (value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise; hmac: (value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise; + aesEncrypt: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise; + aesDecryptSmall: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise; + aesDecryptLarge: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise; + rsaEncrypt: (data: ArrayBuffer, publicKey: ArrayBuffer, algorithm: 'sha1' | 'sha256') => Promise; rsaDecrypt: (data: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256') => Promise; + randomBytes: (length: number) => Promise; } diff --git a/src/angular/components/export.component.ts b/src/angular/components/export.component.ts index f763cf33ab..ac5a6a87a0 100644 --- a/src/angular/components/export.component.ts +++ b/src/angular/components/export.component.ts @@ -40,7 +40,7 @@ export class ExportComponent { } const email = await this.userService.getEmail(); - const key = this.cryptoService.makeKey(this.masterPassword, email); + const key = await this.cryptoService.makeKey(this.masterPassword, email); const keyHash = await this.cryptoService.hashPassword(this.masterPassword, key); const storedKeyHash = await this.cryptoService.getKeyHash(); @@ -67,7 +67,7 @@ export class ExportComponent { private async checkPassword() { const email = await this.userService.getEmail(); - const key = this.cryptoService.makeKey(this.masterPassword, email); + const key = await this.cryptoService.makeKey(this.masterPassword, email); const keyHash = await this.cryptoService.hashPassword(this.masterPassword, key); const storedKeyHash = await this.cryptoService.getKeyHash(); if (storedKeyHash == null || keyHash == null || storedKeyHash !== keyHash) { diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index a109cf31b0..8fd2880000 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -28,7 +28,7 @@ export class LockComponent { } const email = await this.userService.getEmail(); - const key = this.cryptoService.makeKey(this.masterPassword, email); + const key = await this.cryptoService.makeKey(this.masterPassword, email); const keyHash = await this.cryptoService.hashPassword(this.masterPassword, key); const storedKeyHash = await this.cryptoService.getKeyHash(); diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index eb9c39f0da..8b89897324 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -69,7 +69,7 @@ export class RegisterComponent { private async register() { this.email = this.email.toLowerCase(); - const key = this.cryptoService.makeKey(this.masterPassword, this.email); + const key = await this.cryptoService.makeKey(this.masterPassword, this.email); const encKey = await this.cryptoService.makeEncKey(key); const hashedPassword = await this.cryptoService.hashPassword(this.masterPassword, key); const request = new RegisterRequest(this.email, hashedPassword, this.hint, encKey.encryptedString); diff --git a/src/models/domain/cipherString.ts b/src/models/domain/cipherString.ts index d19d191065..b6068edd16 100644 --- a/src/models/domain/cipherString.ts +++ b/src/models/domain/cipherString.ts @@ -87,7 +87,7 @@ export class CipherString { } } - decrypt(orgId: string): Promise { + async decrypt(orgId: string): Promise { if (this.decryptedValue) { return Promise.resolve(this.decryptedValue); } @@ -100,14 +100,12 @@ export class CipherString { 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(() => { + try { + const orgKey = await cryptoService.getOrgKey(orgId); + this.decryptedValue = await cryptoService.decryptToUtf8(this, orgKey); + } catch (e) { this.decryptedValue = '[error: cannot decrypt]'; - return this.decryptedValue; - }); + } + return this.decryptedValue; } } diff --git a/src/models/domain/encryptedObject.ts b/src/models/domain/encryptedObject.ts index e28911e384..10e32797a6 100644 --- a/src/models/domain/encryptedObject.ts +++ b/src/models/domain/encryptedObject.ts @@ -1,8 +1,8 @@ import { SymmetricCryptoKey } from './symmetricCryptoKey'; export class EncryptedObject { - iv: Uint8Array; - ct: Uint8Array; - mac: Uint8Array; + iv: ArrayBuffer; + ct: ArrayBuffer; + mac: ArrayBuffer; key: SymmetricCryptoKey; } diff --git a/src/models/domain/index.ts b/src/models/domain/index.ts index 99846ea2f2..a25fe24735 100644 --- a/src/models/domain/index.ts +++ b/src/models/domain/index.ts @@ -14,4 +14,3 @@ export { LoginUri } from './loginUri'; export { PasswordHistory } from './passwordHistory'; export { SecureNote } from './secureNote'; export { SymmetricCryptoKey } from './symmetricCryptoKey'; -export { SymmetricCryptoKeyBuffers } from './symmetricCryptoKeyBuffers'; diff --git a/src/models/domain/symmetricCryptoKey.ts b/src/models/domain/symmetricCryptoKey.ts index 4e3a5d6c2d..dd7a6800d0 100644 --- a/src/models/domain/symmetricCryptoKey.ts +++ b/src/models/domain/symmetricCryptoKey.ts @@ -1,80 +1,45 @@ -import * as forge from 'node-forge'; - import { EncryptionType } from '../../enums/encryptionType'; -import { SymmetricCryptoKeyBuffers } from './symmetricCryptoKeyBuffers'; - -import { UtilsService } from '../../services/utils.service'; +import { Utils } from '../../misc/utils'; export class SymmetricCryptoKey { - key: string; - keyB64: string; - encKey: string; - macKey: string; + key: ArrayBuffer; + encKey?: ArrayBuffer; + macKey?: ArrayBuffer; encType: EncryptionType; - keyBuf: SymmetricCryptoKeyBuffers; - constructor(keyBytes: string, b64KeyBytes?: boolean, encType?: EncryptionType) { - if (b64KeyBytes) { - keyBytes = forge.util.decode64(keyBytes); + keyB64: string; + + constructor(key: ArrayBuffer, encType?: EncryptionType) { + if (key == null) { + throw new Error('Must provide key'); } - 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) { + if (key.byteLength === 32) { encType = EncryptionType.AesCbc256_B64; - } else if (bufferLength === 64) { + } else if (key.byteLength === 64) { encType = EncryptionType.AesCbc256_HmacSha256_B64; } else { throw new Error('Unable to determine encType.'); } } - this.key = keyBytes; - this.keyB64 = forge.util.encode64(keyBytes); + this.key = key; + this.keyB64 = Utils.fromBufferToB64(key); this.encType = encType; - if (encType === EncryptionType.AesCbc256_B64 && bufferLength === 32) { - this.encKey = keyBytes; + if (encType === EncryptionType.AesCbc256_B64 && key.byteLength === 32) { + this.encKey = key; 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 if (encType === EncryptionType.AesCbc128_HmacSha256_B64 && key.byteLength === 32) { + this.encKey = key.slice(0, 16); + this.macKey = key.slice(16, 32); + } else if (encType === EncryptionType.AesCbc256_HmacSha256_B64 && key.byteLength === 64) { + this.encKey = key.slice(0, 32); + this.macKey = key.slice(32, 64); } else { throw new Error('Unsupported encType/key length.'); } } - - getBuffers(): SymmetricCryptoKeyBuffers { - 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 deleted file mode 100644 index f27d8aed2e..0000000000 --- a/src/models/domain/symmetricCryptoKeyBuffers.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class SymmetricCryptoKeyBuffers { - key: ArrayBuffer; - encKey?: ArrayBuffer; - macKey?: ArrayBuffer; - - constructor(key: ArrayBuffer) { - this.key = key; - } -} diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index b3b9fbc4a0..0f19db50c6 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -95,7 +95,7 @@ export class AuthService { async logIn(email: string, masterPassword: string): Promise { this.selectedTwoFactorProviderType = null; email = email.toLowerCase(); - const key = this.cryptoService.makeKey(masterPassword, email); + const key = await this.cryptoService.makeKey(masterPassword, email); const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); return await this.logInHelper(email, hashedPassword, key); } diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index b8db2f048e..e08094f7dd 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -8,10 +8,12 @@ import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; import { ProfileOrganizationResponse } from '../models/response/profileOrganizationResponse'; import { CryptoService as CryptoServiceAbstraction } from '../abstractions/crypto.service'; -import { StorageService as StorageServiceInterface } from '../abstractions/storage.service'; +import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; +import { StorageService } from '../abstractions/storage.service'; import { ConstantsService } from './constants.service'; -import { UtilsService } from './utils.service'; + +import { Utils } from '../misc/utils'; const Keys = { key: 'key', @@ -21,18 +23,6 @@ const Keys = { 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 CryptoServiceAbstraction { private key: SymmetricCryptoKey; private encKey: SymmetricCryptoKey; @@ -41,9 +31,8 @@ export class CryptoService implements CryptoServiceAbstraction { private privateKey: ArrayBuffer; private orgKeys: Map; - constructor(private storageService: StorageServiceInterface, - private secureStorageService: StorageServiceInterface) { - } + constructor(private storageService: StorageService, private secureStorageService: StorageService, + private cryptoFunctionService: CryptoFunctionService) { } async setKey(key: SymmetricCryptoKey): Promise { this.key = key; @@ -66,6 +55,7 @@ export class CryptoService implements CryptoServiceAbstraction { if (encKey == null) { return; } + await this.storageService.save(Keys.encKey, encKey); this.encKey = null; } @@ -99,8 +89,8 @@ export class CryptoService implements CryptoServiceAbstraction { } const key = await this.secureStorageService.get(Keys.key); - if (key) { - this.key = new SymmetricCryptoKey(key, true); + if (key != null) { + this.key = new SymmetricCryptoKey(Utils.fromB64ToArray(key).buffer); } return key == null ? null : this.key; @@ -129,7 +119,7 @@ export class CryptoService implements CryptoServiceAbstraction { return null; } - const decEncKey = await this.decrypt(new CipherString(encKey), key, 'raw'); + const decEncKey = await this.decrypt(new CipherString(encKey), key); if (decEncKey == null) { return null; } @@ -148,8 +138,7 @@ export class CryptoService implements CryptoServiceAbstraction { return null; } - const privateKeyB64 = await this.decrypt(new CipherString(encPrivateKey), null, 'b64'); - this.privateKey = UtilsService.fromB64ToArray(privateKeyB64).buffer; + this.privateKey = await this.decrypt(new CipherString(encPrivateKey), null); return this.privateKey; } @@ -159,7 +148,7 @@ export class CryptoService implements CryptoServiceAbstraction { } const encOrgKeys = await this.storageService.get(Keys.encOrgKeys); - if (!encOrgKeys) { + if (encOrgKeys == null) { return null; } @@ -171,8 +160,8 @@ export class CryptoService implements CryptoServiceAbstraction { continue; } - const decValueB64 = await this.rsaDecrypt(encOrgKeys[orgId]); - orgKeys.set(orgId, new SymmetricCryptoKey(decValueB64, true)); + const decValue = await this.rsaDecrypt(encOrgKeys[orgId]); + orgKeys.set(orgId, new SymmetricCryptoKey(decValue)); setKey = true; } @@ -253,91 +242,83 @@ export class CryptoService implements CryptoServiceAbstraction { 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 makeKey(password: string, salt: string): Promise { + const key = await this.cryptoFunctionService.pbkdf2(password, salt, 'sha256', 5000); + return new SymmetricCryptoKey(key); } async hashPassword(password: string, key: SymmetricCryptoKey): Promise { const storedKey = await this.getKey(); key = key || storedKey; - if (!password || !key) { + if (password == null || key == null) { 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); + const hash = await this.cryptoFunctionService.pbkdf2(key.key, password, 'sha256', 1); + return Utils.fromBufferToB64(hash); } - makeEncKey(key: SymmetricCryptoKey): Promise { - const bytes = new Uint8Array(512 / 8); - Crypto.getRandomValues(bytes); - return this.encrypt(bytes, key, 'raw'); + async makeEncKey(key: SymmetricCryptoKey): Promise { + const bytes = await this.cryptoFunctionService.randomBytes(64); + return this.encrypt(bytes, key); } - async encrypt(plainValue: string | Uint8Array, key?: SymmetricCryptoKey, - plainValueEncoding: string = 'utf8'): Promise { - if (!plainValue) { + async encrypt(plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey): Promise { + if (plainValue == null) { return Promise.resolve(null); } - let plainValueArr: Uint8Array; - if (plainValueEncoding === 'utf8') { - plainValueArr = UtilsService.fromUtf8ToArray(plainValue as string); + let plainBuf: ArrayBuffer; + if (typeof (plainValue) === 'string') { + plainBuf = Utils.fromUtf8ToArray(plainValue).buffer; } else { - plainValueArr = plainValue as Uint8Array; + plainBuf = plainValue; } - 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); + const encObj = await this.aesEncrypt(plainBuf, key); + const iv = Utils.fromBufferToB64(encObj.iv); + const ct = Utils.fromBufferToB64(encObj.ct); + const mac = encObj.mac != null ? Utils.fromBufferToB64(encObj.mac) : null; + return new CipherString(encObj.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; + if (encValue.mac != null) { + macLen = encValue.mac.byteLength; } - const encBytes = new Uint8Array(1 + encValue.iv.length + macLen + encValue.ct.length); + const encBytes = new Uint8Array(1 + encValue.iv.byteLength + macLen + encValue.ct.byteLength); encBytes.set([encValue.key.encType]); - encBytes.set(encValue.iv, 1); - if (encValue.mac) { - encBytes.set(encValue.mac, 1 + encValue.iv.length); + encBytes.set(new Uint8Array(encValue.iv), 1); + if (encValue.mac != null) { + encBytes.set(new Uint8Array(encValue.mac), 1 + encValue.iv.byteLength); } - encBytes.set(encValue.ct, 1 + encValue.iv.length + macLen); + encBytes.set(new Uint8Array(encValue.ct), 1 + encValue.iv.byteLength + 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) { + async decrypt(cipherString: CipherString, key?: SymmetricCryptoKey): Promise { + const iv = Utils.fromB64ToArray(cipherString.initializationVector).buffer; + const ct = Utils.fromB64ToArray(cipherString.cipherText).buffer; + const mac = cipherString.mac ? Utils.fromB64ToArray(cipherString.mac).buffer : null; + const decipher = await this.aesDecrypt(cipherString.encryptionType, ct, iv, mac, key); + if (decipher == null) { return null; } - if (outputEncoding === 'utf8') { - return decipher.output.toString('utf8'); - } + return decipher; + } - const decipherBytes = decipher.output.getBytes(); - if (outputEncoding === 'b64') { - return forge.util.encode64(decipherBytes); - } else { - return decipherBytes; - } + async decryptToUtf8(cipherString: CipherString, key?: SymmetricCryptoKey): Promise { + const decipher = await this.decrypt(cipherString, key); + return Utils.fromBufferToUtf8(decipher); } async decryptFromBytes(encBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise { - if (!encBuf) { + if (encBuf == null) { throw new Error('no encBuf.'); } @@ -370,10 +351,93 @@ export class CryptoService implements CryptoServiceAbstraction { return null; } - return await this.aesDecryptWC(encType, ctBytes.buffer, ivBytes.buffer, macBytes ? macBytes.buffer : null, key); + return await this.aesDecryptLarge(encType, ctBytes.buffer, ivBytes.buffer, + macBytes != null ? macBytes.buffer : null, key); } - async rsaDecrypt(encValue: string): Promise { + async sha1(password: string): Promise { + const hash = await this.cryptoFunctionService.hash(password, 'sha1'); + return Utils.fromBufferToHex(hash); + } + + // Helpers + + private async aesEncrypt(plainValue: ArrayBuffer, key: SymmetricCryptoKey): Promise { + const obj = new EncryptedObject(); + obj.key = await this.getKeyForEncryption(key); + obj.iv = await this.cryptoFunctionService.randomBytes(16); + obj.ct = await this.cryptoFunctionService.aesEncrypt(plainValue, obj.iv, obj.key.encKey); + + if (obj.key.macKey != null) { + const macData = new Uint8Array(obj.iv.byteLength + obj.ct.byteLength); + macData.set(new Uint8Array(obj.iv), 0); + macData.set(new Uint8Array(obj.ct), obj.iv.byteLength); + obj.mac = await this.cryptoFunctionService.hmac(macData.buffer, obj.key.macKey, 'sha256'); + } + + return obj; + } + + private async aesDecrypt(encType: EncryptionType, ct: ArrayBuffer, iv: ArrayBuffer, mac: ArrayBuffer, + key: SymmetricCryptoKey): Promise { + const keyForEnc = await this.getKeyForEncryption(key); + const theKey = this.resolveLegacyKey(encType, keyForEnc); + + if (theKey.macKey != null && mac == null) { + // tslint:disable-next-line + console.error('mac required.'); + return null; + } + + if (encType !== theKey.encType) { + // tslint:disable-next-line + console.error('encType unavailable.'); + return null; + } + + if (theKey.macKey != null && mac != null) { + const macData = new Uint8Array(iv.byteLength + ct.byteLength); + macData.set(new Uint8Array(iv), 0); + macData.set(new Uint8Array(ct), iv.byteLength); + const computedMac = await this.cryptoFunctionService.hmac(new Uint8Array(iv).buffer, + theKey.macKey, 'sha256'); + if (!this.macsEqual(computedMac, mac)) { + // tslint:disable-next-line + console.error('mac failed.'); + return null; + } + } + + return this.cryptoFunctionService.aesDecryptSmall(ct, iv, theKey.encKey); + } + + private async aesDecryptLarge(encType: EncryptionType, ct: ArrayBuffer, iv: ArrayBuffer, + mac: ArrayBuffer, key: SymmetricCryptoKey): Promise { + const theKey = await this.getKeyForEncryption(key); + if (theKey.macKey == null || mac == null) { + return null; + } + + const macData = new Uint8Array(iv.byteLength + ct.byteLength); + macData.set(new Uint8Array(iv), 0); + macData.set(new Uint8Array(ct), iv.byteLength); + const computedMac = await this.cryptoFunctionService.hmac(new Uint8Array(iv).buffer, + theKey.macKey, 'sha256'); + if (computedMac === null) { + return null; + } + + const macsMatch = await this.macsEqual(mac, computedMac); + if (macsMatch === false) { + // tslint:disable-next-line + console.error('mac failed.'); + return null; + } + + return await this.cryptoFunctionService.aesDecryptLarge(ct, iv, theKey.encKey); + } + + private async rsaDecrypt(encValue: string): Promise { const headerPieces = encValue.split('.'); let encType: EncryptionType = null; let encPieces: string[]; @@ -409,186 +473,51 @@ export class CryptoService implements CryptoServiceAbstraction { throw new Error('encPieces unavailable.'); } + const ct = Utils.fromB64ToArray(encPieces[0]).buffer; 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(macBytes, computedMacBytes); + const mac = Utils.fromB64ToArray(encPieces[1]).buffer; + const computedMac = await this.cryptoFunctionService.hmac(ct, key.macKey, 'sha256'); + const macsEqual = await this.macsEqual(mac, computedMac); if (!macsEqual) { throw new Error('MAC failed.'); } } - const privateKeyBytes = await this.getPrivateKey(); - if (!privateKeyBytes) { + const privateKey = await this.getPrivateKey(); + if (privateKey == null) { throw new Error('No private key.'); } - let rsaAlgorithm: any = null; + let alg: 'sha1' | 'sha256' = 'sha1'; switch (encType) { case EncryptionType.Rsa2048_OaepSha256_B64: case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: - rsaAlgorithm = { - name: 'RSA-OAEP', - hash: { name: 'SHA-256' }, - }; + alg = 'sha256'; 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; - } - - async sha1(password: string): Promise { - const passwordArr = UtilsService.fromUtf8ToArray(password); - const hash = await Subtle.digest({ name: 'SHA-1' }, passwordArr); - return UtilsService.fromBufferToHex(hash); - } - - // 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 (theKey.macKey != null && macBytes == null) { - // tslint:disable-next-line - console.error('macBytes required.'); - return null; - } - - 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(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(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); + return this.cryptoFunctionService.rsaDecrypt(ct, privateKey, alg); } // 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/ // ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy - private macsEqual(mac1: string, mac2: string): boolean { - const hmac = (forge as any).hmac.create(); - - hmac.start('sha256', this.getRandomBytes(32)); - 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(mac1Buf: ArrayBuffer, mac2Buf: ArrayBuffer): Promise { - const compareKey = new Uint8Array(32); - Crypto.getRandomValues(compareKey); - const macKey = await Subtle.importKey('raw', compareKey.buffer, SigningAlgorithm, false, ['sign']); - const mac1 = await Subtle.sign(SigningAlgorithm, macKey, mac1Buf); - const mac2 = await Subtle.sign(SigningAlgorithm, macKey, mac2Buf); - - if (mac1.byteLength !== mac2.byteLength) { + private async macsEqual(mac1: ArrayBuffer, mac2: ArrayBuffer): Promise { + const key = await this.cryptoFunctionService.randomBytes(32); + const newMac1 = await this.cryptoFunctionService.hmac(mac1, key, 'sha256'); + const newMac2 = await this.cryptoFunctionService.hmac(mac2, key, 'sha256'); + if (newMac1.byteLength !== newMac2.byteLength) { return false; } - const arr1 = new Uint8Array(mac1); - const arr2 = new Uint8Array(mac2); - + const arr1 = new Uint8Array(newMac1); + const arr2 = new Uint8Array(newMac2); for (let i = 0; i < arr2.length; i++) { if (arr1[i] !== arr2[i]) { return false; @@ -599,33 +528,28 @@ export class CryptoService implements CryptoServiceAbstraction { } private async getKeyForEncryption(key?: SymmetricCryptoKey): Promise { - if (key) { + if (key != null) { return key; } const encKey = await this.getEncKey(); - return encKey || (await this.getKey()); + if (encKey != null) { + return encKey; + } + + return 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); + if (this.legacyEtmKey == null) { + this.legacyEtmKey = new SymmetricCryptoKey(key.key, EncryptionType.AesCbc128_HmacSha256_B64); + } return this.legacyEtmKey; } return key; } - - private getRandomBytes(byteLength: number): string { - const bytes = new Uint32Array(byteLength / 4); - Crypto.getRandomValues(bytes); - const buffer = forge.util.createBuffer(); - for (let i = 0; i < bytes.length; i++) { - buffer.putInt32(bytes[i]); - } - return buffer.getBytes(); - } } diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index 3ab9db7a16..063d665d93 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -32,7 +32,7 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { hmac(value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { const nodeValue = this.toNodeBuffer(value); - const nodeKey = this.toNodeBuffer(value); + const nodeKey = this.toNodeBuffer(key); const hmac = crypto.createHmac(algorithm, nodeKey); hmac.update(nodeValue); return Promise.resolve(this.toArrayBuffer(hmac.digest())); diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 09b728c050..8674a277ca 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -238,7 +238,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr } const promises = history.map(async (item) => { - const decrypted = await this.cryptoService.decrypt(new CipherString(item.password)); + const decrypted = await this.cryptoService.decryptToUtf8(new CipherString(item.password)); return new PasswordHistory(decrypted, item.date); }); diff --git a/tsconfig.json b/tsconfig.json index 45449f582f..a745228386 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "pretty": true, "moduleResolution": "node", "target": "ES6", "module": "commonjs", From 5e7115f78d8a87b241a43fdcd6d53b67ccf0b605 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 21 Apr 2018 23:55:21 -0400 Subject: [PATCH 0197/1626] implement crypto functions in more places --- src/abstractions/crypto.service.ts | 1 - src/misc/utils.ts | 12 +++++++ src/services/audit.service.ts | 9 ++--- src/services/crypto.service.ts | 5 --- src/services/totp.service.ts | 40 +++++------------------ src/services/webCryptoFunction.service.ts | 2 +- 6 files changed, 26 insertions(+), 43 deletions(-) diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 473758ae6e..2b2adbda6e 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -30,5 +30,4 @@ export abstract class CryptoService { decrypt: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise; decryptToUtf8: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise; decryptFromBytes: (encBuf: ArrayBuffer, key: SymmetricCryptoKey) => Promise; - sha1: (password: string) => Promise; } diff --git a/src/misc/utils.ts b/src/misc/utils.ts index e02bd66f3f..bcb9f76c80 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -26,6 +26,18 @@ export class Utils { } } + static fromHexToArray(str: string): Uint8Array { + if (Utils.isNode) { + return new Uint8Array(Buffer.from(str, 'hex')); + } else { + const bytes = new Uint8Array(str.length / 2); + for (let i = 0; i < str.length; i += 2) { + bytes[i / 2] = parseInt(str.substr(i, 2), 16); + } + return bytes; + } + } + static fromUtf8ToArray(str: string): Uint8Array { if (Utils.isNode) { return new Uint8Array(Buffer.from(str, 'utf8')); diff --git a/src/services/audit.service.ts b/src/services/audit.service.ts index 57279a09da..d18ccbe219 100644 --- a/src/services/audit.service.ts +++ b/src/services/audit.service.ts @@ -1,19 +1,20 @@ import { AuditService as AuditServiceAbstraction } from '../abstractions/audit.service'; -import { CryptoService } from '../abstractions/crypto.service'; +import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; +import { Utils } from '../misc/utils'; const PwnedPasswordsApi = 'https://api.pwnedpasswords.com/range/'; export class AuditService implements AuditServiceAbstraction { - constructor(private cryptoService: CryptoService) { } + constructor(private cryptoFunctionService: CryptoFunctionService) { } async passwordLeaked(password: string): Promise { - const hash = (await this.cryptoService.sha1(password)).toUpperCase(); + const hashBytes = await this.cryptoFunctionService.hash(password, 'sha1'); + const hash = Utils.fromBufferToHex(hashBytes).toUpperCase(); const hashStart = hash.substr(0, 5); const hashEnding = hash.substr(5); const response = await fetch(PwnedPasswordsApi + hashStart); const leakedHashes = await response.text(); - const match = leakedHashes.split(/\r?\n/).find((v) => { return v.split(':')[0] === hashEnding; }); diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index e08094f7dd..ab68394959 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -355,11 +355,6 @@ export class CryptoService implements CryptoServiceAbstraction { macBytes != null ? macBytes.buffer : null, key); } - async sha1(password: string): Promise { - const hash = await this.cryptoFunctionService.hash(password, 'sha1'); - return Utils.fromBufferToHex(hash); - } - // Helpers private async aesEncrypt(plainValue: ArrayBuffer, key: SymmetricCryptoKey): Promise { diff --git a/src/services/totp.service.ts b/src/services/totp.service.ts index e52c11e48c..3a8fb3b896 100644 --- a/src/services/totp.service.ts +++ b/src/services/totp.service.ts @@ -1,23 +1,20 @@ import { ConstantsService } from './constants.service'; +import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; import { StorageService } from '../abstractions/storage.service'; import { TotpService as TotpServiceAbstraction } from '../abstractions/totp.service'; +import { Utils } from '../misc/utils'; + const b32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; -const TotpAlgorithm = { - name: 'HMAC', - hash: { name: 'SHA-1' }, -}; - export class TotpService implements TotpServiceAbstraction { - constructor(private storageService: StorageService) { - } + constructor(private storageService: StorageService, private cryptoFunctionService: CryptoFunctionService) {} async getCode(keyb32: string): Promise { const epoch = Math.round(new Date().getTime() / 1000.0); const timeHex = this.leftpad(this.dec2hex(Math.floor(epoch / 30)), 16, '0'); - const timeBytes = this.hex2bytes(timeHex); + const timeBytes = Utils.fromHexToArray(timeHex); const keyBytes = this.b32tobytes(keyb32); if (!keyBytes.length || !timeBytes.length) { @@ -57,26 +54,6 @@ export class TotpService implements TotpServiceAbstraction { return parseInt(s, 16); } - private hex2bytes(s: string): Uint8Array { - const bytes = new Uint8Array(s.length / 2); - for (let i = 0; i < s.length; i += 2) { - bytes[i / 2] = parseInt(s.substr(i, 2), 16); - } - return bytes; - } - - private buff2hex(buff: ArrayBuffer): string { - const bytes = new Uint8Array(buff); - const hex: string[] = []; - bytes.forEach((b) => { - // tslint:disable-next-line - hex.push((b >>> 4).toString(16)); - // tslint:disable-next-line - hex.push((b & 0xF).toString(16)); - }); - return hex.join(''); - } - private b32tohex(s: string): string { s = s.toUpperCase(); let cleanedInput = ''; @@ -107,12 +84,11 @@ export class TotpService implements TotpServiceAbstraction { } private b32tobytes(s: string): Uint8Array { - return this.hex2bytes(this.b32tohex(s)); + return Utils.fromHexToArray(this.b32tohex(s)); } private async sign(keyBytes: Uint8Array, timeBytes: Uint8Array) { - const key = await window.crypto.subtle.importKey('raw', keyBytes, TotpAlgorithm, false, ['sign']); - const signature = await window.crypto.subtle.sign(TotpAlgorithm, key, timeBytes); - return this.buff2hex(signature); + const signature = await this.cryptoFunctionService.hmac(timeBytes.buffer, keyBytes.buffer, 'sha1'); + return Utils.fromBufferToHex(signature); } } diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index 25efa06d0e..dd4a33c62b 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -38,7 +38,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { }; const impKey = await this.subtle.importKey('raw', passwordBuf, { name: 'PBKDF2' }, false, ['deriveBits']); - return await window.crypto.subtle.deriveBits(pbkdf2Params, impKey, wcLen); + return await this.subtle.deriveBits(pbkdf2Params, impKey, wcLen); } async hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { From 0fa9fc58eb82c98fedc729eef30521c88e105520 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 23 Apr 2018 13:03:47 -0400 Subject: [PATCH 0198/1626] refactor utils service to utils --- spec/common/misc/utils.spec.ts | 34 ++++ spec/web/services/utils.service.spec.ts | 34 ---- src/abstractions/crypto.service.ts | 1 + src/abstractions/index.ts | 1 - .../passwordGeneration.service.ts | 2 +- src/abstractions/utils.service.ts | 5 - .../password-generator.component.ts | 6 +- src/misc/utils.ts | 62 ++++++ src/models/view/loginUriView.ts | 4 +- src/services/appId.service.ts | 4 +- src/services/cipher.service.ts | 9 +- src/services/crypto.service.ts | 36 ++++ src/services/index.ts | 1 - src/services/passwordGeneration.service.ts | 36 ++-- src/services/token.service.ts | 5 +- src/services/utils.service.ts | 178 ------------------ 16 files changed, 168 insertions(+), 250 deletions(-) create mode 100644 spec/common/misc/utils.spec.ts delete mode 100644 spec/web/services/utils.service.spec.ts delete mode 100644 src/abstractions/utils.service.ts delete mode 100644 src/services/utils.service.ts diff --git a/spec/common/misc/utils.spec.ts b/spec/common/misc/utils.spec.ts new file mode 100644 index 0000000000..28fcbd2398 --- /dev/null +++ b/spec/common/misc/utils.spec.ts @@ -0,0 +1,34 @@ +import { Utils } from '../../../src/misc/utils'; + +describe('Utils Service', () => { + describe('getHostname', () => { + it('should fail for invalid urls', () => { + expect(Utils.getHostname(null)).toBeNull(); + expect(Utils.getHostname(undefined)).toBeNull(); + expect(Utils.getHostname(' ')).toBeNull(); + expect(Utils.getHostname('https://bit!:"_&ward.com')).toBeNull(); + expect(Utils.getHostname('bitwarden')).toBeNull(); + }); + + it('should handle valid urls', () => { + expect(Utils.getHostname('bitwarden.com')).toBe('bitwarden.com'); + expect(Utils.getHostname('https://bitwarden.com')).toBe('bitwarden.com'); + expect(Utils.getHostname('http://bitwarden.com')).toBe('bitwarden.com'); + expect(Utils.getHostname('http://vault.bitwarden.com')).toBe('vault.bitwarden.com'); + expect(Utils.getHostname('https://user:password@bitwarden.com:8080/password/sites?and&query#hash')) + .toBe('bitwarden.com'); + }); + + it('should support localhost and IP', () => { + expect(Utils.getHostname('https://localhost')).toBe('localhost'); + expect(Utils.getHostname('https://192.168.1.1')).toBe('192.168.1.1'); + }); + }); + + describe('newGuid', () => { + it('should create a valid guid', () => { + const validGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + expect(Utils.newGuid()).toMatch(validGuid); + }); + }); +}); diff --git a/spec/web/services/utils.service.spec.ts b/spec/web/services/utils.service.spec.ts deleted file mode 100644 index 1241674bc1..0000000000 --- a/spec/web/services/utils.service.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { UtilsService } from '../../../src/services/utils.service'; - -describe('Utils Service', () => { - describe('getHostname', () => { - it('should fail for invalid urls', () => { - expect(UtilsService.getHostname(null)).toBeNull(); - expect(UtilsService.getHostname(undefined)).toBeNull(); - expect(UtilsService.getHostname(' ')).toBeNull(); - expect(UtilsService.getHostname('https://bit!:"_&ward.com')).toBeNull(); - expect(UtilsService.getHostname('bitwarden')).toBeNull(); - }); - - it('should handle valid urls', () => { - expect(UtilsService.getHostname('bitwarden.com')).toBe('bitwarden.com'); - expect(UtilsService.getHostname('https://bitwarden.com')).toBe('bitwarden.com'); - expect(UtilsService.getHostname('http://bitwarden.com')).toBe('bitwarden.com'); - expect(UtilsService.getHostname('http://vault.bitwarden.com')).toBe('vault.bitwarden.com'); - expect(UtilsService.getHostname('https://user:password@bitwarden.com:8080/password/sites?and&query#hash')) - .toBe('bitwarden.com'); - }); - - it('should support localhost and IP', () => { - expect(UtilsService.getHostname('https://localhost')).toBe('localhost'); - expect(UtilsService.getHostname('https://192.168.1.1')).toBe('192.168.1.1'); - }); - }); - - describe('newGuid', () => { - it('should create a valid guid', () => { - const validGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - expect(UtilsService.newGuid()).toMatch(validGuid); - }); - }); -}); diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 2b2adbda6e..d2bb86d1eb 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -30,4 +30,5 @@ export abstract class CryptoService { decrypt: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise; decryptToUtf8: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise; decryptFromBytes: (encBuf: ArrayBuffer, key: SymmetricCryptoKey) => Promise; + randomNumber: (min: number, max: number) => Promise; } diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index a727ee11a8..c9d10b5e58 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -21,4 +21,3 @@ export { SyncService } from './sync.service'; export { TokenService } from './token.service'; export { TotpService } from './totp.service'; export { UserService } from './user.service'; -export { UtilsService } from './utils.service'; diff --git a/src/abstractions/passwordGeneration.service.ts b/src/abstractions/passwordGeneration.service.ts index a8967db281..44f7cee32d 100644 --- a/src/abstractions/passwordGeneration.service.ts +++ b/src/abstractions/passwordGeneration.service.ts @@ -1,7 +1,7 @@ import { PasswordHistory } from '../models/domain/passwordHistory'; export abstract class PasswordGenerationService { - generatePassword: (options: any) => string; + generatePassword: (options: any) => Promise; getOptions: () => any; saveOptions: (options: any) => Promise; getHistory: () => Promise; diff --git a/src/abstractions/utils.service.ts b/src/abstractions/utils.service.ts deleted file mode 100644 index 9dd87774b9..0000000000 --- a/src/abstractions/utils.service.ts +++ /dev/null @@ -1,5 +0,0 @@ -export abstract class UtilsService { - copyToClipboard: (text: string, doc?: Document) => void; - getHostname: (uriString: string) => string; - getHost: (uriString: string) => string; -} diff --git a/src/angular/components/password-generator.component.ts b/src/angular/components/password-generator.component.ts index e4a35c8445..12f6267023 100644 --- a/src/angular/components/password-generator.component.ts +++ b/src/angular/components/password-generator.component.ts @@ -28,7 +28,7 @@ export class PasswordGeneratorComponent implements OnInit { async ngOnInit() { this.options = await this.passwordGenerationService.getOptions(); this.avoidAmbiguous = !this.options.ambiguous; - this.password = this.passwordGenerationService.generatePassword(this.options); + this.password = await this.passwordGenerationService.generatePassword(this.options); this.analytics.eventTrack.next({ action: 'Generated Password' }); await this.passwordGenerationService.addHistory(this.password); } @@ -41,7 +41,7 @@ export class PasswordGeneratorComponent implements OnInit { async sliderInput() { this.normalizeOptions(); - this.password = this.passwordGenerationService.generatePassword(this.options); + this.password = await this.passwordGenerationService.generatePassword(this.options); } async saveOptions(regenerate: boolean = true) { @@ -54,7 +54,7 @@ export class PasswordGeneratorComponent implements OnInit { } async regenerate() { - this.password = this.passwordGenerationService.generatePassword(this.options); + this.password = await this.passwordGenerationService.generatePassword(this.options); await this.passwordGenerationService.addHistory(this.password); this.analytics.eventTrack.next({ action: 'Regenerated Password' }); } diff --git a/src/misc/utils.ts b/src/misc/utils.ts index bcb9f76c80..bf68d929c9 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -95,6 +95,68 @@ export class Utils { return Array.prototype.map.call(bytes, (x: number) => ('00' + x.toString(16)).slice(-2)).join(''); } } + + static urlBase64Decode(str: string): string { + let output = str.replace(/-/g, '+').replace(/_/g, '/'); + switch (output.length % 4) { + case 0: + break; + case 2: + output += '=='; + break; + case 3: + output += '='; + break; + default: + throw new Error('Illegal base64url string!'); + } + + return decodeURIComponent(escape(window.atob(output))); + } + + // ref: http://stackoverflow.com/a/2117523/1090359 + static newGuid(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + // tslint:disable-next-line + const r = Math.random() * 16 | 0; + // tslint:disable-next-line + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } + + static getHostname(uriString: string): string { + const url = Utils.getUrl(uriString); + return url != null ? url.hostname : null; + } + + static getHost(uriString: string): string { + const url = Utils.getUrl(uriString); + return url != null ? url.host : null; + } + + private static getUrl(uriString: string): URL { + if (uriString == null) { + return null; + } + + uriString = uriString.trim(); + if (uriString === '') { + return null; + } + + if (uriString.indexOf('://') === -1 && uriString.indexOf('.') > -1) { + uriString = 'http://' + uriString; + } + + if (uriString.startsWith('http://') || uriString.startsWith('https://')) { + try { + return new URL(uriString); + } catch (e) { } + } + + return null; + } } Utils.init(); diff --git a/src/models/view/loginUriView.ts b/src/models/view/loginUriView.ts index 6e221e02d3..0c78bb50c8 100644 --- a/src/models/view/loginUriView.ts +++ b/src/models/view/loginUriView.ts @@ -6,7 +6,7 @@ import { LoginUri } from '../domain/loginUri'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; -import { UtilsService } from '../../services/utils.service'; +import { Utils } from '../../misc/utils'; export class LoginUriView implements View { match: UriMatchType = null; @@ -52,7 +52,7 @@ export class LoginUriView implements View { get hostname(): string { if (this._hostname == null && this.uri != null) { - this._hostname = UtilsService.getHostname(this.uri); + this._hostname = Utils.getHostname(this.uri); if (this._hostname === '') { this._hostname = null; } diff --git a/src/services/appId.service.ts b/src/services/appId.service.ts index ba60a92551..c37668d192 100644 --- a/src/services/appId.service.ts +++ b/src/services/appId.service.ts @@ -1,4 +1,4 @@ -import { UtilsService } from './utils.service'; +import { Utils } from '../misc/utils'; import { AppIdService as AppIdServiceAbstraction } from '../abstractions/appId.service'; import { StorageService } from '../abstractions/storage.service'; @@ -21,7 +21,7 @@ export class AppIdService implements AppIdServiceAbstraction { return existingId; } - const guid = UtilsService.newGuid(); + const guid = Utils.newGuid(); await this.storageService.save(key, guid); return guid; } diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 5f7dc33a27..e2be81f25f 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -36,7 +36,8 @@ import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { SettingsService } from '../abstractions/settings.service'; import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; -import { UtilsService } from '../abstractions/utils.service'; + +import { Utils } from '../misc/utils'; const Keys = { ciphersPrefix: 'ciphers_', @@ -50,7 +51,7 @@ export class CipherService implements CipherServiceAbstraction { constructor(private cryptoService: CryptoService, private userService: UserService, private settingsService: SettingsService, private apiService: ApiService, private storageService: StorageService, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, private utilsService: UtilsService) { + private platformUtilsService: PlatformUtilsService) { } clearCache(): void { @@ -221,8 +222,8 @@ export class CipherService implements CipherServiceAbstraction { } break; case UriMatchType.Host: - const urlHost = this.utilsService.getHost(url); - if (urlHost != null && urlHost === this.utilsService.getHost(u.uri)) { + const urlHost = Utils.getHost(url); + if (urlHost != null && urlHost === Utils.getHost(u.uri)) { return true; } break; diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index ab68394959..3055953df3 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -355,6 +355,42 @@ export class CryptoService implements CryptoServiceAbstraction { macBytes != null ? macBytes.buffer : null, key); } + // EFForg/OpenWireless + // ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js + async randomNumber(min: number, max: number): Promise { + let rval = 0; + const range = max - min + 1; + const bitsNeeded = Math.ceil(Math.log2(range)); + if (bitsNeeded > 53) { + throw new Error('We cannot generate numbers larger than 53 bits.'); + } + + const bytesNeeded = Math.ceil(bitsNeeded / 8); + const mask = Math.pow(2, bitsNeeded) - 1; + // 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111 + + // Fill a byte array with N random numbers + const byteArray = new Uint8Array(await this.cryptoFunctionService.randomBytes(bytesNeeded)); + + let p = (bytesNeeded - 1) * 8; + for (let i = 0; i < bytesNeeded; i++) { + rval += byteArray[i] * Math.pow(2, p); + p -= 8; + } + + // Use & to apply the mask and reduce the number of recursive lookups + // tslint:disable-next-line + rval = rval & mask; + + if (rval >= range) { + // Integer out of acceptable range + return this.randomNumber(min, max); + } + + // Return an integer that falls within the range + return min + rval; + } + // Helpers private async aesEncrypt(plainValue: ArrayBuffer, key: SymmetricCryptoKey): Promise { diff --git a/src/services/index.ts b/src/services/index.ts index bddd1809b8..bd096fdc10 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -19,4 +19,3 @@ export { SyncService } from './sync.service'; export { TokenService } from './token.service'; export { TotpService } from './totp.service'; export { UserService } from './user.service'; -export { UtilsService } from './utils.service'; diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 8674a277ca..b923e5eb76 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -1,8 +1,6 @@ import { CipherString } from '../models/domain/cipherString'; import { PasswordHistory } from '../models/domain/passwordHistory'; -import { UtilsService } from './utils.service'; - import { CryptoService } from '../abstractions/crypto.service'; import { PasswordGenerationService as PasswordGenerationServiceAbstraction, @@ -30,7 +28,12 @@ const Keys = { const MaxPasswordsInHistory = 100; export class PasswordGenerationService implements PasswordGenerationServiceAbstraction { - static generatePassword(options: any): string { + private optionsCache: any; + private history: PasswordHistory[]; + + constructor(private cryptoService: CryptoService, private storageService: StorageService) { } + + async generatePassword(options: any): Promise { // overload defaults with given options const o = Object.assign({}, DefaultOptions, options); @@ -83,9 +86,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr } // shuffle - positions.sort(() => { - return UtilsService.secureRandomNumber(0, 1) * 2 - 1; - }); + await this.shuffleArray(positions); // build out the char sets let allCharSet = ''; @@ -138,25 +139,18 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr case 'a': positionChars = allCharSet; break; + default: + console.log('unknown position at ' + i + ': ' + positions[i]); + break; } - const randomCharIndex = UtilsService.secureRandomNumber(0, positionChars.length - 1); + const randomCharIndex = await this.cryptoService.randomNumber(0, positionChars.length - 1); password += positionChars.charAt(randomCharIndex); } return password; } - private optionsCache: any; - private history: PasswordHistory[]; - - constructor(private cryptoService: CryptoService, private storageService: StorageService) { - } - - generatePassword(options: any) { - return PasswordGenerationService.generatePassword(options); - } - async getOptions() { if (this.optionsCache == null) { const options = await this.storageService.get(Keys.options); @@ -252,4 +246,12 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return history[history.length - 1].password === password; } + + // ref: https://stackoverflow.com/a/12646864/1090359 + private async shuffleArray(array: string[]) { + for (let i = array.length - 1; i > 0; i--) { + const j = await this.cryptoService.randomNumber(0, i); + [array[i], array[j]] = [array[j], array[i]]; + } + } } diff --git a/src/services/token.service.ts b/src/services/token.service.ts index 94dbfa10fd..7b9807e00d 100644 --- a/src/services/token.service.ts +++ b/src/services/token.service.ts @@ -1,9 +1,10 @@ import { ConstantsService } from './constants.service'; -import { UtilsService } from './utils.service'; import { StorageService } from '../abstractions/storage.service'; import { TokenService as TokenServiceAbstraction } from '../abstractions/token.service'; +import { Utils } from '../misc/utils'; + const Keys = { accessToken: 'accessToken', refreshToken: 'refreshToken', @@ -94,7 +95,7 @@ export class TokenService implements TokenServiceAbstraction { throw new Error('JWT must have 3 parts'); } - const decoded = UtilsService.urlBase64Decode(parts[1]); + const decoded = Utils.urlBase64Decode(parts[1]); if (decoded == null) { throw new Error('Cannot decode the token'); } diff --git a/src/services/utils.service.ts b/src/services/utils.service.ts deleted file mode 100644 index 390d710924..0000000000 --- a/src/services/utils.service.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { UtilsService as UtilsServiceAbstraction } from '../abstractions/utils.service'; - -export class UtilsService implements UtilsServiceAbstraction { - static copyToClipboard(text: string, doc?: Document): void { - doc = doc || document; - if ((window as any).clipboardData && (window as any).clipboardData.setData) { - // IE specific code path to prevent textarea being shown while dialog is visible. - (window as any).clipboardData.setData('Text', text); - } else if (doc.queryCommandSupported && doc.queryCommandSupported('copy')) { - const textarea = doc.createElement('textarea'); - textarea.textContent = text; - // Prevent scrolling to bottom of page in MS Edge. - textarea.style.position = 'fixed'; - doc.body.appendChild(textarea); - textarea.select(); - - try { - // Security exception may be thrown by some browsers. - doc.execCommand('copy'); - } catch (e) { - // tslint:disable-next-line - console.warn('Copy to clipboard failed.', e); - } finally { - doc.body.removeChild(textarea); - } - } - } - - static urlBase64Decode(str: string): string { - let output = str.replace(/-/g, '+').replace(/_/g, '/'); - switch (output.length % 4) { - case 0: - break; - case 2: - output += '=='; - break; - case 3: - output += '='; - break; - default: - throw new Error('Illegal base64url string!'); - } - - return decodeURIComponent(escape(window.atob(output))); - } - - // ref: http://stackoverflow.com/a/2117523/1090359 - static newGuid(): string { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { - // tslint:disable-next-line - const r = Math.random() * 16 | 0; - // tslint:disable-next-line - const v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); - } - - // EFForg/OpenWireless - // ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js - static secureRandomNumber(min: number, max: number): number { - let rval = 0; - const range = max - min + 1; - const bitsNeeded = Math.ceil(Math.log2(range)); - if (bitsNeeded > 53) { - throw new Error('We cannot generate numbers larger than 53 bits.'); - } - - const bytesNeeded = Math.ceil(bitsNeeded / 8); - const mask = Math.pow(2, bitsNeeded) - 1; - // 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111 - - // Create byte array and fill with N random numbers - const byteArray = new Uint8Array(bytesNeeded); - window.crypto.getRandomValues(byteArray); - - let p = (bytesNeeded - 1) * 8; - for (let i = 0; i < bytesNeeded; i++) { - rval += byteArray[i] * Math.pow(2, p); - p -= 8; - } - - // Use & to apply the mask and reduce the number of recursive lookups - // tslint:disable-next-line - rval = rval & mask; - - if (rval >= range) { - // Integer out of acceptable range - return UtilsService.secureRandomNumber(min, max); - } - - // Return an integer that falls within the range - return min + rval; - } - - static fromB64ToArray(str: string): Uint8Array { - const binaryString = window.atob(str); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes; - } - - static fromUtf8ToArray(str: string): Uint8Array { - const strUtf8 = unescape(encodeURIComponent(str)); - const arr = new Uint8Array(strUtf8.length); - for (let i = 0; i < strUtf8.length; i++) { - arr[i] = strUtf8.charCodeAt(i); - } - return arr; - } - - static fromBufferToB64(buffer: ArrayBuffer): string { - let binary = ''; - const bytes = new Uint8Array(buffer); - for (let i = 0; i < bytes.byteLength; i++) { - binary += String.fromCharCode(bytes[i]); - } - return window.btoa(binary); - } - - static fromBufferToUtf8(buffer: ArrayBuffer): string { - const bytes = new Uint8Array(buffer); - const encodedString = String.fromCharCode.apply(null, bytes); - return decodeURIComponent(escape(encodedString)); - } - - // ref: https://stackoverflow.com/a/40031979/1090359 - static fromBufferToHex(buffer: ArrayBuffer): string { - const bytes = new Uint8Array(buffer); - return Array.prototype.map.call(bytes, (x: number) => ('00' + x.toString(16)).slice(-2)).join(''); - } - - static getHostname(uriString: string): string { - const url = UtilsService.getUrl(uriString); - return url != null ? url.hostname : null; - } - - static getHost(uriString: string): string { - const url = UtilsService.getUrl(uriString); - return url != null ? url.host : null; - } - - private static getUrl(uriString: string): URL { - if (uriString == null) { - return null; - } - - uriString = uriString.trim(); - if (uriString === '') { - return null; - } - - if (uriString.indexOf('://') === -1 && uriString.indexOf('.') > -1) { - uriString = 'http://' + uriString; - } - - if (uriString.startsWith('http://') || uriString.startsWith('https://')) { - try { - return new URL(uriString); - } catch (e) { } - } - - return null; - } - - getHostname(uriString: string): string { - return UtilsService.getHostname(uriString); - } - - getHost(uriString: string): string { - return UtilsService.getHost(uriString); - } - - copyToClipboard(text: string, doc?: Document) { - UtilsService.copyToClipboard(text, doc); - } -} From e5ae1cbb1c1bc1141caad91efc6516f232d17411 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 23 Apr 2018 13:05:05 -0400 Subject: [PATCH 0199/1626] remove comment --- src/services/passwordGeneration.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index b923e5eb76..f39acb59cf 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -140,7 +140,6 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr positionChars = allCharSet; break; default: - console.log('unknown position at ' + i + ': ' + positions[i]); break; } From 768ab7dd0afe303e33609118a78674e19a6b4132 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 23 Apr 2018 13:45:40 -0400 Subject: [PATCH 0200/1626] b64 to utf8 utils --- src/misc/utils.ts | 14 +++++++++++--- src/services/token.service.ts | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/misc/utils.ts b/src/misc/utils.ts index bf68d929c9..d91b64209c 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -96,8 +96,8 @@ export class Utils { } } - static urlBase64Decode(str: string): string { - let output = str.replace(/-/g, '+').replace(/_/g, '/'); + static fromUrlB64ToUtf8(b64Str: string): string { + let output = b64Str.replace(/-/g, '+').replace(/_/g, '/'); switch (output.length % 4) { case 0: break; @@ -111,7 +111,15 @@ export class Utils { throw new Error('Illegal base64url string!'); } - return decodeURIComponent(escape(window.atob(output))); + return Utils.fromB64ToUtf8(output); + } + + static fromB64ToUtf8(b64Str: string): string { + if (Utils.isNode) { + return new Buffer(b64Str, 'base64').toString('utf8'); + } else { + return decodeURIComponent(escape(window.atob(b64Str))); + } } // ref: http://stackoverflow.com/a/2117523/1090359 diff --git a/src/services/token.service.ts b/src/services/token.service.ts index 7b9807e00d..c572621103 100644 --- a/src/services/token.service.ts +++ b/src/services/token.service.ts @@ -95,7 +95,7 @@ export class TokenService implements TokenServiceAbstraction { throw new Error('JWT must have 3 parts'); } - const decoded = Utils.urlBase64Decode(parts[1]); + const decoded = Utils.fromUrlB64ToUtf8(parts[1]); if (decoded == null) { throw new Error('Cannot decode the token'); } From 5d3b99ce6f93c3c43adc9e3eb784bc00b728a7f2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 24 Apr 2018 16:00:20 -0400 Subject: [PATCH 0201/1626] move common electron code to jslib --- package-lock.json | 815 +++++++++++++++++- package.json | 3 + src/electron/services/electronLog.service.ts | 64 ++ .../services/electronPlatformUtils.service.ts | 155 ++++ .../electronRendererSecureStorage.service.ts | 30 + .../services/electronStorage.service.ts | 35 + src/electron/utils.ts | 27 + src/electron/window.main.ts | 223 +++++ src/misc/analytics.ts | 14 + 9 files changed, 1353 insertions(+), 13 deletions(-) create mode 100644 src/electron/services/electronLog.service.ts create mode 100644 src/electron/services/electronPlatformUtils.service.ts create mode 100644 src/electron/services/electronRendererSecureStorage.service.ts create mode 100644 src/electron/services/electronStorage.service.ts create mode 100644 src/electron/utils.ts create mode 100644 src/electron/window.main.ts diff --git a/package-lock.json b/package-lock.json index 0517016707..1b8943e9df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -148,6 +148,18 @@ "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", "dev": true }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.1.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, "align-text": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", @@ -296,6 +308,12 @@ "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco=", "dev": true }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true + }, "asn1.js": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", @@ -324,6 +342,12 @@ "util": "0.10.3" } }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -342,12 +366,30 @@ "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, "atob": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.0.tgz", "integrity": "sha512-SuiKH8vbsOyCALjA/+EINmt/Kdl+TQPrtFgW7XZZcwtryFu9e5kQoX3bjCW6mIvGH1fbeAZZuvwGR5IlBRznGw==", "dev": true }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", + "dev": true + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -552,6 +594,16 @@ "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=", "dev": true }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, "better-assert": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", @@ -603,6 +655,15 @@ "type-is": "1.6.16" } }, + "boom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "dev": true, + "requires": { + "hoek": "4.2.1" + } + }, "boxen": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", @@ -900,6 +961,12 @@ "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", "dev": true }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, "center-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", @@ -1053,6 +1120,18 @@ "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", "dev": true }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -1119,6 +1198,15 @@ } } }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, "commander": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", @@ -1154,6 +1242,25 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.6", + "typedarray": "0.0.6" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, "concurrently": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-3.5.1.tgz", @@ -1232,6 +1339,18 @@ } } }, + "conf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-1.4.0.tgz", + "integrity": "sha512-bzlVWS2THbMetHqXKB8ypsXN4DQ/1qopGwNJi1eYbpwesJcd86FBjFciCQX/YwAhp9bM7NVnPFqZ5LpV7gP0Dg==", + "requires": { + "dot-prop": "4.2.0", + "env-paths": "1.0.0", + "make-dir": "1.2.0", + "pkg-up": "2.0.0", + "write-file-atomic": "2.3.0" + } + }, "configstore": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", @@ -1381,6 +1500,26 @@ "which": "1.3.0" } }, + "cryptiles": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "dev": true, + "requires": { + "boom": "5.2.0" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "dev": true, + "requires": { + "hoek": "4.2.1" + } + } + } + }, "crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", @@ -1429,6 +1568,15 @@ "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", "dev": true }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + } + }, "date-fns": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.29.0.tgz", @@ -1561,6 +1709,12 @@ } } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -1639,7 +1793,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", - "dev": true, "requires": { "is-obj": "1.0.1" } @@ -1656,12 +1809,85 @@ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", "dev": true }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", "dev": true }, + "electron": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/electron/-/electron-1.8.4.tgz", + "integrity": "sha512-2f1cx0G3riMFODXFftF5AHXy+oHfhpntZHTDN66Hxtl09gmEr42B3piNEod9MEmw72f75LX2JfeYceqq1PF8cA==", + "dev": true, + "requires": { + "@types/node": "8.10.9", + "electron-download": "3.3.0", + "extract-zip": "1.6.6" + }, + "dependencies": { + "@types/node": { + "version": "8.10.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.9.tgz", + "integrity": "sha512-GUUTbeDaJSRaoLkqVQ5jwwKbDiLWFX3JrKLvC078q2P51Z9Dcb5F5UdnApSYqdMk4X0VrKod1gzeoX8bGl8DMg==", + "dev": true + } + } + }, + "electron-download": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-3.3.0.tgz", + "integrity": "sha1-LP1U1pZsAZxNSa1l++Zcyc3vaMg=", + "dev": true, + "requires": { + "debug": "2.6.9", + "fs-extra": "0.30.0", + "home-path": "1.0.5", + "minimist": "1.2.0", + "nugget": "2.0.1", + "path-exists": "2.1.0", + "rc": "1.2.6", + "semver": "5.5.0", + "sumchecker": "1.3.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + } + } + }, + "electron-log": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.14.tgz", + "integrity": "sha512-Rj+XyK4nShe/nv9v1Uks4KEfjtQ6N+eSnx5CLpAjG6rlyUdAflyFHoybcHSLoq9l9pGavclULWS5IXgk8umc2g==" + }, + "electron-store": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-1.3.0.tgz", + "integrity": "sha512-r1Pdl5MwpiCxgbsl0qnwv/GABO5+J/JTO16+KyqL+bOITIk9o3cq3Sw69uO9NgPkpfcKeEwxtJFbtbiBlGTiDA==", + "requires": { + "conf": "1.4.0" + } + }, "elliptic": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", @@ -1785,6 +2011,11 @@ "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", "dev": true }, + "env-paths": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz", + "integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=" + }, "error-ex": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", @@ -1803,6 +2034,12 @@ "stackframe": "1.0.4" } }, + "es6-promise": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", + "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==", + "dev": true + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2010,12 +2247,68 @@ "is-extglob": "1.0.0" } }, + "extract-zip": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.6.tgz", + "integrity": "sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw=", + "dev": true, + "requires": { + "concat-stream": "1.6.0", + "debug": "2.6.9", + "mkdirp": "0.5.0", + "yauzl": "2.4.1" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", + "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "requires": { + "pend": "1.2.0" + } + }, "filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", @@ -2183,6 +2476,23 @@ "for-in": "1.0.2" } }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "dev": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.18" + } + }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -2207,6 +2517,27 @@ "null-check": "1.0.0" } }, + "fs-extra": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", + "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "2.4.0", + "klaw": "1.3.1", + "path-is-absolute": "1.0.1", + "rimraf": "2.6.2" + }, + "dependencies": { + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + } + } + }, "fs.realpath": { "version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", @@ -3134,6 +3465,15 @@ "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", "dev": true }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "1.0.0" + } + }, "glob": { "version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", @@ -3203,8 +3543,7 @@ "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" }, "handlebars": { "version": "4.0.11", @@ -3218,6 +3557,22 @@ "uglify-js": "2.8.29" } }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "dev": true, + "requires": { + "ajv": "5.5.2", + "har-schema": "2.0.0" + } + }, "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", @@ -3352,6 +3707,18 @@ } } }, + "hawk": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "dev": true, + "requires": { + "boom": "4.3.1", + "cryptiles": "3.1.2", + "hoek": "4.2.1", + "sntp": "2.1.0" + } + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -3363,6 +3730,18 @@ "minimalistic-crypto-utils": "1.0.1" } }, + "hoek": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", + "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", + "dev": true + }, + "home-path": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/home-path/-/home-path-1.0.5.tgz", + "integrity": "sha1-eIspgVsS1Tus9XVkhHbm+QQdEz8=", + "dev": true + }, "hosted-git-info": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", @@ -3399,6 +3778,17 @@ "requires-port": "1.0.0" } }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.14.1" + } + }, "https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", @@ -3432,8 +3822,7 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" }, "indent-string": { "version": "2.1.0", @@ -3651,8 +4040,7 @@ "is-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" }, "is-odd": { "version": "2.0.0", @@ -3727,6 +4115,12 @@ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "dev": true }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", @@ -3766,6 +4160,12 @@ "isarray": "1.0.0" } }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, "istanbul": { "version": "0.4.5", "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", @@ -4238,12 +4638,31 @@ "esprima": "4.0.0" } }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, "jsesc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", "dev": true }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -4256,6 +4675,27 @@ "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", "dev": true }, + "jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, "karma": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/karma/-/karma-1.7.1.tgz", @@ -4733,6 +5173,15 @@ "is-buffer": "1.1.6" } }, + "klaw": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", + "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, "latest-version": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", @@ -4772,6 +5221,22 @@ "strip-bom": "2.0.0" } }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + } + } + }, "lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", @@ -4876,7 +5341,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.2.0.tgz", "integrity": "sha512-aNUAa4UMg/UougV25bbrU4ZaaKNjJ/3/xnvg/twpmKROPdKZPZ9wGgI0opdZzO8q/zUFawoUuixuOv33eZ61Iw==", - "dev": true, "requires": { "pify": "3.0.0" }, @@ -4884,8 +5348,7 @@ "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" } } }, @@ -5667,6 +6130,29 @@ "path-key": "2.0.1" } }, + "nugget": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nugget/-/nugget-2.0.1.tgz", + "integrity": "sha1-IBCVpIfhrTYIGzQy+jytpPjQcbA=", + "dev": true, + "requires": { + "debug": "2.6.9", + "minimist": "1.2.0", + "pretty-bytes": "1.0.4", + "progress-stream": "1.2.0", + "request": "2.85.0", + "single-line-log": "1.1.2", + "throttleit": "0.0.2" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, "null-check": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz", @@ -5679,6 +6165,12 @@ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true + }, "object-assign": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", @@ -5713,6 +6205,12 @@ } } }, + "object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", + "dev": true + }, "object-visit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", @@ -5830,6 +6328,27 @@ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", "dev": true }, + "p-limit": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", + "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", + "requires": { + "p-try": "1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "1.2.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, "package-json": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", @@ -6009,6 +6528,18 @@ "sha.js": "2.4.11" } }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -6030,6 +6561,24 @@ "pinkie": "2.0.4" } }, + "pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", + "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=", + "requires": { + "find-up": "2.1.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "2.0.0" + } + } + } + }, "plugin-error": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", @@ -6085,6 +6634,16 @@ "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", "dev": true }, + "pretty-bytes": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz", + "integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=", + "dev": true, + "requires": { + "get-stdin": "4.0.1", + "meow": "3.7.0" + } + }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -6097,6 +6656,67 @@ "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true }, + "progress-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-1.2.0.tgz", + "integrity": "sha1-LNPP6jO6OonJwSHsM0er6asSX3c=", + "dev": true, + "requires": { + "speedometer": "0.1.4", + "through2": "0.2.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz", + "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=", + "dev": true, + "requires": { + "readable-stream": "1.1.14", + "xtend": "2.1.2" + } + }, + "xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", + "dev": true, + "requires": { + "object-keys": "0.4.0" + } + } + } + }, "ps-tree": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.1.0.tgz", @@ -6538,6 +7158,36 @@ "is-finite": "1.0.2" } }, + "request": { + "version": "2.85.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", + "integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", + "dev": true, + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.7.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.2", + "har-validator": "5.0.3", + "hawk": "6.0.2", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.1", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.2.1" + } + }, "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -6731,8 +7381,38 @@ "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "single-line-log": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-1.1.2.tgz", + "integrity": "sha1-wvg/Jzo+GhbtsJlWYdoO1e8DM2Q=", + "dev": true, + "requires": { + "string-width": "1.0.2" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } }, "snapdragon": { "version": "0.8.2", @@ -6848,6 +7528,15 @@ "kind-of": "3.2.2" } }, + "sntp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", + "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "dev": true, + "requires": { + "hoek": "4.2.1" + } + }, "socket.io": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.7.3.tgz", @@ -7050,6 +7739,12 @@ "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", "dev": true }, + "speedometer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-0.1.4.tgz", + "integrity": "sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0=", + "dev": true + }, "split": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", @@ -7095,6 +7790,22 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "sshpk": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", + "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", + "dev": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + } + }, "stackframe": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.0.4.tgz", @@ -7262,6 +7973,12 @@ "safe-buffer": "5.1.1" } }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -7301,6 +8018,16 @@ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true }, + "sumchecker": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-1.3.1.tgz", + "integrity": "sha1-ebs7RFbdBPGOvbwNcDodHa7FEF0=", + "dev": true, + "requires": { + "debug": "2.6.9", + "es6-promise": "4.2.4" + } + }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -7321,6 +8048,12 @@ "execa": "0.7.0" } }, + "throttleit": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz", + "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8=", + "dev": true + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -7496,6 +8229,15 @@ } } }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "dev": true, + "requires": { + "punycode": "1.4.1" + } + }, "tree-kill": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.0.tgz", @@ -7821,6 +8563,22 @@ "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", "dev": true }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -7840,6 +8598,12 @@ "mime-types": "2.1.18" } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, "typemoq": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/typemoq/-/typemoq-2.1.0.tgz", @@ -8158,6 +8922,12 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", "dev": true }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "dev": true + }, "validate-npm-package-license": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", @@ -8168,6 +8938,17 @@ "spdx-expression-parse": "3.0.0" } }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + }, "vm-browserify": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", @@ -8232,7 +9013,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", - "dev": true, "requires": { "graceful-fs": "4.1.11", "imurmurhash": "0.1.4", @@ -8292,6 +9072,15 @@ "window-size": "0.1.0" } }, + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true, + "requires": { + "fd-slicer": "1.0.1" + } + }, "yeast": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", diff --git a/package.json b/package.json index ea2447ebee..4a6653d9ff 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@types/papaparse": "4.1.31", "@types/webcrypto": "0.0.28", "concurrently": "3.5.1", + "electron": "1.8.4", "jasmine": "^3.1.0", "jasmine-ts-console-reporter": "^3.1.1", "jasmine-core": "^2.8.0", @@ -62,6 +63,8 @@ "angular2-toaster": "4.0.2", "angulartics2": "5.0.1", "core-js": "2.4.1", + "electron-log": "2.2.14", + "electron-store": "1.3.0", "lunr": "2.1.6", "node-forge": "0.7.1", "rxjs": "5.5.6", diff --git a/src/electron/services/electronLog.service.ts b/src/electron/services/electronLog.service.ts new file mode 100644 index 0000000000..c66a398d67 --- /dev/null +++ b/src/electron/services/electronLog.service.ts @@ -0,0 +1,64 @@ +import log from 'electron-log'; +import * as path from 'path'; + +import { isDev } from '../utils'; + +import { LogLevelType } from '../../enums/logLevelType'; + +import { LogService as LogServiceAbstraction } from '../../abstractions/log.service'; + +export class ElectronLogService implements LogServiceAbstraction { + constructor(private filter: (level: LogLevelType) => boolean = null, logDir: string = null) { + if (log.transports == null) { + return; + } + + log.transports.file.level = 'info'; + if (logDir != null) { + log.transports.file.file = path.join(logDir, 'app.log'); + } + } + + debug(message: string) { + if (!isDev()) { + return; + } + + this.write(LogLevelType.Debug, message); + } + + info(message: string) { + this.write(LogLevelType.Info, message); + } + + warning(message: string) { + this.write(LogLevelType.Warning, message); + } + + error(message: string) { + this.write(LogLevelType.Error, message); + } + + write(level: LogLevelType, message: string) { + if (this.filter != null && this.filter(level)) { + return; + } + + switch (level) { + case LogLevelType.Debug: + log.debug(message); + break; + case LogLevelType.Info: + log.info(message); + break; + case LogLevelType.Warning: + log.warn(message); + break; + case LogLevelType.Error: + log.error(message); + break; + default: + break; + } + } +} diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts new file mode 100644 index 0000000000..6c4509e0a1 --- /dev/null +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -0,0 +1,155 @@ +import { + clipboard, + remote, + shell, +} from 'electron'; + +import { + isDev, + isMacAppStore, +} from '../utils'; + +import { DeviceType } from '../../enums/deviceType'; + +import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +import { AnalyticsIds } from '../../misc/analytics'; +import { Utils } from '../../misc/utils'; + +export class ElectronPlatformUtilsService implements PlatformUtilsService { + identityClientId: string; + + private deviceCache: DeviceType = null; + private analyticsIdCache: string = null; + + constructor(private i18nService: I18nService, private isDesktopApp: boolean) { + this.identityClientId = isDesktopApp ? 'desktop' : 'connector'; + } + + getDevice(): DeviceType { + if (!this.deviceCache) { + switch (process.platform) { + case 'win32': + this.deviceCache = DeviceType.Windows; + break; + case 'darwin': + this.deviceCache = DeviceType.MacOs; + break; + case 'linux': + default: + this.deviceCache = DeviceType.Linux; + break; + } + } + + return this.deviceCache; + } + + getDeviceString(): string { + return DeviceType[this.getDevice()].toLowerCase(); + } + + isFirefox(): boolean { + return false; + } + + isChrome(): boolean { + return true; + } + + isEdge(): boolean { + return false; + } + + isOpera(): boolean { + return false; + } + + isVivaldi(): boolean { + return false; + } + + isSafari(): boolean { + return false; + } + + isMacAppStore(): boolean { + return isMacAppStore(); + } + + analyticsId(): string { + if (!this.isDesktopApp) { + return null; + } + + if (this.analyticsIdCache) { + return this.analyticsIdCache; + } + + this.analyticsIdCache = (AnalyticsIds as any)[this.getDevice()]; + return this.analyticsIdCache; + } + + getDomain(uriString: string): string { + return Utils.getHostname(uriString); + } + + isViewOpen(): boolean { + return false; + } + + launchUri(uri: string, options?: any): void { + shell.openExternal(uri); + } + + saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void { + const blob = new Blob([blobData], blobOptions); + const a = win.document.createElement('a'); + a.href = win.URL.createObjectURL(blob); + a.download = fileName; + window.document.body.appendChild(a); + a.click(); + window.document.body.removeChild(a); + } + + getApplicationVersion(): string { + return remote.app.getVersion(); + } + + supportsU2f(win: Window): boolean { + // Not supported in Electron at this time. + // ref: https://github.com/electron/electron/issues/3226 + return false; + } + + showDialog(text: string, title?: string, confirmText?: string, cancelText?: string, type?: string): + Promise { + const buttons = [confirmText == null ? this.i18nService.t('ok') : confirmText]; + if (cancelText != null) { + buttons.push(cancelText); + } + + const result = remote.dialog.showMessageBox(remote.getCurrentWindow(), { + type: type, + title: title, + message: title, + detail: text, + buttons: buttons, + cancelId: buttons.length === 2 ? 1 : null, + defaultId: 0, + noLink: true, + }); + + return Promise.resolve(result === 0); + } + + isDev(): boolean { + return isDev(); + } + + copyToClipboard(text: string, options?: any): void { + const type = options ? options.type : null; + clipboard.writeText(text, type); + } +} diff --git a/src/electron/services/electronRendererSecureStorage.service.ts b/src/electron/services/electronRendererSecureStorage.service.ts new file mode 100644 index 0000000000..e88a77b5d7 --- /dev/null +++ b/src/electron/services/electronRendererSecureStorage.service.ts @@ -0,0 +1,30 @@ +import { ipcRenderer } from 'electron'; + +import { StorageService } from '../../abstractions/storage.service'; + +export class ElectronRendererSecureStorageService implements StorageService { + async get(key: string): Promise { + const val = ipcRenderer.sendSync('keytar', { + action: 'getPassword', + key: key, + }); + return Promise.resolve(val != null ? JSON.parse(val) as T : null); + } + + async save(key: string, obj: any): Promise { + ipcRenderer.sendSync('keytar', { + action: 'setPassword', + key: key, + value: JSON.stringify(obj), + }); + return Promise.resolve(); + } + + async remove(key: string): Promise { + ipcRenderer.sendSync('keytar', { + action: 'deletePassword', + key: key, + }); + return Promise.resolve(); + } +} diff --git a/src/electron/services/electronStorage.service.ts b/src/electron/services/electronStorage.service.ts new file mode 100644 index 0000000000..86c6aab608 --- /dev/null +++ b/src/electron/services/electronStorage.service.ts @@ -0,0 +1,35 @@ +import { StorageService } from '../../abstractions/storage.service'; + +import { ConstantsService } from '../../services/constants.service'; + +// tslint:disable-next-line +const Store = require('electron-store'); + +export class ElectronStorageService implements StorageService { + private store: any; + + constructor() { + const storeConfig: any = { + defaults: {} as any, + name: 'data', + }; + // Default lock options to "on restart". + storeConfig.defaults[ConstantsService.lockOptionKey] = -1; + this.store = new Store(storeConfig); + } + + get(key: string): Promise { + const val = this.store.get(key) as T; + return Promise.resolve(val != null ? val : null); + } + + save(key: string, obj: any): Promise { + this.store.set(key, obj); + return Promise.resolve(); + } + + remove(key: string): Promise { + this.store.delete(key); + return Promise.resolve(); + } +} diff --git a/src/electron/utils.ts b/src/electron/utils.ts new file mode 100644 index 0000000000..cb19144bc4 --- /dev/null +++ b/src/electron/utils.ts @@ -0,0 +1,27 @@ +export function isDev() { + // ref: https://github.com/sindresorhus/electron-is-dev + if ('ELECTRON_IS_DEV' in process.env) { + return parseInt(process.env.ELECTRON_IS_DEV, 10) === 1; + } + return (process.defaultApp || /node_modules[\\/]electron[\\/]/.test(process.execPath)); +} + +export function isAppImage() { + return process.platform === 'linux' && 'APPIMAGE' in process.env; +} + +export function isMacAppStore() { + return process.platform === 'darwin' && process.mas && process.mas === true; +} + +export function isWindowsStore() { + return process.platform === 'win32' && process.windowsStore && process.windowsStore === true; +} + +export function isSnapStore() { + return process.platform === 'linux' && process.env.SNAP_USER_DATA != null; +} + +export function isWindowsPortable() { + return process.platform === 'win32' && process.env.PORTABLE_EXECUTABLE_DIR != null; +} diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts new file mode 100644 index 0000000000..87f9fed6fc --- /dev/null +++ b/src/electron/window.main.ts @@ -0,0 +1,223 @@ +import { app, BrowserWindow, screen } from 'electron'; +import * as path from 'path'; +import * as url from 'url'; + +import { isDev, isMacAppStore, isSnapStore } from './utils'; + +import { StorageService } from '../abstractions/storage.service'; + +const WindowEventHandlingDelay = 100; +const Keys = { + mainWindowSize: 'mainWindowSize', +}; + +export class WindowMain { + win: BrowserWindow; + + private windowStateChangeTimer: NodeJS.Timer; + private windowStates: { [key: string]: any; } = {}; + + constructor(private storageService: StorageService) { } + + init(): Promise { + return new Promise((resolve, reject) => { + try { + if (!isMacAppStore() && !isSnapStore()) { + const shouldQuit = app.makeSingleInstance((args, dir) => { + // Someone tried to run a second instance, we should focus our window. + if (this.win != null) { + if (this.win.isMinimized()) { + this.win.restore(); + } + this.win.focus(); + } + }); + + if (shouldQuit) { + app.quit(); + return; + } + } + + // This method will be called when Electron has finished + // initialization and is ready to create browser windows. + // Some APIs can only be used after this event occurs. + app.on('ready', async () => { + await this.createWindow(); + resolve(); + }); + + // Quit when all windows are closed. + app.on('window-all-closed', () => { + // On OS X it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin' || isMacAppStore()) { + app.quit(); + } + }); + + app.on('activate', async () => { + // On OS X it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (this.win === null) { + await this.createWindow(); + } + }); + + } catch (e) { + // Catch Error + // throw e; + reject(e); + } + }); + } + + private async createWindow() { + this.windowStates[Keys.mainWindowSize] = await this.getWindowState(Keys.mainWindowSize, 950, 600); + + // Create the browser window. + this.win = new BrowserWindow({ + width: this.windowStates[Keys.mainWindowSize].width, + height: this.windowStates[Keys.mainWindowSize].height, + minWidth: 680, + minHeight: 500, + x: this.windowStates[Keys.mainWindowSize].x, + y: this.windowStates[Keys.mainWindowSize].y, + title: app.getName(), + icon: process.platform === 'linux' ? path.join(__dirname, '/images/icon.png') : undefined, + show: false, + }); + + if (this.windowStates[Keys.mainWindowSize].isMaximized) { + this.win.maximize(); + } + + // Show it later since it might need to be maximized. + this.win.show(); + + // and load the index.html of the app. + this.win.loadURL(url.format({ + protocol: 'file:', + pathname: path.join(__dirname, '/index.html'), + slashes: true, + })); + + // Open the DevTools. + if (isDev()) { + this.win.webContents.openDevTools(); + } + + // Emitted when the window is closed. + this.win.on('closed', async () => { + await this.updateWindowState(Keys.mainWindowSize, this.win); + + // Dereference the window object, usually you would store window + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + this.win = null; + }); + + this.win.on('close', async () => { + await this.updateWindowState(Keys.mainWindowSize, this.win); + }); + + this.win.on('maximize', async () => { + await this.updateWindowState(Keys.mainWindowSize, this.win); + }); + + this.win.on('unmaximize', async () => { + await this.updateWindowState(Keys.mainWindowSize, this.win); + }); + + this.win.on('resize', () => { + this.windowStateChangeHandler(Keys.mainWindowSize, this.win); + }); + + this.win.on('move', () => { + this.windowStateChangeHandler(Keys.mainWindowSize, this.win); + }); + } + + private windowStateChangeHandler(configKey: string, win: BrowserWindow) { + global.clearTimeout(this.windowStateChangeTimer); + this.windowStateChangeTimer = global.setTimeout(async () => { + await this.updateWindowState(configKey, win); + }, WindowEventHandlingDelay); + } + + private async updateWindowState(configKey: string, win: BrowserWindow) { + if (win == null) { + return; + } + + try { + const bounds = win.getBounds(); + + if (this.windowStates[configKey] == null) { + this.windowStates[configKey] = await this.storageService.get(configKey); + if (this.windowStates[configKey] == null) { + this.windowStates[configKey] = {}; + } + } + + this.windowStates[configKey].isMaximized = win.isMaximized(); + this.windowStates[configKey].displayBounds = screen.getDisplayMatching(bounds).bounds; + + if (!win.isMaximized() && !win.isMinimized() && !win.isFullScreen()) { + this.windowStates[configKey].x = bounds.x; + this.windowStates[configKey].y = bounds.y; + this.windowStates[configKey].width = bounds.width; + this.windowStates[configKey].height = bounds.height; + } + + await this.storageService.save(configKey, this.windowStates[configKey]); + } catch (e) { } + } + + private async getWindowState(configKey: string, defaultWidth: number, defaultHeight: number) { + let state = await this.storageService.get(configKey); + + const isValid = state != null && (this.stateHasBounds(state) || state.isMaximized); + let displayBounds: Electron.Rectangle = null; + if (!isValid) { + state = { + width: defaultWidth, + height: defaultHeight, + }; + + displayBounds = screen.getPrimaryDisplay().bounds; + } else if (this.stateHasBounds(state) && state.displayBounds) { + // Check if the display where the window was last open is still available + displayBounds = screen.getDisplayMatching(state.displayBounds).bounds; + + if (displayBounds.width !== state.displayBounds.width || + displayBounds.height !== state.displayBounds.height || + displayBounds.x !== state.displayBounds.x || + displayBounds.y !== state.displayBounds.y) { + state.x = undefined; + state.y = undefined; + displayBounds = screen.getPrimaryDisplay().bounds; + } + } + + if (displayBounds != null) { + if (state.width > displayBounds.width && state.height > displayBounds.height) { + state.isMaximized = true; + } + + if (state.width > displayBounds.width) { + state.width = displayBounds.width - 10; + } + if (state.height > displayBounds.height) { + state.height = displayBounds.height - 10; + } + } + + return state; + } + + private stateHasBounds(state: any): boolean { + return state != null && Number.isInteger(state.x) && Number.isInteger(state.y) && + Number.isInteger(state.width) && state.width > 0 && Number.isInteger(state.height) && state.height > 0; + } +} diff --git a/src/misc/analytics.ts b/src/misc/analytics.ts index e78635815c..3ecf1c5b16 100644 --- a/src/misc/analytics.ts +++ b/src/misc/analytics.ts @@ -4,8 +4,22 @@ import { StorageService } from '../abstractions/storage.service'; import { ConstantsService } from '../services/constants.service'; +import { DeviceType } from '../enums/deviceType'; + const GaObj = 'ga'; +export const AnalyticsIds = { + [DeviceType.Chrome]: 'UA-81915606-6', + [DeviceType.Firefox]: 'UA-81915606-7', + [DeviceType.Opera]: 'UA-81915606-8', + [DeviceType.Edge]: 'UA-81915606-9', + [DeviceType.Vivaldi]: 'UA-81915606-15', + [DeviceType.Safari]: 'UA-81915606-16', + [DeviceType.Windows]: 'UA-81915606-17', + [DeviceType.Linux]: 'UA-81915606-19', + [DeviceType.MacOs]: 'UA-81915606-18', +}; + export class Analytics { private gaTrackingId: string = null; private defaultDisabled = false; From 8469d18f47f0dbfc1677f98323169a93c7b1e193 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 24 Apr 2018 23:22:21 -0400 Subject: [PATCH 0202/1626] move modal and duo to jslib --- src/angular/components/modal.component.ts | 66 ++++ src/misc/duo.js | 418 ++++++++++++++++++++++ 2 files changed, 484 insertions(+) create mode 100644 src/angular/components/modal.component.ts create mode 100644 src/misc/duo.js diff --git a/src/angular/components/modal.component.ts b/src/angular/components/modal.component.ts new file mode 100644 index 0000000000..a56d84fd61 --- /dev/null +++ b/src/angular/components/modal.component.ts @@ -0,0 +1,66 @@ +import { + Component, + ComponentFactoryResolver, + EventEmitter, + OnDestroy, + Output, + Type, + ViewChild, + ViewContainerRef, +} from '@angular/core'; + +@Component({ + selector: 'app-modal', + template: ``, +}) +export class ModalComponent implements OnDestroy { + @Output() onClose = new EventEmitter(); + @Output() onClosed = new EventEmitter(); + @Output() onShow = new EventEmitter(); + @Output() onShown = new EventEmitter(); + @ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef; + parentContainer: ViewContainerRef = null; + fade: boolean = true; + + constructor(private componentFactoryResolver: ComponentFactoryResolver) { } + + ngOnDestroy() { + document.body.classList.remove('modal-open'); + document.body.removeChild(document.querySelector('.modal-backdrop')); + } + + show(type: Type, parentContainer: ViewContainerRef, fade: boolean = true): T { + this.onShow.emit(); + this.parentContainer = parentContainer; + this.fade = fade; + + document.body.classList.add('modal-open'); + const backdrop = document.createElement('div'); + backdrop.className = 'modal-backdrop' + (this.fade ? ' fade' : ''); + document.body.appendChild(backdrop); + + const factory = this.componentFactoryResolver.resolveComponentFactory(type); + const componentRef = this.container.createComponent(factory); + + document.querySelector('.modal-dialog').addEventListener('click', (e: Event) => { + e.stopPropagation(); + }); + + for (const closeElement of document.querySelectorAll('.modal, .modal *[data-dismiss="modal"]')) { + closeElement.addEventListener('click', (event) => { + this.close(); + }); + } + + this.onShown.emit(); + return componentRef.instance; + } + + close() { + this.onClose.emit(); + this.onClosed.emit(); + if (this.parentContainer != null) { + this.parentContainer.clear(); + } + } +} diff --git a/src/misc/duo.js b/src/misc/duo.js new file mode 100644 index 0000000000..8b712dcf25 --- /dev/null +++ b/src/misc/duo.js @@ -0,0 +1,418 @@ +/** + * Duo Web SDK v2 + * Copyright 2017, Duo Security + */ + +var Duo; +(function (root, factory) { + // Browser globals (root is window) + var d = factory(); + // If the Javascript was loaded via a script tag, attempt to autoload + // the frame. + d._onReady(d.init); + // Attach Duo to the `window` object + root.Duo = Duo = d; +}(window, function () { + var DUO_MESSAGE_FORMAT = /^(?:AUTH|ENROLL)+\|[A-Za-z0-9\+\/=]+\|[A-Za-z0-9\+\/=]+$/; + var DUO_ERROR_FORMAT = /^ERR\|[\w\s\.\(\)]+$/; + var DUO_OPEN_WINDOW_FORMAT = /^DUO_OPEN_WINDOW\|/; + var VALID_OPEN_WINDOW_DOMAINS = [ + 'duo.com', + 'duosecurity.com', + 'duomobile.s3-us-west-1.amazonaws.com' + ]; + + var iframeId = 'duo_iframe', + postAction = '', + postArgument = 'sig_response', + host, + sigRequest, + duoSig, + appSig, + iframe, + submitCallback; + + function throwError(message, url) { + throw new Error( + 'Duo Web SDK error: ' + message + + (url ? ('\n' + 'See ' + url + ' for more information') : '') + ); + } + + function hyphenize(str) { + return str.replace(/([a-z])([A-Z])/, '$1-$2').toLowerCase(); + } + + // cross-browser data attributes + function getDataAttribute(element, name) { + if ('dataset' in element) { + return element.dataset[name]; + } else { + return element.getAttribute('data-' + hyphenize(name)); + } + } + + // cross-browser event binding/unbinding + function on(context, event, fallbackEvent, callback) { + if ('addEventListener' in window) { + context.addEventListener(event, callback, false); + } else { + context.attachEvent(fallbackEvent, callback); + } + } + + function off(context, event, fallbackEvent, callback) { + if ('removeEventListener' in window) { + context.removeEventListener(event, callback, false); + } else { + context.detachEvent(fallbackEvent, callback); + } + } + + function onReady(callback) { + on(document, 'DOMContentLoaded', 'onreadystatechange', callback); + } + + function offReady(callback) { + off(document, 'DOMContentLoaded', 'onreadystatechange', callback); + } + + function onMessage(callback) { + on(window, 'message', 'onmessage', callback); + } + + function offMessage(callback) { + off(window, 'message', 'onmessage', callback); + } + + /** + * Parse the sig_request parameter, throwing errors if the token contains + * a server error or if the token is invalid. + * + * @param {String} sig Request token + */ + function parseSigRequest(sig) { + if (!sig) { + // nothing to do + return; + } + + // see if the token contains an error, throwing it if it does + if (sig.indexOf('ERR|') === 0) { + throwError(sig.split('|')[1]); + } + + // validate the token + if (sig.indexOf(':') === -1 || sig.split(':').length !== 2) { + throwError( + 'Duo was given a bad token. This might indicate a configuration ' + + 'problem with one of Duo\'s client libraries.', + 'https://www.duosecurity.com/docs/duoweb#first-steps' + ); + } + + var sigParts = sig.split(':'); + + // hang on to the token, and the parsed duo and app sigs + sigRequest = sig; + duoSig = sigParts[0]; + appSig = sigParts[1]; + + return { + sigRequest: sig, + duoSig: sigParts[0], + appSig: sigParts[1] + }; + } + + /** + * This function is set up to run when the DOM is ready, if the iframe was + * not available during `init`. + */ + function onDOMReady() { + iframe = document.getElementById(iframeId); + + if (!iframe) { + throw new Error( + 'This page does not contain an iframe for Duo to use.' + + 'Add an element like ' + + 'to this page. ' + + 'See https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe ' + + 'for more information.' + ); + } + + // we've got an iframe, away we go! + ready(); + + // always clean up after yourself + offReady(onDOMReady); + } + + /** + * Validate that a MessageEvent came from the Duo service, and that it + * is a properly formatted payload. + * + * The Google Chrome sign-in page injects some JS into pages that also + * make use of postMessage, so we need to do additional validation above + * and beyond the origin. + * + * @param {MessageEvent} event Message received via postMessage + */ + function isDuoMessage(event) { + return Boolean( + event.origin === ('https://' + host) && + typeof event.data === 'string' && + ( + event.data.match(DUO_MESSAGE_FORMAT) || + event.data.match(DUO_ERROR_FORMAT) || + event.data.match(DUO_OPEN_WINDOW_FORMAT) + ) + ); + } + + /** + * Validate the request token and prepare for the iframe to become ready. + * + * All options below can be passed into an options hash to `Duo.init`, or + * specified on the iframe using `data-` attributes. + * + * Options specified using the options hash will take precedence over + * `data-` attributes. + * + * Example using options hash: + * ```javascript + * Duo.init({ + * iframe: "some_other_id", + * host: "api-main.duo.test", + * sig_request: "...", + * post_action: "/auth", + * post_argument: "resp" + * }); + * ``` + * + * Example using `data-` attributes: + * ``` + * + * ``` + * + * @param {Object} options + * @param {String} options.iframe The iframe, or id of an iframe to set up + * @param {String} options.host Hostname + * @param {String} options.sig_request Request token + * @param {String} [options.post_action=''] URL to POST back to after successful auth + * @param {String} [options.post_argument='sig_response'] Parameter name to use for response token + * @param {Function} [options.submit_callback] If provided, duo will not submit the form instead execute + * the callback function with reference to the "duo_form" form object + * submit_callback can be used to prevent the webpage from reloading. + */ + function init(options) { + if (options) { + if (options.host) { + host = options.host; + } + + if (options.sig_request) { + parseSigRequest(options.sig_request); + } + + if (options.post_action) { + postAction = options.post_action; + } + + if (options.post_argument) { + postArgument = options.post_argument; + } + + if (options.iframe) { + if (options.iframe.tagName) { + iframe = options.iframe; + } else if (typeof options.iframe === 'string') { + iframeId = options.iframe; + } + } + + if (typeof options.submit_callback === 'function') { + submitCallback = options.submit_callback; + } + } + + // if we were given an iframe, no need to wait for the rest of the DOM + if (false && iframe) { + ready(); + } else { + // try to find the iframe in the DOM + iframe = document.getElementById(iframeId); + + // iframe is in the DOM, away we go! + if (iframe) { + ready(); + } else { + // wait until the DOM is ready, then try again + onReady(onDOMReady); + } + } + + // always clean up after yourself! + offReady(init); + } + + /** + * This function is called when a message was received from another domain + * using the `postMessage` API. Check that the event came from the Duo + * service domain, and that the message is a properly formatted payload, + * then perform the post back to the primary service. + * + * @param event Event object (contains origin and data) + */ + function onReceivedMessage(event) { + if (isDuoMessage(event)) { + if (event.data.match(DUO_OPEN_WINDOW_FORMAT)) { + var url = event.data.substring("DUO_OPEN_WINDOW|".length); + if (isValidUrlToOpen(url)) { + // Open the URL that comes after the DUO_WINDOW_OPEN token. + window.open(url, "_self"); + } + } + else { + // the event came from duo, do the post back + doPostBack(event.data); + + // always clean up after yourself! + offMessage(onReceivedMessage); + } + } + } + + /** + * Validate that this passed in URL is one that we will actually allow to + * be opened. + * @param url String URL that the message poster wants to open + * @returns {boolean} true if we allow this url to be opened in the window + */ + function isValidUrlToOpen(url) { + if (!url) { + return false; + } + + var parser = document.createElement('a'); + parser.href = url; + + if (parser.protocol === "duotrustedendpoints:") { + return true; + } else if (parser.protocol !== "https:") { + return false; + } + + for (var i = 0; i < VALID_OPEN_WINDOW_DOMAINS.length; i++) { + if (parser.hostname.endsWith("." + VALID_OPEN_WINDOW_DOMAINS[i]) || + parser.hostname === VALID_OPEN_WINDOW_DOMAINS[i]) { + return true; + } + } + return false; + } + + /** + * Point the iframe at Duo, then wait for it to postMessage back to us. + */ + function ready() { + if (!host) { + host = getDataAttribute(iframe, 'host'); + + if (!host) { + throwError( + 'No API hostname is given for Duo to use. Be sure to pass ' + + 'a `host` parameter to Duo.init, or through the `data-host` ' + + 'attribute on the iframe element.', + 'https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe' + ); + } + } + + if (!duoSig || !appSig) { + parseSigRequest(getDataAttribute(iframe, 'sigRequest')); + + if (!duoSig || !appSig) { + throwError( + 'No valid signed request is given. Be sure to give the ' + + '`sig_request` parameter to Duo.init, or use the ' + + '`data-sig-request` attribute on the iframe element.', + 'https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe' + ); + } + } + + // if postAction/Argument are defaults, see if they are specified + // as data attributes on the iframe + if (postAction === '') { + postAction = getDataAttribute(iframe, 'postAction') || postAction; + } + + if (postArgument === 'sig_response') { + postArgument = getDataAttribute(iframe, 'postArgument') || postArgument; + } + + // point the iframe at Duo + iframe.src = [ + 'https://', host, '/frame/web/v1/auth?tx=', duoSig, + '&parent=', encodeURIComponent(document.location.href), + '&v=2.6' + ].join(''); + + // listen for the 'message' event + onMessage(onReceivedMessage); + } + + /** + * We received a postMessage from Duo. POST back to the primary service + * with the response token, and any additional user-supplied parameters + * given in form#duo_form. + */ + function doPostBack(response) { + // create a hidden input to contain the response token + var input = document.createElement('input'); + input.type = 'hidden'; + input.name = postArgument; + input.value = response + ':' + appSig; + + // user may supply their own form with additional inputs + var form = document.getElementById('duo_form'); + + // if the form doesn't exist, create one + if (!form) { + form = document.createElement('form'); + + // insert the new form after the iframe + iframe.parentElement.insertBefore(form, iframe.nextSibling); + } + + // make sure we are actually posting to the right place + form.method = 'POST'; + form.action = postAction; + + // add the response token input to the form + form.appendChild(input); + + // away we go! + if (typeof submitCallback === "function") { + submitCallback.call(null, form); + } else { + form.submit(); + } + } + + return { + init: init, + _onReady: onReady, + _parseSigRequest: parseSigRequest, + _isDuoMessage: isDuoMessage, + _doPostBack: doPostBack + }; +})); From 119a9ba6bc847049624244c8fb6957e7e2f28256 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 24 Apr 2018 23:53:03 -0400 Subject: [PATCH 0203/1626] locale key --- src/services/constants.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index 6718730e82..2326534f88 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -12,6 +12,7 @@ export class ConstantsService { static readonly lastActiveKey: string = 'lastActive'; static readonly neverDomainsKey: string = 'neverDomains'; static readonly installedVersionKey: string = 'installedVersion'; + static readonly localeKey: string = 'locale'; readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; readonly disableGaKey: string = ConstantsService.disableGaKey; @@ -24,6 +25,7 @@ export class ConstantsService { readonly lastActiveKey: string = ConstantsService.lastActiveKey; readonly neverDomainsKey: string = ConstantsService.neverDomainsKey; readonly installedVersionKey: string = ConstantsService.installedVersionKey; + readonly localeKey: string = ConstantsService.localeKey; // TODO: Convert these objects to enums readonly encType: any = { From 0ad2c9d6676783f2cd68a27733c3823eafc6d65b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 25 Apr 2018 08:26:02 -0400 Subject: [PATCH 0204/1626] make supportedTranslationLocales public --- src/abstractions/i18n.service.ts | 1 + src/services/i18n.service.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/abstractions/i18n.service.ts b/src/abstractions/i18n.service.ts index f57783b474..fa2019202c 100644 --- a/src/abstractions/i18n.service.ts +++ b/src/abstractions/i18n.service.ts @@ -1,5 +1,6 @@ export abstract class I18nService { locale: string; + supportedTranslationLocales: string[]; translationLocale: string; collator: Intl.Collator; t: (id: string, p1?: string, p2?: string, p3?: string) => string; diff --git a/src/services/i18n.service.ts b/src/services/i18n.service.ts index 9b9aaea68f..41609ce602 100644 --- a/src/services/i18n.service.ts +++ b/src/services/i18n.service.ts @@ -2,11 +2,11 @@ import { I18nService as I18nServiceAbstraction } from '../abstractions/i18n.serv export class I18nService implements I18nServiceAbstraction { locale: string; + // First locale is the default (English) + supportedTranslationLocales: string[] = ['en']; translationLocale: string; collator: Intl.Collator; - // First locale is the default (English) - protected supportedTranslationLocales: string[] = ['en']; protected inited: boolean; protected defaultMessages: any = {}; protected localeMessages: any = {}; From 31bd1f9423f71ed749d27f9500eb88d272f112d1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 25 Apr 2018 09:00:58 -0400 Subject: [PATCH 0205/1626] move electron renderer messaging service to jslib --- .../electronRendererMessaging.service.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/electron/services/electronRendererMessaging.service.ts diff --git a/src/electron/services/electronRendererMessaging.service.ts b/src/electron/services/electronRendererMessaging.service.ts new file mode 100644 index 0000000000..9dedd360a7 --- /dev/null +++ b/src/electron/services/electronRendererMessaging.service.ts @@ -0,0 +1,21 @@ +import { ipcRenderer } from 'electron'; + +import { MessagingService } from '../../abstractions/messaging.service'; + +import { BroadcasterService } from '../../angular/services/broadcaster.service'; + +export class ElectronRendererMessagingService implements MessagingService { + constructor(private broadcasterService: BroadcasterService) { + ipcRenderer.on('messagingService', async (event: any, message: any) => { + if (message.command) { + this.send(message.command, message); + } + }); + } + + send(subscriber: string, arg: any = {}) { + const message = Object.assign({}, { command: subscriber }, arg); + ipcRenderer.send('messagingService', message); + this.broadcasterService.send(message); + } +} From 05e6d2c0f0aa6cae6a5e53fac6dccf2a1d75a555 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 25 Apr 2018 12:08:18 -0400 Subject: [PATCH 0206/1626] move sync to post login action --- src/angular/components/login.component.ts | 7 +++++-- src/angular/components/two-factor.component.ts | 9 ++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/angular/components/login.component.ts b/src/angular/components/login.component.ts index 0da933fa28..79d5589622 100644 --- a/src/angular/components/login.component.ts +++ b/src/angular/components/login.component.ts @@ -16,13 +16,14 @@ export class LoginComponent { masterPassword: string = ''; showPassword: boolean = false; formPromise: Promise; + onSuccessfullLogin: () => Promise; protected twoFactorRoute = '2fa'; protected successRoute = 'vault'; constructor(protected authService: AuthService, protected router: Router, protected analytics: Angulartics2, protected toasterService: ToasterService, - protected i18nService: I18nService, protected syncService: SyncService) { } + protected i18nService: I18nService) { } async submit() { if (this.email == null || this.email === '') { @@ -48,7 +49,9 @@ export class LoginComponent { this.analytics.eventTrack.next({ action: 'Logged In To Two-step' }); this.router.navigate([this.twoFactorRoute]); } else { - this.syncService.fullSync(true); + if (this.onSuccessfullLogin != null) { + this.onSuccessfullLogin(); + } this.analytics.eventTrack.next({ action: 'Logged In' }); this.router.navigate([this.successRoute]); } diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index a448e12843..9c2e4ca354 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -37,6 +37,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { twoFactorEmail: string = null; formPromise: Promise; emailPromise: Promise; + onSuccessfullLogin: () => Promise; protected loginRoute = 'login'; protected successRoute = 'vault'; @@ -44,8 +45,8 @@ export class TwoFactorComponent implements OnInit, OnDestroy { constructor(protected authService: AuthService, protected router: Router, protected analytics: Angulartics2, protected toasterService: ToasterService, protected i18nService: I18nService, protected apiService: ApiService, - protected platformUtilsService: PlatformUtilsService, protected syncService: SyncService, - protected win: Window, protected environmentService: EnvironmentService) { + protected platformUtilsService: PlatformUtilsService, protected win: Window, + protected environmentService: EnvironmentService) { this.u2fSupported = this.platformUtilsService.supportsU2f(win); } @@ -164,7 +165,9 @@ export class TwoFactorComponent implements OnInit, OnDestroy { try { this.formPromise = this.authService.logInTwoFactor(this.selectedProviderType, this.token, this.remember); await this.formPromise; - this.syncService.fullSync(true); + if (this.onSuccessfullLogin != null) { + this.onSuccessfullLogin(); + } this.analytics.eventTrack.next({ action: 'Logged In From Two-step' }); this.router.navigate([this.successRoute]); } catch (e) { From 6e6dc422ac13047427d21d44463a5de2a3db7ebb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 25 Apr 2018 12:42:52 -0400 Subject: [PATCH 0207/1626] option to not set crypto keys --- src/services/auth.service.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 0f19db50c6..07ba8a0c32 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -65,10 +65,11 @@ export class AuthService { private key: SymmetricCryptoKey; - constructor(private cryptoService: CryptoService, private apiService: ApiService, private userService: UserService, - private tokenService: TokenService, private appIdService: AppIdService, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, private messagingService: MessagingService) { - } + constructor(private cryptoService: CryptoService, private apiService: ApiService, + private userService: UserService, private tokenService: TokenService, + private appIdService: AppIdService, private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, private messagingService: MessagingService, + private setCryptoKeys = true) { } init() { TwoFactorProviders[TwoFactorProviderType.Email].name = this.i18nService.t('emailTitle'); @@ -166,7 +167,7 @@ export class AuthService { const twoFactorResponse = response as IdentityTwoFactorResponse; this.email = email; this.masterPasswordHash = hashedPassword; - this.key = key; + this.key = this.setCryptoKeys ? key : null; this.twoFactorProviders = twoFactorResponse.twoFactorProviders2; result.twoFactorProviders = twoFactorResponse.twoFactorProviders2; return result; @@ -178,11 +179,13 @@ export class AuthService { } await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken); - await this.cryptoService.setKey(key); - await this.cryptoService.setKeyHash(hashedPassword); await this.userService.setUserIdAndEmail(this.tokenService.getUserId(), this.tokenService.getEmail()); - await this.cryptoService.setEncKey(tokenResponse.key); - await this.cryptoService.setEncPrivateKey(tokenResponse.privateKey); + if (this.setCryptoKeys) { + await this.cryptoService.setKey(key); + await this.cryptoService.setKeyHash(hashedPassword); + await this.cryptoService.setEncKey(tokenResponse.key); + await this.cryptoService.setEncPrivateKey(tokenResponse.privateKey); + } this.messagingService.send('loggedIn'); return result; From 990c9a4c5d1aaec4deb61e3ffcf72b4177708cbc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 25 Apr 2018 15:42:51 -0400 Subject: [PATCH 0208/1626] keytar storage listener --- package-lock.json | 21 ++++++++++++++++++ package.json | 2 ++ src/electron/keytarStorageListener.ts | 32 +++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 src/electron/keytarStorageListener.ts diff --git a/package-lock.json b/package-lock.json index 1b8943e9df..d0ec9309f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -90,6 +90,12 @@ "integrity": "sha512-clg9raJTY0EOo5pVZKX3ZlMjlYzVU73L71q5OV1jhE2Uezb7oF94jh4CvwrW6wInquQAdhOxJz5VDF2TLUGmmA==", "dev": true }, + "@types/keytar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/keytar/-/keytar-4.0.1.tgz", + "integrity": "sha512-loKBID6UL4QjhD2scuvv6oAPlQ/WAY7aYTDyKlKo7fIgriLS8EZExqT567cHL5CY6si51MRoX1+r3mitD3eYrA==", + "dev": true + }, "@types/lunr": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/lunr/-/lunr-2.1.5.tgz", @@ -5164,6 +5170,21 @@ } } }, + "keytar": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-4.1.0.tgz", + "integrity": "sha512-L3KqiSMtpVitlug4uuI+K5XLne9SAVEFWE8SCQIhQiH0IA/CTbon5v5prVLKK0Ken54o2O8V9HceKagpwJum+Q==", + "requires": { + "nan": "2.5.1" + }, + "dependencies": { + "nan": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.5.1.tgz", + "integrity": "sha1-1bAWkSUzJql6K77p5hxV2NYDUeI=" + } + } + }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", diff --git a/package.json b/package.json index 4a6653d9ff..41533ba5db 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ }, "devDependencies": { "@types/jasmine": "^2.8.2", + "@types/keytar": "^4.0.1", "@types/lunr": "2.1.5", "@types/node": "8.0.19", "@types/node-forge": "0.7.1", @@ -65,6 +66,7 @@ "core-js": "2.4.1", "electron-log": "2.2.14", "electron-store": "1.3.0", + "keytar": "4.1.0", "lunr": "2.1.6", "node-forge": "0.7.1", "rxjs": "5.5.6", diff --git a/src/electron/keytarStorageListener.ts b/src/electron/keytarStorageListener.ts new file mode 100644 index 0000000000..633e0264f5 --- /dev/null +++ b/src/electron/keytarStorageListener.ts @@ -0,0 +1,32 @@ +import { ipcMain } from 'electron'; + +import { + deletePassword, + getPassword, + setPassword, +} from 'keytar'; + +export class KeytarStorageListener { + constructor(private serviceName: string) { } + + init() { + ipcMain.on('keytar', async (event: any, message: any) => { + try { + let val: string = null; + if (message.action && message.key) { + if (message.action === 'getPassword') { + val = await getPassword(this.serviceName, message.key); + } else if (message.action === 'setPassword' && message.value) { + await setPassword(this.serviceName, message.key, message.value); + } else if (message.action === 'deletePassword') { + await deletePassword(this.serviceName, message.key); + } + } + + event.returnValue = val; + } catch { + event.returnValue = null; + } + }); + } +} From 42bf9b2edbafcda5d180df6d2746f19881a63315 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 25 Apr 2018 15:43:02 -0400 Subject: [PATCH 0209/1626] base electron menu class --- src/electron/baseMenu.ts | 272 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 src/electron/baseMenu.ts diff --git a/src/electron/baseMenu.ts b/src/electron/baseMenu.ts new file mode 100644 index 0000000000..77a9aff3bd --- /dev/null +++ b/src/electron/baseMenu.ts @@ -0,0 +1,272 @@ +import { + app, + clipboard, + dialog, + Menu, + MenuItemConstructorOptions, +} from 'electron'; + +import { I18nService } from '../abstractions/i18n.service'; +import { WindowMain } from './window.main'; + +import { isMacAppStore } from './utils'; + +export class BaseMenu { + protected logOutMenuItemOptions: MenuItemConstructorOptions; + protected aboutMenuItemOptions: MenuItemConstructorOptions; + protected editMenuItemOptions: MenuItemConstructorOptions; + protected viewSubMenuItemOptions: MenuItemConstructorOptions[]; + protected windowMenuItemOptions: MenuItemConstructorOptions; + protected macAppMenuItemOptions: MenuItemConstructorOptions[]; + protected macWindowSubmenuOptions: MenuItemConstructorOptions[]; + + constructor(protected i18nService: I18nService, protected windowMain: WindowMain, + protected appName: string, private onLogOut: () => void) { } + + protected initProperties() { + this.logOutMenuItemOptions = { + label: this.i18nService.t('logOut'), + id: 'logOut', + click: () => { + const result = dialog.showMessageBox(this.windowMain.win, { + title: this.i18nService.t('logOut'), + message: this.i18nService.t('logOut'), + detail: this.i18nService.t('logOutConfirmation'), + buttons: [this.i18nService.t('logOut'), this.i18nService.t('cancel')], + cancelId: 1, + defaultId: 0, + noLink: true, + }); + if (result === 0) { + this.onLogOut(); + } + }, + }; + + this.aboutMenuItemOptions = { + label: this.i18nService.t('aboutBitwarden'), + click: () => { + const aboutInformation = this.i18nService.t('version', app.getVersion()) + + '\nShell ' + process.versions.electron + + '\nRenderer ' + process.versions.chrome + + '\nNode ' + process.versions.node + + '\nArchitecture ' + process.arch; + const result = dialog.showMessageBox(this.windowMain.win, { + title: this.appName, + message: this.appName, + detail: aboutInformation, + type: 'info', + noLink: true, + buttons: [this.i18nService.t('ok'), this.i18nService.t('copy')], + }); + if (result === 1) { + clipboard.writeText(aboutInformation); + } + }, + }; + + this.editMenuItemOptions = { + label: this.i18nService.t('edit'), + submenu: [ + { + label: this.i18nService.t('undo'), + role: 'undo', + }, + { + label: this.i18nService.t('redo'), + role: 'redo', + }, + { type: 'separator' }, + { + label: this.i18nService.t('cut'), + role: 'cut', + }, + { + label: this.i18nService.t('copy'), + role: 'copy', + }, + { + label: this.i18nService.t('paste'), + role: 'paste', + }, + { type: 'separator' }, + { + label: this.i18nService.t('selectAll'), + role: 'selectall', + }, + ], + }; + + this.viewSubMenuItemOptions = [ + { + label: this.i18nService.t('zoomIn'), + role: 'zoomin', accelerator: 'CmdOrCtrl+=', + }, + { + label: this.i18nService.t('zoomOut'), + role: 'zoomout', accelerator: 'CmdOrCtrl+-', + }, + { + label: this.i18nService.t('resetZoom'), + role: 'resetzoom', accelerator: 'CmdOrCtrl+0', + }, + { type: 'separator' }, + { + label: this.i18nService.t('toggleFullScreen'), + role: 'togglefullscreen', + }, + { type: 'separator' }, + { + label: this.i18nService.t('reload'), + role: 'forcereload', + }, + { + label: this.i18nService.t('toggleDevTools'), + role: 'toggledevtools', + accelerator: 'F12', + }, + ]; + + this.windowMenuItemOptions = { + label: this.i18nService.t('window'), + role: 'window', + submenu: [ + { + label: this.i18nService.t('minimize'), + role: 'minimize', + }, + { + label: this.i18nService.t('close'), + role: 'close', + }, + ], + }; + + if (process.platform === 'darwin') { + this.macAppMenuItemOptions = [ + { + label: this.i18nService.t('services'), + role: 'services', submenu: [], + }, + { type: 'separator' }, + { + label: this.i18nService.t('hideBitwarden'), + role: 'hide', + }, + { + label: this.i18nService.t('hideOthers'), + role: 'hideothers', + }, + { + label: this.i18nService.t('showAll'), + role: 'unhide', + }, + { type: 'separator' }, + { + label: this.i18nService.t('quitBitwarden'), + role: 'quit', + }, + ]; + + this.macWindowSubmenuOptions = [ + { + label: this.i18nService.t('close'), + role: isMacAppStore() ? 'quit' : 'close', + }, + { + label: this.i18nService.t('minimize'), + role: 'minimize', + }, + { + label: this.i18nService.t('zoom'), + role: 'zoom', + }, + { type: 'separator' }, + { + label: this.i18nService.t('bringAllToFront'), + role: 'front', + }, + ]; + } + } + + protected initContextMenu() { + if (this.windowMain.win == null) { + return; + } + + const selectionMenu = Menu.buildFromTemplate([ + { + label: this.i18nService.t('copy'), + role: 'copy', + }, + { type: 'separator' }, + { + label: this.i18nService.t('selectAll'), + role: 'selectall', + }, + ]); + + const inputMenu = Menu.buildFromTemplate([ + { + label: this.i18nService.t('undo'), + role: 'undo', + }, + { + label: this.i18nService.t('redo'), + role: 'redo', + }, + { type: 'separator' }, + { + label: this.i18nService.t('cut'), + role: 'cut', + enabled: false, + }, + { + label: this.i18nService.t('copy'), + role: 'copy', + enabled: false, + }, + { + label: this.i18nService.t('paste'), + role: 'paste', + }, + { type: 'separator' }, + { + label: this.i18nService.t('selectAll'), + role: 'selectall', + }, + ]); + + const inputSelectionMenu = Menu.buildFromTemplate([ + { + label: this.i18nService.t('cut'), + role: 'cut', + }, + { + label: this.i18nService.t('copy'), + role: 'copy', + }, + { + label: this.i18nService.t('paste'), + role: 'paste', + }, + { type: 'separator' }, + { + label: this.i18nService.t('selectAll'), + role: 'selectall', + }, + ]); + + this.windowMain.win.webContents.on('context-menu', (e, props) => { + const selected = props.selectionText && props.selectionText.trim() !== ''; + if (props.isEditable && selected) { + inputSelectionMenu.popup(this.windowMain.win); + } else if (props.isEditable) { + inputMenu.popup(this.windowMain.win); + } else if (selected) { + selectionMenu.popup(this.windowMain.win); + } + }); + } +} From 33c3af3a8ed5ac5009b48e73b0cd4da25d42cec1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 25 Apr 2018 16:30:18 -0400 Subject: [PATCH 0210/1626] require node URL --- src/misc/utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/misc/utils.ts b/src/misc/utils.ts index d91b64209c..0ff14d3dbd 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -1,3 +1,6 @@ +// tslint:disable-next-line +const nodeURL = typeof window === 'undefined' ? require('url').URL : null; + export class Utils { static inited = false; static isNode = false; @@ -159,7 +162,7 @@ export class Utils { if (uriString.startsWith('http://') || uriString.startsWith('https://')) { try { - return new URL(uriString); + return nodeURL != null ? new nodeURL(uriString) : new URL(uriString); } catch (e) { } } From e7735f2d802b2aaba9fc4b41d0760701e375ec86 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 25 Apr 2018 16:30:47 -0400 Subject: [PATCH 0211/1626] test pbkdf2 node function --- .../nodeCryptoFunction.service.spec.ts | 50 +++++++++++++++++++ src/services/nodeCryptoFunction.service.ts | 4 +- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/spec/node/services/nodeCryptoFunction.service.spec.ts b/spec/node/services/nodeCryptoFunction.service.spec.ts index 746a0563b4..6250c642ca 100644 --- a/spec/node/services/nodeCryptoFunction.service.spec.ts +++ b/spec/node/services/nodeCryptoFunction.service.spec.ts @@ -27,6 +27,22 @@ const RsaPrivateKey = 'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX 'BokBGnjFnTnKcs7nv/O8='; describe('NodeCrypto Function Service', () => { + describe('pbkdf2', () => { + const regular256Key = 'pj9prw/OHPleXI6bRdmlaD+saJS4awrMiQsQiDjeu2I='; + const utf8256Key = 'yqvoFXgMRmHR3QPYr5pyR4uVuoHkltv9aHUP63p8n7I='; + const unicode256Key = 'ZdeOata6xoRpB4DLp8zHhXz5kLmkWtX5pd+TdRH8w8w='; + + const regular512Key = 'liTi/Ke8LPU1Qv+Vl7NGEVt/XMbsBVJ2kQxtVG/Z1/JFHFKQW3ZkI81qVlwTiCpb+cFXzs+57' + + 'eyhhx5wfKo5Cg=='; + const utf8512Key = 'df0KdvIBeCzD/kyXptwQohaqUa4e7IyFUyhFQjXCANu5T+scq55hCcE4dG4T/MhAk2exw8j7ixRN' + + 'zXANiVZpnw=='; + const unicode512Key = 'FE+AnUJaxv8jh+zUDtZz4mjjcYk0/PZDZm+SLJe3XtxtnpdqqpblX6JjuMZt/dYYNMOrb2+mD' + + 'L3FiQDTROh1lg=='; + + testPbkdf2('sha256', regular256Key, utf8256Key, unicode256Key); + testPbkdf2('sha512', regular512Key, utf8512Key, unicode512Key); + }); + describe('aesEncrypt', () => { it('should successfully encrypt data', async () => { const nodeCryptoFunctionService = new NodeCryptoFunctionService(); @@ -113,6 +129,40 @@ describe('NodeCrypto Function Service', () => { }); }); +function testPbkdf2(algorithm: 'sha256' | 'sha512', regularKey: string, utf8Key: string, unicodeKey: string) { + const regularEmail = 'user@example.com'; + const utf8Email = 'üser@example.com'; + + const regularPassword = 'password'; + const utf8Password = 'pǻssword'; + const unicodePassword = '😀password🙏'; + + it('should create valid ' + algorithm + ' key from regular input', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const key = await cryptoFunctionService.pbkdf2(regularPassword, regularEmail, algorithm, 5000); + expect(Utils.fromBufferToB64(key)).toBe(regularKey); + }); + + it('should create valid ' + algorithm + ' key from utf8 input', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const key = await cryptoFunctionService.pbkdf2(utf8Password, utf8Email, algorithm, 5000); + expect(Utils.fromBufferToB64(key)).toBe(utf8Key); + }); + + it('should create valid ' + algorithm + ' key from unicode input', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const key = await cryptoFunctionService.pbkdf2(unicodePassword, regularEmail, algorithm, 5000); + expect(Utils.fromBufferToB64(key)).toBe(unicodeKey); + }); + + it('should create valid ' + algorithm + ' key from array buffer input', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const key = await cryptoFunctionService.pbkdf2(Utils.fromUtf8ToArray(regularPassword).buffer, + Utils.fromUtf8ToArray(regularEmail).buffer, algorithm, 5000); + expect(Utils.fromBufferToB64(key)).toBe(regularKey); + }); +} + function makeStaticByteArray(length: number) { const arr = new Uint8Array(length); for (let i = 0; i < length; i++) { diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index 063d665d93..1739df9987 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -9,11 +9,11 @@ import { Utils } from '../misc/utils'; export class NodeCryptoFunctionService implements CryptoFunctionService { pbkdf2(password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', iterations: number): Promise { - const len = algorithm === 'sha256' ? 256 : 512; + const len = algorithm === 'sha256' ? 32 : 64; const nodePassword = this.toNodeValue(password); const nodeSalt = this.toNodeValue(salt); return new Promise((resolve, reject) => { - crypto.pbkdf2(nodePassword, nodeSalt, iterations, length, algorithm, (error, key) => { + crypto.pbkdf2(nodePassword, nodeSalt, iterations, len, algorithm, (error, key) => { if (error != null) { reject(error); } else { From ac22a4f4c19ea9d1eba73dac36b3223a352ab52f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 25 Apr 2018 16:33:58 -0400 Subject: [PATCH 0212/1626] test all node crypto functions --- .../nodeCryptoFunction.service.spec.ts | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/spec/node/services/nodeCryptoFunction.service.spec.ts b/spec/node/services/nodeCryptoFunction.service.spec.ts index 6250c642ca..3964d39ae5 100644 --- a/spec/node/services/nodeCryptoFunction.service.spec.ts +++ b/spec/node/services/nodeCryptoFunction.service.spec.ts @@ -43,6 +43,38 @@ describe('NodeCrypto Function Service', () => { testPbkdf2('sha512', regular512Key, utf8512Key, unicode512Key); }); + describe('hash', () => { + const regular1Hash = '2a241604fb921fad12bf877282457268e1dccb70'; + const utf81Hash = '85672798dc5831e96d6c48655d3d39365a9c88b6'; + const unicode1Hash = '39c975935054a3efc805a9709b60763a823a6ad4'; + + const regular256Hash = '2b8e96031d352a8655d733d7a930b5ffbea69dc25cf65c7bca7dd946278908b2'; + const utf8256Hash = '25fe8440f5b01ed113b0a0e38e721b126d2f3f77a67518c4a04fcde4e33eeb9d'; + const unicode256Hash = 'adc1c0c2afd6e92cefdf703f9b6eb2c38e0d6d1a040c83f8505c561fea58852e'; + + const regular512Hash = 'c15cf11d43bde333647e3f559ec4193bb2edeaa0e8b902772f514cdf3f785a3f49a6e02a4b87b3' + + 'b47523271ad45b7e0aebb5cdcc1bc54815d256eb5dcb80da9d'; + const utf8512Hash = '035c31a877a291af09ed2d3a1a293e69c3e079ea2cecc00211f35e6bce10474ca3ad6e30b59e26118' + + '37463f20969c5bc95282965a051a88f8cdf2e166549fcdd'; + const unicode512Hash = '2b16a5561af8ad6fe414cc103fc8036492e1fc6d9aabe1b655497054f760fe0e34c5d100ac773d' + + '9f3030438284f22dbfa20cb2e9b019f2c98dfe38ce1ef41bae'; + + testHash('sha1', regular1Hash, utf81Hash, unicode1Hash); + testHash('sha256', regular256Hash, utf8256Hash, unicode256Hash); + testHash('sha512', regular512Hash, utf8512Hash, unicode512Hash); + }); + + describe('hmac', () => { + const sha1Mac = '4d4c223f95dc577b665ec4ccbcb680b80a397038'; + const sha256Mac = '6be3caa84922e12aaaaa2f16c40d44433bb081ef323db584eb616333ab4e874f'; + const sha512Mac = '21910e341fa12106ca35758a2285374509326c9fbe0bd64e7b99c898f841dc948c58ce66d3504d8883c' + + '5ea7817a0b7c5d4d9b00364ccd214669131fc17fe4aca'; + + testHmac('sha1', sha1Mac); + testHmac('sha256', sha256Mac); + testHmac('sha512', sha512Mac); + }); + describe('aesEncrypt', () => { it('should successfully encrypt data', async () => { const nodeCryptoFunctionService = new NodeCryptoFunctionService(); @@ -163,6 +195,45 @@ function testPbkdf2(algorithm: 'sha256' | 'sha512', regularKey: string, utf8Key: }); } +function testHash(algorithm: 'sha1' | 'sha256' | 'sha512', regularHash: string, utf8Hash: string, unicodeHash: string) { + const regularValue = 'HashMe!!'; + const utf8Value = 'HǻshMe!!'; + const unicodeValue = '😀HashMe!!!🙏'; + + it('should create valid ' + algorithm + ' hash from regular input', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const hash = await cryptoFunctionService.hash(regularValue, algorithm); + expect(Utils.fromBufferToHex(hash)).toBe(regularHash); + }); + + it('should create valid ' + algorithm + ' hash from utf8 input', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const hash = await cryptoFunctionService.hash(utf8Value, algorithm); + expect(Utils.fromBufferToHex(hash)).toBe(utf8Hash); + }); + + it('should create valid ' + algorithm + ' hash from unicode input', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const hash = await cryptoFunctionService.hash(unicodeValue, algorithm); + expect(Utils.fromBufferToHex(hash)).toBe(unicodeHash); + }); + + it('should create valid ' + algorithm + ' hash from array buffer input', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const hash = await cryptoFunctionService.hash(Utils.fromUtf8ToArray(regularValue).buffer, algorithm); + expect(Utils.fromBufferToHex(hash)).toBe(regularHash); + }); +} + +function testHmac(algorithm: 'sha1' | 'sha256' | 'sha512', mac: string) { + it('should create valid ' + algorithm + ' hmac', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const computedMac = await cryptoFunctionService.hmac(Utils.fromUtf8ToArray('SignMe!!').buffer, + Utils.fromUtf8ToArray('secretkey').buffer, algorithm); + expect(Utils.fromBufferToHex(computedMac)).toBe(mac); + }); +} + function makeStaticByteArray(length: number) { const arr = new Uint8Array(length); for (let i = 0; i < length; i++) { From 10b9e9c2d6fae90af6c481741830a92ba9d986b3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 25 Apr 2018 16:50:44 -0400 Subject: [PATCH 0213/1626] quotes not needed --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 41533ba5db..ba4055c47f 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test": "karma start ./spec/support/karma.conf.js --single-run", "test:watch": "karma start ./spec/support/karma.conf.js", "test:node": "npm run build && jasmine", - "test:node:watch": "concurrently -k -n \"TSC,Node\" -c \"yellow,cyan\" \"npm run build:watch\" \"nodemon -w ./dist --delay 500ms --exec jasmine\"" + "test:node:watch": "concurrently -k -n TSC,Node -c yellow,cyan \"npm run build:watch\" \"nodemon -w ./dist --delay 500ms --exec jasmine\"" }, "devDependencies": { "@types/jasmine": "^2.8.2", From df1649bac151ccb7a00244a3633f5ba3adf178a8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 25 Apr 2018 17:53:37 -0400 Subject: [PATCH 0214/1626] input verbatim directive --- .../directives/input-verbatim.directive.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/angular/directives/input-verbatim.directive.ts diff --git a/src/angular/directives/input-verbatim.directive.ts b/src/angular/directives/input-verbatim.directive.ts new file mode 100644 index 0000000000..570defe0d4 --- /dev/null +++ b/src/angular/directives/input-verbatim.directive.ts @@ -0,0 +1,37 @@ +import { + Directive, + ElementRef, + Input, + Renderer2, +} from '@angular/core'; + +@Directive({ + selector: '[appInputVerbatim]', +}) +export class InputVerbatimDirective { + @Input() set appInputVerbatim(condition: boolean | string) { + this.disableComplete = condition === '' || condition === true; + } + + private disableComplete: boolean; + + constructor(private el: ElementRef, private renderer: Renderer2) { } + + ngOnInit() { + if (this.disableComplete && !this.el.nativeElement.hasAttribute('autocomplete')) { + this.renderer.setAttribute(this.el.nativeElement, 'autocomplete', 'nope'); + } + if (!this.el.nativeElement.hasAttribute('autocapitalize')) { + this.renderer.setAttribute(this.el.nativeElement, 'autocapitalize', 'none'); + } + if (!this.el.nativeElement.hasAttribute('autocorrect')) { + this.renderer.setAttribute(this.el.nativeElement, 'autocorrect', 'none'); + } + if (!this.el.nativeElement.hasAttribute('spellcheck')) { + this.renderer.setAttribute(this.el.nativeElement, 'spellcheck', 'false'); + } + if (!this.el.nativeElement.hasAttribute('inputmode')) { + this.renderer.setAttribute(this.el.nativeElement, 'inputmode', 'verbatim'); + } + } +} From 7b5d3eb15c2b273e762d47c18c8a72288a7763ea Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 26 Apr 2018 00:19:54 -0400 Subject: [PATCH 0215/1626] autocomplete off --- src/angular/directives/input-verbatim.directive.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/directives/input-verbatim.directive.ts b/src/angular/directives/input-verbatim.directive.ts index 570defe0d4..9c94fbcb40 100644 --- a/src/angular/directives/input-verbatim.directive.ts +++ b/src/angular/directives/input-verbatim.directive.ts @@ -19,7 +19,7 @@ export class InputVerbatimDirective { ngOnInit() { if (this.disableComplete && !this.el.nativeElement.hasAttribute('autocomplete')) { - this.renderer.setAttribute(this.el.nativeElement, 'autocomplete', 'nope'); + this.renderer.setAttribute(this.el.nativeElement, 'autocomplete', 'off'); } if (!this.el.nativeElement.hasAttribute('autocapitalize')) { this.renderer.setAttribute(this.el.nativeElement, 'autocapitalize', 'none'); From 983a7b474c344475edc3290d0ce1894a92794752 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 26 Apr 2018 00:43:01 -0400 Subject: [PATCH 0216/1626] dummy module. ref https://github.com/angular/angular/issues/13590 --- src/angular/dummy.module.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/angular/dummy.module.ts diff --git a/src/angular/dummy.module.ts b/src/angular/dummy.module.ts new file mode 100644 index 0000000000..bdd63830b2 --- /dev/null +++ b/src/angular/dummy.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; + +import { InputVerbatimDirective } from './directives/input-verbatim.directive'; + +@NgModule({ + imports: [], + declarations: [ + InputVerbatimDirective, + ], +}) +export class DummyModule { +} From 35039fdae2038f6bdfbdef6616b2bfae0053d47b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 26 Apr 2018 15:43:59 -0400 Subject: [PATCH 0217/1626] main messaging service to jslib --- .../services/electronMainMessaging.service.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/electron/services/electronMainMessaging.service.ts diff --git a/src/electron/services/electronMainMessaging.service.ts b/src/electron/services/electronMainMessaging.service.ts new file mode 100644 index 0000000000..6e366147da --- /dev/null +++ b/src/electron/services/electronMainMessaging.service.ts @@ -0,0 +1,15 @@ +import { MessagingService } from '../../abstractions/messaging.service'; + +import { WindowMain } from '../window.main'; + +export class ElectronMainMessagingService implements MessagingService { + constructor(private windowMain: WindowMain, private onMessage: (message: any) => void) { } + + send(subscriber: string, arg: any = {}) { + const message = Object.assign({}, { command: subscriber }, arg); + this.onMessage(message); + if (this.windowMain.win != null) { + this.windowMain.win.webContents.send('messagingService', message); + } + } +} From 12533dd951e5f248ee82ca0100fb9fd215fc6bd3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 26 Apr 2018 16:17:11 -0400 Subject: [PATCH 0218/1626] move storage defaults out to app --- src/electron/services/electronStorage.service.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/electron/services/electronStorage.service.ts b/src/electron/services/electronStorage.service.ts index 86c6aab608..953e33c9d9 100644 --- a/src/electron/services/electronStorage.service.ts +++ b/src/electron/services/electronStorage.service.ts @@ -1,20 +1,21 @@ import { StorageService } from '../../abstractions/storage.service'; -import { ConstantsService } from '../../services/constants.service'; - // tslint:disable-next-line const Store = require('electron-store'); export class ElectronStorageService implements StorageService { private store: any; - constructor() { + constructor(defaults?: any) { const storeConfig: any = { defaults: {} as any, name: 'data', }; - // Default lock options to "on restart". - storeConfig.defaults[ConstantsService.lockOptionKey] = -1; + + if (defaults != null) { + storeConfig.defaults = Object.assign({}, storeConfig.defaults, defaults); + } + this.store = new Store(storeConfig); } From 66bdf442d6f809487f674fecae325ddef0c8470f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 30 Apr 2018 17:00:31 -0400 Subject: [PATCH 0219/1626] add necessary apis for org directory imports --- src/abstractions/api.service.ts | 2 ++ src/models/request/importDirectoryRequest.ts | 7 +++++++ .../request/importDirectoryRequestGroup.ts | 5 +++++ .../request/importDirectoryRequestUser.ts | 5 +++++ src/services/api.service.ts | 21 +++++++++++++++++++ 5 files changed, 40 insertions(+) create mode 100644 src/models/request/importDirectoryRequest.ts create mode 100644 src/models/request/importDirectoryRequestGroup.ts create mode 100644 src/models/request/importDirectoryRequestUser.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 4c7d621c14..daad664cb2 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -2,6 +2,7 @@ import { EnvironmentUrls } from '../models/domain/environmentUrls'; import { CipherRequest } from '../models/request/cipherRequest'; import { FolderRequest } from '../models/request/folderRequest'; +import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { TokenRequest } from '../models/request/tokenRequest'; @@ -36,4 +37,5 @@ export abstract class ApiService { postCipherAttachment: (id: string, data: FormData) => Promise; deleteCipherAttachment: (id: string, attachmentId: string) => Promise; getSync: () => Promise; + postImportDirectory: (organizationId: string, request: ImportDirectoryRequest) => Promise; } diff --git a/src/models/request/importDirectoryRequest.ts b/src/models/request/importDirectoryRequest.ts new file mode 100644 index 0000000000..cd6eb06309 --- /dev/null +++ b/src/models/request/importDirectoryRequest.ts @@ -0,0 +1,7 @@ +import { ImportDirectoryRequestGroup } from './importDirectoryRequestGroup'; +import { ImportDirectoryRequestUser } from './importDirectoryRequestUser'; + +export class ImportDirectoryRequest { + groups: ImportDirectoryRequestGroup[] = []; + users: ImportDirectoryRequestUser[] = []; +} diff --git a/src/models/request/importDirectoryRequestGroup.ts b/src/models/request/importDirectoryRequestGroup.ts new file mode 100644 index 0000000000..8e4f7f4ace --- /dev/null +++ b/src/models/request/importDirectoryRequestGroup.ts @@ -0,0 +1,5 @@ +export class ImportDirectoryRequestGroup { + name: string; + externalId: string; + users: string[]; +} diff --git a/src/models/request/importDirectoryRequestUser.ts b/src/models/request/importDirectoryRequestUser.ts new file mode 100644 index 0000000000..99d699e77b --- /dev/null +++ b/src/models/request/importDirectoryRequestUser.ts @@ -0,0 +1,5 @@ +export class ImportDirectoryRequestUser { + externalId: string; + email: string; + deleted: boolean; +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 35a5f0f8d1..be3a93d674 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -8,6 +8,7 @@ import { EnvironmentUrls } from '../models/domain/environmentUrls'; import { CipherRequest } from '../models/request/cipherRequest'; import { FolderRequest } from '../models/request/folderRequest'; +import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { TokenRequest } from '../models/request/tokenRequest'; @@ -379,6 +380,26 @@ export class ApiService implements ApiServiceAbstraction { } } + async postImportDirectory(organizationId: string, request: ImportDirectoryRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/organizations/' + organizationId + '/import', { + body: JSON.stringify(request), + cache: 'no-cache', + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Content-Type': 'application/json; charset=utf-8', + 'Device-Type': this.deviceType, + }), + method: 'POST', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + // Helpers private async handleError(response: Response, tokenError: boolean): Promise { From c29b53cdd62369de59f6ae483b78b9bcc67d9238 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 2 May 2018 16:00:46 -0400 Subject: [PATCH 0220/1626] show string errors --- src/angular/services/validation.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/angular/services/validation.service.ts b/src/angular/services/validation.service.ts index 06c8bde835..ddcc37afff 100644 --- a/src/angular/services/validation.service.ts +++ b/src/angular/services/validation.service.ts @@ -12,7 +12,9 @@ export class ValidationService { const defaultErrorMessage = this.i18nService.t('unexpectedError'); const errors: string[] = []; - if (data == null || typeof data !== 'object') { + if (data != null && typeof data === 'string') { + errors.push(data); + } else if (data == null || typeof data !== 'object') { errors.push(defaultErrorMessage); } else if (data.validationErrors == null) { errors.push(data.message ? data.message : defaultErrorMessage); From b5db9edc3f930a4553f1bdab347cf8468f282a54 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 2 May 2018 17:00:24 -0400 Subject: [PATCH 0221/1626] adjust collator sorting options --- src/services/i18n.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/i18n.service.ts b/src/services/i18n.service.ts index 41609ce602..b826f97234 100644 --- a/src/services/i18n.service.ts +++ b/src/services/i18n.service.ts @@ -28,7 +28,7 @@ export class I18nService implements I18nServiceAbstraction { this.locale = this.translationLocale = locale != null ? locale : this.systemLanguage; try { - this.collator = new Intl.Collator(this.locale); + this.collator = new Intl.Collator(this.locale, { numeric: true, sensitivity: 'base' }); } catch { this.collator = null; } From c3dad8fd1ae862476ccc417b4d33eecd3edd61a9 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 3 May 2018 12:46:49 -0400 Subject: [PATCH 0222/1626] add account profile api --- src/abstractions/api.service.ts | 2 ++ src/enums/organizationUserStatusType.ts | 5 +++++ src/enums/organizationUserType.ts | 5 +++++ .../response/profileOrganizationResponse.ts | 13 ++++++++++-- src/services/api.service.ts | 21 +++++++++++++++++++ 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/enums/organizationUserStatusType.ts create mode 100644 src/enums/organizationUserType.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index daad664cb2..db6f351637 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -12,6 +12,7 @@ import { CipherResponse } from '../models/response/cipherResponse'; import { FolderResponse } from '../models/response/folderResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; +import { ProfileResponse } from '../models/response/profileResponse'; import { SyncResponse } from '../models/response/syncResponse'; export abstract class ApiService { @@ -25,6 +26,7 @@ export abstract class ApiService { postIdentityToken: (request: TokenRequest) => Promise; refreshIdentityToken: () => Promise; postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise; + getProfile: () => Promise; getAccountRevisionDate: () => Promise; postPasswordHint: (request: PasswordHintRequest) => Promise; postRegister: (request: RegisterRequest) => Promise; diff --git a/src/enums/organizationUserStatusType.ts b/src/enums/organizationUserStatusType.ts new file mode 100644 index 0000000000..eae4a0a636 --- /dev/null +++ b/src/enums/organizationUserStatusType.ts @@ -0,0 +1,5 @@ +export enum OrganizationUserStatusType { + Invited = 0, + Accepted = 1, + Confirmed = 2, +} diff --git a/src/enums/organizationUserType.ts b/src/enums/organizationUserType.ts new file mode 100644 index 0000000000..217c0b450e --- /dev/null +++ b/src/enums/organizationUserType.ts @@ -0,0 +1,5 @@ +export enum OrganizationUserType { + Owner = 0, + Admin = 1, + User = 2, +} diff --git a/src/models/response/profileOrganizationResponse.ts b/src/models/response/profileOrganizationResponse.ts index 05b2b980b5..7ea65f87e9 100644 --- a/src/models/response/profileOrganizationResponse.ts +++ b/src/models/response/profileOrganizationResponse.ts @@ -1,27 +1,36 @@ +import { OrganizationUserStatusType } from '../../enums/organizationUserStatusType'; +import { OrganizationUserType } from '../../enums/organizationUserType'; + export class ProfileOrganizationResponse { id: string; name: string; useGroups: boolean; useDirectory: boolean; + useEvents: boolean; useTotp: boolean; + use2fa: boolean; seats: number; maxCollections: number; maxStorageGb?: number; key: string; - status: number; // TODO: map to enum - type: number; // TODO: map to enum + status: OrganizationUserStatusType; + type: OrganizationUserType; + enabled: boolean; constructor(response: any) { this.id = response.Id; this.name = response.Name; this.useGroups = response.UseGroups; this.useDirectory = response.UseDirectory; + this.useEvents = response.UseEvents; this.useTotp = response.UseTotp; + this.use2fa = response.Use2fa; this.seats = response.Seats; this.maxCollections = response.MaxCollections; this.maxStorageGb = response.MaxStorageGb; this.key = response.Key; this.status = response.Status; this.type = response.Type; + this.enabled = response.Enabled; } } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index be3a93d674..b4274465a6 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -19,6 +19,7 @@ import { ErrorResponse } from '../models/response/errorResponse'; import { FolderResponse } from '../models/response/folderResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; +import { ProfileResponse } from '../models/response/profileResponse'; import { SyncResponse } from '../models/response/syncResponse'; export class ApiService implements ApiServiceAbstraction { @@ -134,6 +135,26 @@ export class ApiService implements ApiServiceAbstraction { // Account APIs + async getProfile(): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/accounts/profile', { + cache: 'no-cache', + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Device-Type': this.deviceType, + }), + })); + + if (response.status === 200) { + const responseJson = await response.json(); + return new ProfileResponse(responseJson); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + async getAccountRevisionDate(): Promise { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/accounts/revision-date', { From 2032e14285ac3d4b2f3e9e310ad19ca1dd40c525 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 4 May 2018 14:16:23 -0400 Subject: [PATCH 0223/1626] move updater to jslib --- package-lock.json | 149 +++++++++++++++++++++++++++++++--- package.json | 1 + src/electron/updater.main.ts | 153 +++++++++++++++++++++++++++++++++++ 3 files changed, 294 insertions(+), 9 deletions(-) create mode 100644 src/electron/updater.main.ts diff --git a/package-lock.json b/package-lock.json index d0ec9309f4..1343aecc53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -264,7 +264,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "requires": { "sprintf-js": "1.0.3" } @@ -634,8 +633,15 @@ "bluebird": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", - "dev": true + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + }, + "bluebird-lst": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.5.tgz", + "integrity": "sha512-Ey0bDNys5qpYPhZ/oQ9vOEvD0TYQDTILMXWP2iGfvMg7rSDde+oV4aQQgqRH+CvBFNz2BSDQnPGMUl6LKBUUQA==", + "requires": { + "bluebird": "3.5.1" + } }, "bn.js": { "version": "4.11.8", @@ -875,12 +881,38 @@ "ieee754": "1.1.11" } }, + "buffer-from": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", + "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==" + }, "buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", "dev": true }, + "builder-util-runtime": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-4.2.1.tgz", + "integrity": "sha512-6Ufp6ExT40RDYNXQgD4xG0fgtpUHyc8XIld6lptKr0re1DNnUrQP4sSV/lJOajpzyercMP/YIzO60/mNuAFiWg==", + "requires": { + "bluebird-lst": "1.0.5", + "debug": "3.1.0", + "fs-extra-p": "4.6.0", + "sax": "1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, "builtin-modules": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", @@ -1881,6 +1913,11 @@ } } }, + "electron-is-dev": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-0.3.0.tgz", + "integrity": "sha1-FOb9pcaOnk7L7/nM8DfL18BcWv4=" + }, "electron-log": { "version": "2.2.14", "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.14.tgz", @@ -1894,6 +1931,38 @@ "conf": "1.4.0" } }, + "electron-updater": { + "version": "2.21.4", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-2.21.4.tgz", + "integrity": "sha512-x6QSbyxgGR3szIOBtFoCJH0TfgB55AWHaXmilNgorfvpnCdEMQEATxEzLOW0JCzzcB5y3vBrawvmMUEdXwutmA==", + "requires": { + "bluebird-lst": "1.0.5", + "builder-util-runtime": "4.2.1", + "electron-is-dev": "0.3.0", + "fs-extra-p": "4.6.0", + "js-yaml": "3.11.0", + "lazy-val": "1.0.3", + "lodash.isequal": "4.5.0", + "semver": "5.5.0", + "source-map-support": "0.5.5" + }, + "dependencies": { + "js-yaml": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz", + "integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==", + "requires": { + "argparse": "1.0.10", + "esprima": "4.0.0" + } + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" + } + } + }, "elliptic": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", @@ -2092,8 +2161,7 @@ "esprima": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", - "dev": true + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==" }, "estraverse": { "version": "1.9.3", @@ -2544,6 +2612,35 @@ } } }, + "fs-extra-p": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.6.0.tgz", + "integrity": "sha512-nSVqB5UfWZQdU6pzBwcFh+7lJpBynnTsVtNJTBhAnAppUQRut0W7WeM271iS0TqQ9FoCqDXqyL0+h+h8DQUCpg==", + "requires": { + "bluebird-lst": "1.0.5", + "fs-extra": "6.0.0" + }, + "dependencies": { + "fs-extra": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.0.tgz", + "integrity": "sha512-lk2cUCo8QzbiEWEbt7Cw3m27WMiRG321xsssbcIpfMhpRjrlC08WBOVQqj1/nQYYNnPtyIhP1oqLO3QwT2tPCw==", + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "4.0.0", + "universalify": "0.1.1" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "4.1.11" + } + } + } + }, "fs.realpath": { "version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", @@ -5219,6 +5316,11 @@ "dev": true, "optional": true }, + "lazy-val": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.3.tgz", + "integrity": "sha512-pjCf3BYk+uv3ZcPzEVM0BFvO9Uw58TmlrU0oG5tTrr9Kcid3+kdKxapH8CjdYmVa2nO5wOoZn2rdvZx2PKj/xg==" + }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -5264,6 +5366,11 @@ "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", "dev": true }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, "lodash.memoize": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", @@ -5569,8 +5676,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "nan": { "version": "2.10.0", @@ -7302,6 +7408,11 @@ "ret": "0.1.15" } }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, "semver": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", @@ -7716,6 +7827,22 @@ "urix": "0.1.0" } }, + "source-map-support": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.5.tgz", + "integrity": "sha512-mR7/Nd5l1z6g99010shcXJiNEaf3fEtmLhRB/sBcQVJGodcHCULPp2y4Sfa43Kv2zq7T+Izmfp/WHCR6dYkQCA==", + "requires": { + "buffer-from": "1.0.0", + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, "source-map-url": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", @@ -7808,8 +7935,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "sshpk": { "version": "1.14.1", @@ -8735,6 +8861,11 @@ "crypto-random-string": "1.0.0" } }, + "universalify": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index ba4055c47f..4425fdb0c9 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "core-js": "2.4.1", "electron-log": "2.2.14", "electron-store": "1.3.0", + "electron-updater": "2.21.4", "keytar": "4.1.0", "lunr": "2.1.6", "node-forge": "0.7.1", diff --git a/src/electron/updater.main.ts b/src/electron/updater.main.ts new file mode 100644 index 0000000000..012f169f49 --- /dev/null +++ b/src/electron/updater.main.ts @@ -0,0 +1,153 @@ +import { + dialog, + Menu, + MenuItem, + shell, +} from 'electron'; +import log from 'electron-log'; +import { autoUpdater } from 'electron-updater'; + +import { + isAppImage, + isDev, + isMacAppStore, + isWindowsPortable, + isWindowsStore, +} from './utils'; + +import { I18nService } from '../abstractions/i18n.service'; +import { WindowMain } from './window.main'; + +const UpdaterCheckInitalDelay = 5 * 1000; // 5 seconds +const UpdaterCheckInterval = 12 * 60 * 60 * 1000; // 12 hours + +export class UpdaterMain { + private doingUpdateCheck = false; + private doingUpdateCheckWithFeedback = false; + private canUpdate = false; + + constructor(private i18nService: I18nService, private windowMain: WindowMain, + private gitHubProject: string, private onCheckingForUpdate: () => void = null, + private onReset: () => void = null, private onUpdateDownloaded: () => void = null) { + autoUpdater.logger = log; + + const linuxCanUpdate = process.platform === 'linux' && isAppImage(); + const windowsCanUpdate = process.platform === 'win32' && !isWindowsStore() && !isWindowsPortable(); + const macCanUpdate = process.platform === 'darwin' && !isMacAppStore(); + this.canUpdate = linuxCanUpdate || windowsCanUpdate || macCanUpdate; + } + + async init() { + global.setTimeout(async () => await this.checkForUpdate(), UpdaterCheckInitalDelay); + global.setInterval(async () => await this.checkForUpdate(), UpdaterCheckInterval); + + autoUpdater.on('checking-for-update', () => { + if (this.onCheckingForUpdate != null) { + this.onCheckingForUpdate(); + } + this.doingUpdateCheck = true; + }); + + autoUpdater.on('update-available', () => { + if (this.doingUpdateCheckWithFeedback) { + if (this.windowMain.win == null) { + this.reset(); + return; + } + + const result = dialog.showMessageBox(this.windowMain.win, { + type: 'info', + title: this.i18nService.t('updateAvailable'), + message: this.i18nService.t('updateAvailable'), + detail: this.i18nService.t('updateAvailableDesc'), + buttons: [this.i18nService.t('yes'), this.i18nService.t('no')], + cancelId: 1, + defaultId: 0, + noLink: true, + }); + + if (result === 0) { + autoUpdater.downloadUpdate(); + } else { + this.reset(); + } + } + }); + + autoUpdater.on('update-not-available', () => { + if (this.doingUpdateCheckWithFeedback && this.windowMain.win != null) { + dialog.showMessageBox(this.windowMain.win, { + message: this.i18nService.t('noUpdatesAvailable'), + buttons: [this.i18nService.t('ok')], + defaultId: 0, + noLink: true, + }); + } + + this.reset(); + }); + + autoUpdater.on('update-downloaded', (info) => { + if (this.onUpdateDownloaded != null) { + this.onUpdateDownloaded(); + } + + if (this.windowMain.win == null) { + return; + } + + const result = dialog.showMessageBox(this.windowMain.win, { + type: 'info', + title: this.i18nService.t('restartToUpdate'), + message: this.i18nService.t('restartToUpdate'), + detail: this.i18nService.t('restartToUpdateDesc', info.version), + buttons: [this.i18nService.t('restart'), this.i18nService.t('later')], + cancelId: 1, + defaultId: 0, + noLink: true, + }); + + if (result === 0) { + autoUpdater.quitAndInstall(false, true); + } + }); + + autoUpdater.on('error', (error) => { + if (this.doingUpdateCheckWithFeedback) { + dialog.showErrorBox(this.i18nService.t('updateError'), + error == null ? this.i18nService.t('unknown') : (error.stack || error).toString()); + } + + this.reset(); + }); + } + + async checkForUpdate(withFeedback: boolean = false) { + if (this.doingUpdateCheck || isDev()) { + return; + } + + if (!this.canUpdate) { + if (withFeedback) { + shell.openExternal('https://github.com/bitwarden/' + this.gitHubProject + '/releases'); + } + + return; + } + + this.doingUpdateCheckWithFeedback = withFeedback; + if (withFeedback) { + autoUpdater.autoDownload = false; + } + + await autoUpdater.checkForUpdates(); + } + + private reset() { + if (this.onReset != null) { + this.onReset(); + } + autoUpdater.autoDownload = true; + this.doingUpdateCheck = false; + } +} From 36dfdf24f146062447d2b8884fbf84981ecff664 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 4 May 2018 15:57:54 -0400 Subject: [PATCH 0224/1626] pass in default width/height of window --- src/electron/window.main.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index 87f9fed6fc..5c462eeb2f 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -17,7 +17,7 @@ export class WindowMain { private windowStateChangeTimer: NodeJS.Timer; private windowStates: { [key: string]: any; } = {}; - constructor(private storageService: StorageService) { } + constructor(private storageService: StorageService, private defaultWidth = 950, private defaultHeight: 600) { } init(): Promise { return new Promise((resolve, reject) => { @@ -73,7 +73,8 @@ export class WindowMain { } private async createWindow() { - this.windowStates[Keys.mainWindowSize] = await this.getWindowState(Keys.mainWindowSize, 950, 600); + this.windowStates[Keys.mainWindowSize] = await this.getWindowState(Keys.mainWindowSize, this.defaultWidth, + this.defaultHeight); // Create the browser window. this.win = new BrowserWindow({ From 61e13daefafe53c4c4b33fa9419052bfc2cfc9cd Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 4 May 2018 17:33:06 -0400 Subject: [PATCH 0225/1626] proper default height --- src/electron/window.main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index 5c462eeb2f..d5f9302424 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -17,7 +17,7 @@ export class WindowMain { private windowStateChangeTimer: NodeJS.Timer; private windowStates: { [key: string]: any; } = {}; - constructor(private storageService: StorageService, private defaultWidth = 950, private defaultHeight: 600) { } + constructor(private storageService: StorageService, private defaultWidth = 950, private defaultHeight = 600) { } init(): Promise { return new Promise((resolve, reject) => { From c6c5dd6d46526a36b65de3f5069e56d131843f7d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 5 May 2018 00:28:33 -0400 Subject: [PATCH 0226/1626] electron constants --- src/electron/electronConstants.ts | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/electron/electronConstants.ts diff --git a/src/electron/electronConstants.ts b/src/electron/electronConstants.ts new file mode 100644 index 0000000000..c22e2b5640 --- /dev/null +++ b/src/electron/electronConstants.ts @@ -0,0 +1,4 @@ +export class ElectronConstants { + static readonly enableMinimizeToTrayKey: string = 'enableMinimizeToTray'; + static readonly enableTrayKey: string = 'enableTray'; +} From de4494e1b3d884e84ae2ba07b16f0c6968da9edc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 7 May 2018 09:00:49 -0400 Subject: [PATCH 0227/1626] fast decrypt --- .../nodeCryptoFunction.service.spec.ts | 16 +-- .../webCryptoFunction.service.spec.ts | 22 ++-- src/abstractions/crypto.service.ts | 1 - src/abstractions/cryptoFunction.service.ts | 11 +- src/models/domain/decryptParameters.ts | 8 ++ src/models/domain/symmetricCryptoKey.ts | 13 ++- src/services/crypto.service.ts | 102 ++++++++---------- src/services/nodeCryptoFunction.service.ts | 57 +++++++++- src/services/webCryptoFunction.service.ts | 91 ++++++++++++++-- 9 files changed, 234 insertions(+), 87 deletions(-) create mode 100644 src/models/domain/decryptParameters.ts diff --git a/spec/node/services/nodeCryptoFunction.service.spec.ts b/spec/node/services/nodeCryptoFunction.service.spec.ts index 3964d39ae5..d4e33b4e0c 100644 --- a/spec/node/services/nodeCryptoFunction.service.spec.ts +++ b/spec/node/services/nodeCryptoFunction.service.spec.ts @@ -1,6 +1,7 @@ import { NodeCryptoFunctionService } from '../../../src/services/nodeCryptoFunction.service'; import { Utils } from '../../../src/misc/utils'; +import { SymmetricCryptoKey } from '../../../src/models/domain/symmetricCryptoKey'; const RsaPublicKey = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl0Vawl/toXzkEvB82FEtqHP' + '4xlU2ab/v0crqIfXfIoWF/XXdHGIdrZeilnRXPPJT1B9dTsasttEZNnua/0Rek/cjNDHtzT52irfoZYS7X6HNIfOi54Q+egP' + @@ -92,19 +93,20 @@ describe('NodeCrypto Function Service', () => { const value = 'EncryptMe!'; const data = Utils.fromUtf8ToArray(value); const encValue = await nodeCryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); - const decValue = await nodeCryptoFunctionService.aesDecryptSmall(encValue, iv.buffer, key.buffer); + const decValue = await nodeCryptoFunctionService.aesDecryptLarge(encValue, iv.buffer, key.buffer); expect(Utils.fromBufferToUtf8(decValue)).toBe(value); }); }); - describe('aesDecryptSmall', () => { + describe('aesDecryptFast', () => { it('should successfully decrypt data', async () => { const nodeCryptoFunctionService = new NodeCryptoFunctionService(); - const iv = makeStaticByteArray(16); - const key = makeStaticByteArray(32); - const data = Utils.fromB64ToArray('ByUF8vhyX4ddU9gcooznwA=='); - const decValue = await nodeCryptoFunctionService.aesDecryptSmall(data.buffer, iv.buffer, key.buffer); - expect(Utils.fromBufferToUtf8(decValue)).toBe('EncryptMe!'); + const iv = Utils.fromBufferToB64(makeStaticByteArray(16).buffer); + const symKey = new SymmetricCryptoKey(makeStaticByteArray(32).buffer); + const data = 'ByUF8vhyX4ddU9gcooznwA=='; + const params = nodeCryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey); + const decValue = await nodeCryptoFunctionService.aesDecryptFast(params); + expect(decValue).toBe('EncryptMe!'); }); }); diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index 386a56aebd..de3f2af126 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -7,6 +7,7 @@ import { PlatformUtilsService } from '../../../src/abstractions/platformUtils.se import { WebCryptoFunctionService } from '../../../src/services/webCryptoFunction.service'; import { Utils } from '../../../src/misc/utils'; +import { SymmetricCryptoKey } from '../../../src/models/domain'; const RsaPublicKey = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl0Vawl/toXzkEvB82FEtqHP' + '4xlU2ab/v0crqIfXfIoWF/XXdHGIdrZeilnRXPPJT1B9dTsasttEZNnua/0Rek/cjNDHtzT52irfoZYS7X6HNIfOi54Q+egP' + @@ -109,8 +110,12 @@ describe('WebCrypto Function Service', () => { const value = 'EncryptMe!'; const data = Utils.fromUtf8ToArray(value); const encValue = await webCryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); - const decValue = await webCryptoFunctionService.aesDecryptSmall(encValue, iv.buffer, key.buffer); - expect(Utils.fromBufferToUtf8(decValue)).toBe(value); + const encData = Utils.fromBufferToB64(encValue); + const b64Iv = Utils.fromBufferToB64(iv.buffer); + const symKey = new SymmetricCryptoKey(key.buffer); + const params = webCryptoFunctionService.aesDecryptFastParameters(encData, b64Iv, null, symKey); + const decValue = await webCryptoFunctionService.aesDecryptFast(params); + expect(decValue).toBe(value); }); it('should successfully encrypt and then decrypt large data', async () => { @@ -125,14 +130,15 @@ describe('WebCrypto Function Service', () => { }); }); - describe('aesDecryptSmall', () => { + describe('aesDecryptFast', () => { it('should successfully decrypt data', async () => { const webCryptoFunctionService = getWebCryptoFunctionService(); - const iv = makeStaticByteArray(16); - const key = makeStaticByteArray(32); - const data = Utils.fromB64ToArray('ByUF8vhyX4ddU9gcooznwA=='); - const decValue = await webCryptoFunctionService.aesDecryptSmall(data.buffer, iv.buffer, key.buffer); - expect(Utils.fromBufferToUtf8(decValue)).toBe('EncryptMe!'); + const iv = Utils.fromBufferToB64(makeStaticByteArray(16).buffer); + const symKey = new SymmetricCryptoKey(makeStaticByteArray(32).buffer); + const data = 'ByUF8vhyX4ddU9gcooznwA=='; + const params = webCryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey); + const decValue = await webCryptoFunctionService.aesDecryptFast(params); + expect(decValue).toBe('EncryptMe!'); }); }); diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index d2bb86d1eb..4220a36b90 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -27,7 +27,6 @@ export abstract class CryptoService { makeEncKey: (key: SymmetricCryptoKey) => Promise; encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise; encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise; - decrypt: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise; decryptToUtf8: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise; decryptFromBytes: (encBuf: ArrayBuffer, key: SymmetricCryptoKey) => Promise; randomNumber: (min: number, max: number) => Promise; diff --git a/src/abstractions/cryptoFunction.service.ts b/src/abstractions/cryptoFunction.service.ts index c8f5bdf5bd..8935d32aa7 100644 --- a/src/abstractions/cryptoFunction.service.ts +++ b/src/abstractions/cryptoFunction.service.ts @@ -1,10 +1,19 @@ +import { DecryptParameters } from '../models/domain/decryptParameters'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; + export abstract class CryptoFunctionService { pbkdf2: (password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', iterations: number) => Promise; hash: (value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise; hmac: (value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise; + timeSafeEqual: (a: ArrayBuffer, b: ArrayBuffer) => Promise; + hmacFast: (value: ArrayBuffer | string, key: ArrayBuffer | string, algorithm: 'sha1' | 'sha256' | 'sha512') => + Promise; + timeSafeEqualFast: (a: ArrayBuffer | string, b: ArrayBuffer | string) => Promise; aesEncrypt: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise; - aesDecryptSmall: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise; + aesDecryptFastParameters: (data: string, iv: string, mac: string, key: SymmetricCryptoKey) => + DecryptParameters; + aesDecryptFast: (parameters: DecryptParameters) => Promise; aesDecryptLarge: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise; rsaEncrypt: (data: ArrayBuffer, publicKey: ArrayBuffer, algorithm: 'sha1' | 'sha256') => Promise; rsaDecrypt: (data: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256') => Promise; diff --git a/src/models/domain/decryptParameters.ts b/src/models/domain/decryptParameters.ts new file mode 100644 index 0000000000..1274e88d4f --- /dev/null +++ b/src/models/domain/decryptParameters.ts @@ -0,0 +1,8 @@ +export class DecryptParameters { + encKey: T; + data: T; + iv: T; + macKey: T; + mac: T; + macData: T; +} diff --git a/src/models/domain/symmetricCryptoKey.ts b/src/models/domain/symmetricCryptoKey.ts index dd7a6800d0..f2b593d402 100644 --- a/src/models/domain/symmetricCryptoKey.ts +++ b/src/models/domain/symmetricCryptoKey.ts @@ -9,6 +9,8 @@ export class SymmetricCryptoKey { encType: EncryptionType; keyB64: string; + encKeyB64: string; + macKeyB64: string; constructor(key: ArrayBuffer, encType?: EncryptionType) { if (key == null) { @@ -26,7 +28,6 @@ export class SymmetricCryptoKey { } this.key = key; - this.keyB64 = Utils.fromBufferToB64(key); this.encType = encType; if (encType === EncryptionType.AesCbc256_B64 && key.byteLength === 32) { @@ -41,5 +42,15 @@ export class SymmetricCryptoKey { } else { throw new Error('Unsupported encType/key length.'); } + + if (this.key != null) { + this.keyB64 = Utils.fromBufferToB64(this.key); + } + if (this.encKey != null) { + this.encKeyB64 = Utils.fromBufferToB64(this.encKey); + } + if (this.macKey != null) { + this.macKeyB64 = Utils.fromBufferToB64(this.macKey); + } } } diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 3055953df3..7d5a56a754 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -304,7 +304,7 @@ export class CryptoService implements CryptoServiceAbstraction { const iv = Utils.fromB64ToArray(cipherString.initializationVector).buffer; const ct = Utils.fromB64ToArray(cipherString.cipherText).buffer; const mac = cipherString.mac ? Utils.fromB64ToArray(cipherString.mac).buffer : null; - const decipher = await this.aesDecrypt(cipherString.encryptionType, ct, iv, mac, key); + const decipher = await this.aesDecryptToBytes(cipherString.encryptionType, ct, iv, mac, key); if (decipher == null) { return null; } @@ -313,8 +313,8 @@ export class CryptoService implements CryptoServiceAbstraction { } async decryptToUtf8(cipherString: CipherString, key?: SymmetricCryptoKey): Promise { - const decipher = await this.decrypt(cipherString, key); - return Utils.fromBufferToUtf8(decipher); + return await this.aesDecryptToUtf8(cipherString.encryptionType, cipherString.cipherText, + cipherString.initializationVector, cipherString.mac, key); } async decryptFromBytes(encBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise { @@ -351,7 +351,7 @@ export class CryptoService implements CryptoServiceAbstraction { return null; } - return await this.aesDecryptLarge(encType, ctBytes.buffer, ivBytes.buffer, + return await this.aesDecryptToBytes(encType, ctBytes.buffer, ivBytes.buffer, macBytes != null ? macBytes.buffer : null, key); } @@ -409,8 +409,8 @@ export class CryptoService implements CryptoServiceAbstraction { return obj; } - private async aesDecrypt(encType: EncryptionType, ct: ArrayBuffer, iv: ArrayBuffer, mac: ArrayBuffer, - key: SymmetricCryptoKey): Promise { + private async aesDecryptToUtf8(encType: EncryptionType, ct: string, iv: string, mac: string, + key: SymmetricCryptoKey): Promise { const keyForEnc = await this.getKeyForEncryption(key); const theKey = this.resolveLegacyKey(encType, keyForEnc); @@ -420,51 +420,57 @@ export class CryptoService implements CryptoServiceAbstraction { return null; } - if (encType !== theKey.encType) { + if (theKey.encType !== encType) { // tslint:disable-next-line console.error('encType unavailable.'); return null; } + const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(ct, iv, mac, theKey); + if (fastParams.macKey != null && fastParams.mac != null) { + const computedMac = await this.cryptoFunctionService.hmacFast(fastParams.macData, + fastParams.macKey, 'sha256'); + const macsEqual = await this.cryptoFunctionService.timeSafeEqualFast(fastParams.mac, computedMac); + if (!macsEqual) { + // tslint:disable-next-line + console.error('mac failed.'); + return null; + } + } + + return this.cryptoFunctionService.aesDecryptFast(fastParams); + } + + private async aesDecryptToBytes(encType: EncryptionType, ct: ArrayBuffer, iv: ArrayBuffer, + mac: ArrayBuffer, key: SymmetricCryptoKey): Promise { + const keyForEnc = await this.getKeyForEncryption(key); + const theKey = this.resolveLegacyKey(encType, keyForEnc); + + if (theKey.macKey != null && mac == null) { + return null; + } + + if (theKey.encType !== encType) { + return null; + } + if (theKey.macKey != null && mac != null) { const macData = new Uint8Array(iv.byteLength + ct.byteLength); macData.set(new Uint8Array(iv), 0); macData.set(new Uint8Array(ct), iv.byteLength); - const computedMac = await this.cryptoFunctionService.hmac(new Uint8Array(iv).buffer, - theKey.macKey, 'sha256'); - if (!this.macsEqual(computedMac, mac)) { + const computedMac = await this.cryptoFunctionService.hmac(macData.buffer, theKey.macKey, 'sha256'); + if (computedMac === null) { + return null; + } + + const macsMatch = await this.cryptoFunctionService.timeSafeEqual(mac, computedMac); + if (!macsMatch) { // tslint:disable-next-line console.error('mac failed.'); return null; } } - return this.cryptoFunctionService.aesDecryptSmall(ct, iv, theKey.encKey); - } - - private async aesDecryptLarge(encType: EncryptionType, ct: ArrayBuffer, iv: ArrayBuffer, - mac: ArrayBuffer, key: SymmetricCryptoKey): Promise { - const theKey = await this.getKeyForEncryption(key); - if (theKey.macKey == null || mac == null) { - return null; - } - - const macData = new Uint8Array(iv.byteLength + ct.byteLength); - macData.set(new Uint8Array(iv), 0); - macData.set(new Uint8Array(ct), iv.byteLength); - const computedMac = await this.cryptoFunctionService.hmac(new Uint8Array(iv).buffer, - theKey.macKey, 'sha256'); - if (computedMac === null) { - return null; - } - - const macsMatch = await this.macsEqual(mac, computedMac); - if (macsMatch === false) { - // tslint:disable-next-line - console.error('mac failed.'); - return null; - } - return await this.cryptoFunctionService.aesDecryptLarge(ct, iv, theKey.encKey); } @@ -509,7 +515,7 @@ export class CryptoService implements CryptoServiceAbstraction { if (key != null && key.macKey != null && encPieces.length > 1) { const mac = Utils.fromB64ToArray(encPieces[1]).buffer; const computedMac = await this.cryptoFunctionService.hmac(ct, key.macKey, 'sha256'); - const macsEqual = await this.macsEqual(mac, computedMac); + const macsEqual = await this.cryptoFunctionService.timeSafeEqual(mac, computedMac); if (!macsEqual) { throw new Error('MAC failed.'); } @@ -536,28 +542,6 @@ export class CryptoService implements CryptoServiceAbstraction { return this.cryptoFunctionService.rsaDecrypt(ct, privateKey, alg); } - // 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/ - // ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy - private async macsEqual(mac1: ArrayBuffer, mac2: ArrayBuffer): Promise { - const key = await this.cryptoFunctionService.randomBytes(32); - const newMac1 = await this.cryptoFunctionService.hmac(mac1, key, 'sha256'); - const newMac2 = await this.cryptoFunctionService.hmac(mac2, key, 'sha256'); - if (newMac1.byteLength !== newMac2.byteLength) { - return false; - } - - const arr1 = new Uint8Array(newMac1); - const arr2 = new Uint8Array(newMac2); - 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 != null) { return key; diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index 1739df9987..c4fdc89ccd 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -6,6 +6,9 @@ import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; import { Utils } from '../misc/utils'; +import { DecryptParameters } from '../models/domain/decryptParameters'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; + export class NodeCryptoFunctionService implements CryptoFunctionService { pbkdf2(password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', iterations: number): Promise { @@ -38,6 +41,33 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { return Promise.resolve(this.toArrayBuffer(hmac.digest())); } + async timeSafeEqual(a: ArrayBuffer, b: ArrayBuffer): Promise { + const key = await this.randomBytes(32); + const mac1 = await this.hmac(a, key, 'sha256'); + const mac2 = await this.hmac(b, key, 'sha256'); + 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; + } + + hmacFast(value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { + return this.hmac(value, key, algorithm); + } + + timeSafeEqualFast(a: ArrayBuffer, b: ArrayBuffer): Promise { + return this.timeSafeEqual(a, b); + } + aesEncrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { const nodeData = this.toNodeBuffer(data); const nodeIv = this.toNodeBuffer(iv); @@ -47,8 +77,31 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { return Promise.resolve(this.toArrayBuffer(encBuf)); } - aesDecryptSmall(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { - return this.aesDecryptLarge(data, iv, key); + aesDecryptFastParameters(data: string, iv: string, mac: string, key: SymmetricCryptoKey): + DecryptParameters { + const p = new DecryptParameters(); + p.encKey = key.encKey; + p.data = Utils.fromB64ToArray(data).buffer; + p.iv = Utils.fromB64ToArray(iv).buffer; + + const macData = new Uint8Array(p.iv.byteLength + p.data.byteLength); + macData.set(new Uint8Array(p.iv), 0); + macData.set(new Uint8Array(p.data), p.iv.byteLength); + p.macData = macData.buffer; + + if (key.macKey != null) { + p.macKey = key.macKey; + } + if (mac != null) { + p.mac = Utils.fromB64ToArray(mac).buffer; + } + + return p; + } + + async aesDecryptFast(parameters: DecryptParameters): Promise { + const decBuf = await this.aesDecryptLarge(parameters.data, parameters.iv, parameters.encKey); + return Utils.fromBufferToUtf8(decBuf); } aesDecryptLarge(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index dd4a33c62b..da1c18cdce 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -5,6 +5,9 @@ import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { Utils } from '../misc/utils'; +import { SymmetricCryptoKey } from '../models/domain'; +import { DecryptParameters } from '../models/domain/decryptParameters'; + export class WebCryptoFunctionService implements CryptoFunctionService { private crypto: Crypto; private subtle: SubtleCrypto; @@ -80,21 +83,93 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return await this.subtle.sign(signingAlgorithm, impKey, value); } + // Safely compare two values 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/ + // ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy + async timeSafeEqual(a: ArrayBuffer, b: ArrayBuffer): Promise { + const macKey = await this.randomBytes(32); + const signingAlgorithm = { + name: 'HMAC', + hash: { name: 'SHA-256' }, + }; + const impKey = await this.subtle.importKey('raw', macKey, signingAlgorithm, false, ['sign']); + const mac1 = await this.subtle.sign(signingAlgorithm, impKey, a); + const mac2 = await this.subtle.sign(signingAlgorithm, impKey, b); + + 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; + } + + hmacFast(value: string, key: string, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { + const hmac = (forge as any).hmac.create(); + hmac.start(algorithm, key); + hmac.update(value); + const bytes = hmac.digest().getBytes(); + return Promise.resolve(bytes); + } + + async timeSafeEqualFast(a: string, b: string): Promise { + const rand = await this.randomBytes(32); + const bytes = new Uint32Array(rand); + const buffer = forge.util.createBuffer(); + for (let i = 0; i < bytes.length; i++) { + buffer.putInt32(bytes[i]); + } + const macKey = buffer.getBytes(); + + const hmac = (forge as any).hmac.create(); + hmac.start('sha256', macKey); + hmac.update(a); + const mac1 = hmac.digest().getBytes(); + + hmac.start(null, null); + hmac.update(b); + const mac2 = hmac.digest().getBytes(); + + const equals = mac1 === mac2; + return equals; + } + async aesEncrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { const impKey = await this.subtle.importKey('raw', key, { name: 'AES-CBC' }, false, ['encrypt']); return await this.subtle.encrypt({ name: 'AES-CBC', iv: iv }, impKey, data); } - async aesDecryptSmall(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { - const dataBytes = this.toByteString(data); - const ivBytes = this.toByteString(iv); - const keyBytes = this.toByteString(key); - const dataBuffer = (forge as any).util.createBuffer(dataBytes); - const decipher = (forge as any).cipher.createDecipher('AES-CBC', keyBytes); - decipher.start({ iv: ivBytes }); + aesDecryptFastParameters(data: string, iv: string, mac: string, key: SymmetricCryptoKey): + DecryptParameters { + const p = new DecryptParameters(); + p.encKey = forge.util.decode64(key.encKeyB64); + p.data = forge.util.decode64(data); + p.iv = forge.util.decode64(iv); + p.macData = p.iv + p.data; + if (key.macKeyB64 != null) { + p.macKey = forge.util.decode64(key.macKeyB64); + } + if (mac != null) { + p.mac = forge.util.decode64(mac); + } + return p; + } + + aesDecryptFast(parameters: DecryptParameters): Promise { + const dataBuffer = (forge as any).util.createBuffer(parameters.data); + const decipher = (forge as any).cipher.createDecipher('AES-CBC', parameters.encKey); + decipher.start({ iv: parameters.iv }); decipher.update(dataBuffer); decipher.finish(); - return Utils.fromByteStringToArray(decipher.output.getBytes()).buffer; + const val = decipher.output.toString('utf8'); + return Promise.resolve(val); } async aesDecryptLarge(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { From 87ac298af90159338e5a8793907e7ccd73d87e79 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 7 May 2018 11:43:14 -0400 Subject: [PATCH 0228/1626] web crypto testing. more test browsers --- package-lock.json | 36 +++ package.json | 4 + spec/common/misc/utils.spec.ts | 9 +- spec/support/karma.conf.js | 28 ++- .../webCryptoFunction.service.spec.ts | 233 ++++++++++++------ src/services/webCryptoFunction.service.ts | 21 +- 6 files changed, 227 insertions(+), 104 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1343aecc53..9e5eb8f861 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1857,6 +1857,12 @@ "jsbn": "0.1.1" } }, + "edge-launcher": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/edge-launcher/-/edge-launcher-1.2.2.tgz", + "integrity": "sha1-60Cq+9Bnpup27/+rBke81VCbN7I=", + "dev": true + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -5073,6 +5079,30 @@ } } }, + "karma-detect-browsers": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/karma-detect-browsers/-/karma-detect-browsers-2.3.2.tgz", + "integrity": "sha512-EFku2S5IpUEpJR2XxJa/onW6tIuapa3kYWJDD7Tk6LqhhIxfKWvJ+vnleLop6utXT28204hZptnfH7PGSmk4Nw==", + "dev": true, + "requires": { + "which": "1.3.0" + } + }, + "karma-edge-launcher": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/karma-edge-launcher/-/karma-edge-launcher-0.4.2.tgz", + "integrity": "sha512-YAJZb1fmRcxNhMIWYsjLuxwODBjh2cSHgTW/jkVmdpGguJjLbs9ZgIK/tEJsMQcBLUkO+yO4LBbqYxqgGW2HIw==", + "dev": true, + "requires": { + "edge-launcher": "1.2.2" + } + }, + "karma-firefox-launcher": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-1.1.0.tgz", + "integrity": "sha512-LbZ5/XlIXLeQ3cqnCbYLn+rOVhuMIK9aZwlP6eOLGzWdo1UVp7t6CN3DP4SafiRLjexKwHeKHDm0c38Mtd3VxA==", + "dev": true + }, "karma-jasmine": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.1.1.tgz", @@ -5088,6 +5118,12 @@ "karma-jasmine": "1.1.1" } }, + "karma-safari-launcher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/karma-safari-launcher/-/karma-safari-launcher-1.0.0.tgz", + "integrity": "sha1-lpgqLMR9BmquccVTursoMZEVos4=", + "dev": true + }, "karma-typescript": { "version": "3.0.12", "resolved": "https://registry.npmjs.org/karma-typescript/-/karma-typescript-3.0.12.tgz", diff --git a/package.json b/package.json index 4425fdb0c9..10e66ab3bc 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,12 @@ "karma-chrome-launcher": "^2.2.0", "karma-cli": "^1.0.1", "karma-coverage-istanbul-reporter": "^1.3.0", + "karma-detect-browsers": "^2.3.2", + "karma-edge-launcher": "^0.4.2", + "karma-firefox-launcher": "^1.1.0", "karma-jasmine": "^1.1.0", "karma-jasmine-html-reporter": "^0.2.2", + "karma-safari-launcher": "^1.0.0", "karma-typescript": "^3.0.8", "nodemon": "^1.17.3", "rimraf": "^2.6.2", diff --git a/spec/common/misc/utils.spec.ts b/spec/common/misc/utils.spec.ts index 28fcbd2398..adbe753718 100644 --- a/spec/common/misc/utils.spec.ts +++ b/spec/common/misc/utils.spec.ts @@ -15,8 +15,13 @@ describe('Utils Service', () => { expect(Utils.getHostname('https://bitwarden.com')).toBe('bitwarden.com'); expect(Utils.getHostname('http://bitwarden.com')).toBe('bitwarden.com'); expect(Utils.getHostname('http://vault.bitwarden.com')).toBe('vault.bitwarden.com'); - expect(Utils.getHostname('https://user:password@bitwarden.com:8080/password/sites?and&query#hash')) - .toBe('bitwarden.com'); + + if (Utils.isNode || window.navigator.userAgent.indexOf(' Edge/') === -1) { + // Note: Broken in Edge browser. See + // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8004284/ + expect(Utils.getHostname('https://user:password@bitwarden.com:8080/password/sites?and&query#hash')) + .toBe('bitwarden.com'); + } }); it('should support localhost and IP', () => { diff --git a/spec/support/karma.conf.js b/spec/support/karma.conf.js index d2e6d219ae..d177dd699e 100644 --- a/spec/support/karma.conf.js +++ b/spec/support/karma.conf.js @@ -1,11 +1,11 @@ -module.exports = function(config) { +module.exports = (config) => { config.set({ // base path that will be used to resolve all patterns (eg. files, exclude) basePath: '../../', // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['jasmine', 'karma-typescript'], + frameworks: ['jasmine', 'karma-typescript', 'detectBrowsers'], // list of files / patterns to load in the browser files: [ @@ -43,15 +43,11 @@ module.exports = function(config) { // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG logLevel: config.LOG_INFO, - // start these browsers - // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - browsers: ['Chrome'], - // Concurrency level // how many browser should be started simultaneous concurrency: Infinity, - client:{ + client: { clearContext: false // leave Jasmine Spec Runner output visible in browser }, @@ -62,5 +58,23 @@ module.exports = function(config) { sourceMap: true } }, + + detectBrowsers: { + usePhantomJS: false, + postDetection: (availableBrowsers) => { + const result = availableBrowsers; + function removeBrowser(browser) { + if (availableBrowsers.length > 1 && availableBrowsers.indexOf(browser) > -1) { + result.splice(result.indexOf(browser), 1); + } + } + + removeBrowser('IE'); + removeBrowser('Opera'); + removeBrowser('SafariTechPreview'); + + return result; + } + }, }) } diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index de3f2af126..3993f42b9d 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -46,11 +46,8 @@ describe('WebCrypto Function Service', () => { const unicode512Key = 'FE+AnUJaxv8jh+zUDtZz4mjjcYk0/PZDZm+SLJe3XtxtnpdqqpblX6JjuMZt/dYYNMOrb2+mD' + 'L3FiQDTROh1lg=='; - testPbkdf2(false, 'sha256', regular256Key, utf8256Key, unicode256Key); - testPbkdf2(false, 'sha512', regular512Key, utf8512Key, unicode512Key); - - testPbkdf2(true, 'sha256', regular256Key, utf8256Key, unicode256Key); - testPbkdf2(true, 'sha512', regular512Key, utf8512Key, unicode512Key); + testPbkdf2('sha256', regular256Key, utf8256Key, unicode256Key); + testPbkdf2('sha512', regular512Key, utf8512Key, unicode512Key); }); describe('hash', () => { @@ -69,13 +66,9 @@ describe('WebCrypto Function Service', () => { const unicode512Hash = '2b16a5561af8ad6fe414cc103fc8036492e1fc6d9aabe1b655497054f760fe0e34c5d100ac773d' + '9f3030438284f22dbfa20cb2e9b019f2c98dfe38ce1ef41bae'; - testHash(false, 'sha1', regular1Hash, utf81Hash, unicode1Hash); - testHash(false, 'sha256', regular256Hash, utf8256Hash, unicode256Hash); - testHash(false, 'sha512', regular512Hash, utf8512Hash, unicode512Hash); - - testHash(true, 'sha1', regular1Hash, utf81Hash, unicode1Hash); - testHash(true, 'sha256', regular256Hash, utf8256Hash, unicode256Hash); - testHash(true, 'sha512', regular512Hash, utf8512Hash, unicode512Hash); + testHash('sha1', regular1Hash, utf81Hash, unicode1Hash); + testHash('sha256', regular256Hash, utf8256Hash, unicode256Hash); + testHash('sha512', regular512Hash, utf8512Hash, unicode512Hash); }); describe('hmac', () => { @@ -84,120 +77,199 @@ describe('WebCrypto Function Service', () => { const sha512Mac = '21910e341fa12106ca35758a2285374509326c9fbe0bd64e7b99c898f841dc948c58ce66d3504d8883c' + '5ea7817a0b7c5d4d9b00364ccd214669131fc17fe4aca'; - testHmac(false, 'sha1', sha1Mac); - testHmac(false, 'sha256', sha256Mac); - testHmac(false, 'sha512', sha512Mac); + testHmac('sha1', sha1Mac); + testHmac('sha256', sha256Mac); + testHmac('sha512', sha512Mac); + }); - testHmac(true, 'sha1', sha1Mac); - testHmac(true, 'sha256', sha256Mac); - testHmac(true, 'sha512', sha512Mac); + describe('timeSafeEqual', () => { + it('should successfully compare two of the same values', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const a = new Uint8Array(2); + a[0] = 1; + a[1] = 2; + const equal = await cryptoFunctionService.timeSafeEqual(a.buffer, a.buffer); + expect(equal).toBe(true); + }); + + it('should successfully compare two different values of the same length', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const a = new Uint8Array(2); + a[0] = 1; + a[1] = 2; + const b = new Uint8Array(2); + b[0] = 3; + b[1] = 4; + const equal = await cryptoFunctionService.timeSafeEqual(a.buffer, b.buffer); + expect(equal).toBe(false); + }); + + it('should successfully compare two different values of different lengths', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const a = new Uint8Array(2); + a[0] = 1; + a[1] = 2; + const b = new Uint8Array(2); + b[0] = 3; + const equal = await cryptoFunctionService.timeSafeEqual(a.buffer, b.buffer); + expect(equal).toBe(false); + }); + }); + + describe('hmacFast', () => { + const sha1Mac = '4d4c223f95dc577b665ec4ccbcb680b80a397038'; + const sha256Mac = '6be3caa84922e12aaaaa2f16c40d44433bb081ef323db584eb616333ab4e874f'; + const sha512Mac = '21910e341fa12106ca35758a2285374509326c9fbe0bd64e7b99c898f841dc948c58ce66d3504d8883c' + + '5ea7817a0b7c5d4d9b00364ccd214669131fc17fe4aca'; + + testHmacFast('sha1', sha1Mac); + testHmacFast('sha256', sha256Mac); + testHmacFast('sha512', sha512Mac); + }); + + describe('timeSafeEqualFast', () => { + it('should successfully compare two of the same values', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const a = new Uint8Array(2); + a[0] = 1; + a[1] = 2; + const aByteString = Utils.fromBufferToByteString(a.buffer); + const equal = await cryptoFunctionService.timeSafeEqualFast(aByteString, aByteString); + expect(equal).toBe(true); + }); + + it('should successfully compare two different values of the same length', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const a = new Uint8Array(2); + a[0] = 1; + a[1] = 2; + const aByteString = Utils.fromBufferToByteString(a.buffer); + const b = new Uint8Array(2); + b[0] = 3; + b[1] = 4; + const bByteString = Utils.fromBufferToByteString(b.buffer); + const equal = await cryptoFunctionService.timeSafeEqualFast(aByteString, bByteString); + expect(equal).toBe(false); + }); + + it('should successfully compare two different values of different lengths', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const a = new Uint8Array(2); + a[0] = 1; + a[1] = 2; + const aByteString = Utils.fromBufferToByteString(a.buffer); + const b = new Uint8Array(2); + b[0] = 3; + const bByteString = Utils.fromBufferToByteString(b.buffer); + const equal = await cryptoFunctionService.timeSafeEqualFast(aByteString, bByteString); + expect(equal).toBe(false); + }); }); describe('aesEncrypt', () => { it('should successfully encrypt data', async () => { - const webCryptoFunctionService = getWebCryptoFunctionService(); + const cryptoFunctionService = getWebCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); const data = Utils.fromUtf8ToArray('EncryptMe!'); - const encValue = await webCryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); + const encValue = await cryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); expect(Utils.fromBufferToB64(encValue)).toBe('ByUF8vhyX4ddU9gcooznwA=='); }); it('should successfully encrypt and then decrypt small data', async () => { - const webCryptoFunctionService = getWebCryptoFunctionService(); + const cryptoFunctionService = getWebCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); const value = 'EncryptMe!'; const data = Utils.fromUtf8ToArray(value); - const encValue = await webCryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); + const encValue = await cryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); const encData = Utils.fromBufferToB64(encValue); const b64Iv = Utils.fromBufferToB64(iv.buffer); const symKey = new SymmetricCryptoKey(key.buffer); - const params = webCryptoFunctionService.aesDecryptFastParameters(encData, b64Iv, null, symKey); - const decValue = await webCryptoFunctionService.aesDecryptFast(params); + const params = cryptoFunctionService.aesDecryptFastParameters(encData, b64Iv, null, symKey); + const decValue = await cryptoFunctionService.aesDecryptFast(params); expect(decValue).toBe(value); }); it('should successfully encrypt and then decrypt large data', async () => { - const webCryptoFunctionService = getWebCryptoFunctionService(); + const cryptoFunctionService = getWebCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); const value = 'EncryptMe!'; const data = Utils.fromUtf8ToArray(value); - const encValue = await webCryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); - const decValue = await webCryptoFunctionService.aesDecryptLarge(encValue, iv.buffer, key.buffer); + const encValue = await cryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); + const decValue = await cryptoFunctionService.aesDecryptLarge(encValue, iv.buffer, key.buffer); expect(Utils.fromBufferToUtf8(decValue)).toBe(value); }); }); describe('aesDecryptFast', () => { it('should successfully decrypt data', async () => { - const webCryptoFunctionService = getWebCryptoFunctionService(); + const cryptoFunctionService = getWebCryptoFunctionService(); const iv = Utils.fromBufferToB64(makeStaticByteArray(16).buffer); const symKey = new SymmetricCryptoKey(makeStaticByteArray(32).buffer); const data = 'ByUF8vhyX4ddU9gcooznwA=='; - const params = webCryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey); - const decValue = await webCryptoFunctionService.aesDecryptFast(params); + const params = cryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey); + const decValue = await cryptoFunctionService.aesDecryptFast(params); expect(decValue).toBe('EncryptMe!'); }); }); describe('aesDecryptLarge', () => { it('should successfully decrypt data', async () => { - const webCryptoFunctionService = getWebCryptoFunctionService(); + const cryptoFunctionService = getWebCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); const data = Utils.fromB64ToArray('ByUF8vhyX4ddU9gcooznwA=='); - const decValue = await webCryptoFunctionService.aesDecryptLarge(data.buffer, iv.buffer, key.buffer); + const decValue = await cryptoFunctionService.aesDecryptLarge(data.buffer, iv.buffer, key.buffer); expect(Utils.fromBufferToUtf8(decValue)).toBe('EncryptMe!'); }); }); describe('rsaEncrypt', () => { it('should successfully encrypt and then decrypt data', async () => { - const webCryptoFunctionService = getWebCryptoFunctionService(); + const cryptoFunctionService = getWebCryptoFunctionService(); const pubKey = Utils.fromB64ToArray(RsaPublicKey); const privKey = Utils.fromB64ToArray(RsaPrivateKey); const value = 'EncryptMe!'; const data = Utils.fromUtf8ToArray(value); - const encValue = await webCryptoFunctionService.rsaEncrypt(data.buffer, pubKey.buffer, 'sha1'); - const decValue = await webCryptoFunctionService.rsaDecrypt(encValue, privKey.buffer, 'sha1'); + const encValue = await cryptoFunctionService.rsaEncrypt(data.buffer, pubKey.buffer, 'sha1'); + const decValue = await cryptoFunctionService.rsaDecrypt(encValue, privKey.buffer, 'sha1'); expect(Utils.fromBufferToUtf8(decValue)).toBe(value); }); }); describe('rsaDecrypt', () => { it('should successfully decrypt data', async () => { - const webCryptoFunctionService = getWebCryptoFunctionService(); + const cryptoFunctionService = getWebCryptoFunctionService(); const privKey = Utils.fromB64ToArray(RsaPrivateKey); const data = Utils.fromB64ToArray('A1/p8BQzN9UrbdYxUY2Va5+kPLyfZXF9JsZrjeEXcaclsnHurdxVAJcnbEqYMP3UXV' + '4YAS/mpf+Rxe6/X0WS1boQdA0MAHSgx95hIlAraZYpiMLLiJRKeo2u8YivCdTM9V5vuAEJwf9Tof/qFsFci3sApdbATkorCT' + 'zFOIEPF2S1zgperEP23M01mr4dWVdYN18B32YF67xdJHMbFhp5dkQwv9CmscoWq7OE5HIfOb+JAh7BEZb+CmKhM3yWJvoR/D' + '/5jcercUtK2o+XrzNrL4UQ7yLZcFz6Bfwb/j6ICYvqd/YJwXNE6dwlL57OfwJyCdw2rRYf0/qI00t9u8Iitw=='); - const decValue = await webCryptoFunctionService.rsaDecrypt(data.buffer, privKey.buffer, 'sha1'); + const decValue = await cryptoFunctionService.rsaDecrypt(data.buffer, privKey.buffer, 'sha1'); expect(Utils.fromBufferToUtf8(decValue)).toBe('EncryptMe!'); }); }); describe('randomBytes', () => { it('should make a value of the correct length', async () => { - const webCryptoFunctionService = getWebCryptoFunctionService(); - const randomData = await webCryptoFunctionService.randomBytes(16); + const cryptoFunctionService = getWebCryptoFunctionService(); + const randomData = await cryptoFunctionService.randomBytes(16); expect(randomData.byteLength).toBe(16); }); it('should not make the same value twice', async () => { - const webCryptoFunctionService = getWebCryptoFunctionService(); - const randomData = await webCryptoFunctionService.randomBytes(16); - const randomData2 = await webCryptoFunctionService.randomBytes(16); + const cryptoFunctionService = getWebCryptoFunctionService(); + const randomData = await cryptoFunctionService.randomBytes(16); + const randomData2 = await cryptoFunctionService.randomBytes(16); expect(randomData.byteLength === randomData2.byteLength && randomData !== randomData2).toBeTruthy(); }); }); }); -function testPbkdf2(edge: boolean, algorithm: 'sha256' | 'sha512', regularKey: string, +function testPbkdf2(algorithm: 'sha256' | 'sha512', regularKey: string, utf8Key: string, unicodeKey: string) { - const forEdge = edge ? ' for edge' : ''; const regularEmail = 'user@example.com'; const utf8Email = 'üser@example.com'; @@ -205,76 +277,85 @@ function testPbkdf2(edge: boolean, algorithm: 'sha256' | 'sha512', regularKey: s const utf8Password = 'pǻssword'; const unicodePassword = '😀password🙏'; - it('should create valid ' + algorithm + ' key from regular input' + forEdge, async () => { - const webCryptoFunctionService = getWebCryptoFunctionService(edge); - const key = await webCryptoFunctionService.pbkdf2(regularPassword, regularEmail, algorithm, 5000); + it('should create valid ' + algorithm + ' key from regular input', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const key = await cryptoFunctionService.pbkdf2(regularPassword, regularEmail, algorithm, 5000); expect(Utils.fromBufferToB64(key)).toBe(regularKey); }); - it('should create valid ' + algorithm + ' key from utf8 input' + forEdge, async () => { - const webCryptoFunctionService = getWebCryptoFunctionService(edge); - const key = await webCryptoFunctionService.pbkdf2(utf8Password, utf8Email, algorithm, 5000); + it('should create valid ' + algorithm + ' key from utf8 input', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const key = await cryptoFunctionService.pbkdf2(utf8Password, utf8Email, algorithm, 5000); expect(Utils.fromBufferToB64(key)).toBe(utf8Key); }); - it('should create valid ' + algorithm + ' key from unicode input' + forEdge, async () => { - const webCryptoFunctionService = getWebCryptoFunctionService(edge); - const key = await webCryptoFunctionService.pbkdf2(unicodePassword, regularEmail, algorithm, 5000); + it('should create valid ' + algorithm + ' key from unicode input', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const key = await cryptoFunctionService.pbkdf2(unicodePassword, regularEmail, algorithm, 5000); expect(Utils.fromBufferToB64(key)).toBe(unicodeKey); }); - it('should create valid ' + algorithm + ' key from array buffer input' + forEdge, async () => { - const webCryptoFunctionService = getWebCryptoFunctionService(edge); - const key = await webCryptoFunctionService.pbkdf2(Utils.fromUtf8ToArray(regularPassword).buffer, + it('should create valid ' + algorithm + ' key from array buffer input', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const key = await cryptoFunctionService.pbkdf2(Utils.fromUtf8ToArray(regularPassword).buffer, Utils.fromUtf8ToArray(regularEmail).buffer, algorithm, 5000); expect(Utils.fromBufferToB64(key)).toBe(regularKey); }); } -function testHash(edge: boolean, algorithm: 'sha1' | 'sha256' | 'sha512', regularHash: string, +function testHash(algorithm: 'sha1' | 'sha256' | 'sha512', regularHash: string, utf8Hash: string, unicodeHash: string) { - const forEdge = edge ? ' for edge' : ''; const regularValue = 'HashMe!!'; const utf8Value = 'HǻshMe!!'; const unicodeValue = '😀HashMe!!!🙏'; - it('should create valid ' + algorithm + ' hash from regular input' + forEdge, async () => { - const webCryptoFunctionService = getWebCryptoFunctionService(edge); - const hash = await webCryptoFunctionService.hash(regularValue, algorithm); + it('should create valid ' + algorithm + ' hash from regular input', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const hash = await cryptoFunctionService.hash(regularValue, algorithm); expect(Utils.fromBufferToHex(hash)).toBe(regularHash); }); - it('should create valid ' + algorithm + ' hash from utf8 input' + forEdge, async () => { - const webCryptoFunctionService = getWebCryptoFunctionService(edge); - const hash = await webCryptoFunctionService.hash(utf8Value, algorithm); + it('should create valid ' + algorithm + ' hash from utf8 input', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const hash = await cryptoFunctionService.hash(utf8Value, algorithm); expect(Utils.fromBufferToHex(hash)).toBe(utf8Hash); }); - it('should create valid ' + algorithm + ' hash from unicode input' + forEdge, async () => { - const webCryptoFunctionService = getWebCryptoFunctionService(edge); - const hash = await webCryptoFunctionService.hash(unicodeValue, algorithm); + it('should create valid ' + algorithm + ' hash from unicode input', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const hash = await cryptoFunctionService.hash(unicodeValue, algorithm); expect(Utils.fromBufferToHex(hash)).toBe(unicodeHash); }); - it('should create valid ' + algorithm + ' hash from array buffer input' + forEdge, async () => { - const webCryptoFunctionService = getWebCryptoFunctionService(edge); - const hash = await webCryptoFunctionService.hash(Utils.fromUtf8ToArray(regularValue).buffer, algorithm); + it('should create valid ' + algorithm + ' hash from array buffer input', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const hash = await cryptoFunctionService.hash(Utils.fromUtf8ToArray(regularValue).buffer, algorithm); expect(Utils.fromBufferToHex(hash)).toBe(regularHash); }); } -function testHmac(edge: boolean, algorithm: 'sha1' | 'sha256' | 'sha512', mac: string) { - it('should create valid ' + algorithm + ' hmac' + (edge ? ' for edge' : ''), async () => { - const webCryptoFunctionService = getWebCryptoFunctionService(edge); - const computedMac = await webCryptoFunctionService.hmac(Utils.fromUtf8ToArray('SignMe!!').buffer, +function testHmac(algorithm: 'sha1' | 'sha256' | 'sha512', mac: string) { + it('should create valid ' + algorithm + ' hmac', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const computedMac = await cryptoFunctionService.hmac(Utils.fromUtf8ToArray('SignMe!!').buffer, Utils.fromUtf8ToArray('secretkey').buffer, algorithm); expect(Utils.fromBufferToHex(computedMac)).toBe(mac); }); } -function getWebCryptoFunctionService(edge = false) { +function testHmacFast(algorithm: 'sha1' | 'sha256' | 'sha512', mac: string) { + it('should create valid ' + algorithm + ' hmac', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const keyByteString = Utils.fromBufferToByteString(Utils.fromUtf8ToArray('secretkey').buffer); + const dataByteString = Utils.fromBufferToByteString(Utils.fromUtf8ToArray('SignMe!!').buffer); + const computedMac = await cryptoFunctionService.hmacFast(dataByteString, keyByteString, algorithm); + expect(Utils.fromBufferToHex(Utils.fromByteStringToArray(computedMac).buffer)).toBe(mac); + }); +} + +function getWebCryptoFunctionService() { const platformUtilsMock = TypeMoq.Mock.ofType(PlatformUtilsServiceMock); - platformUtilsMock.setup((x) => x.isEdge()).returns(() => edge); + platformUtilsMock.setup((x) => x.isEdge()).returns(() => navigator.userAgent.indexOf(' Edge/') !== -1); return new WebCryptoFunctionService(window, platformUtilsMock.object); } diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index da1c18cdce..8a0b169b0d 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -45,16 +45,8 @@ export class WebCryptoFunctionService implements CryptoFunctionService { } async hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { - if (this.isEdge) { - let md: forge.md.MessageDigest; - if (algorithm === 'sha1') { - md = forge.md.sha1.create(); - } else if (algorithm === 'sha256') { - md = forge.md.sha256.create(); - } else { - md = (forge as any).md.sha512.create(); - } - + if (this.isEdge && algorithm === 'sha1') { + const md = forge.md.sha1.create(); const valueBytes = this.toByteString(value); md.update(valueBytes, 'raw'); return Utils.fromByteStringToArray(md.digest().data).buffer; @@ -65,15 +57,6 @@ export class WebCryptoFunctionService implements CryptoFunctionService { } async hmac(value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { - if (this.isEdge) { - const valueBytes = this.toByteString(value); - const keyBytes = this.toByteString(key); - const hmac = (forge as any).hmac.create(); - hmac.start(algorithm, keyBytes); - hmac.update(valueBytes); - return Utils.fromByteStringToArray(hmac.digest().getBytes()).buffer; - } - const signingAlgorithm = { name: 'HMAC', hash: { name: this.toWebCryptoAlgorithm(algorithm) }, From dfcde8a29ad75ab0f2d6fd255244305c683e7777 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 7 May 2018 12:08:22 -0400 Subject: [PATCH 0229/1626] new tests for node --- .../nodeCryptoFunction.service.spec.ts | 77 ++++++++++++++++--- .../webCryptoFunction.service.spec.ts | 27 +++---- 2 files changed, 78 insertions(+), 26 deletions(-) diff --git a/spec/node/services/nodeCryptoFunction.service.spec.ts b/spec/node/services/nodeCryptoFunction.service.spec.ts index d4e33b4e0c..c86f97e8a2 100644 --- a/spec/node/services/nodeCryptoFunction.service.spec.ts +++ b/spec/node/services/nodeCryptoFunction.service.spec.ts @@ -27,6 +27,11 @@ const RsaPrivateKey = 'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX '+tPVgppLcG0+tMdLjigFQiDUQk2y3WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEzXKZ' + 'BokBGnjFnTnKcs7nv/O8='; +const Sha1Mac = '4d4c223f95dc577b665ec4ccbcb680b80a397038'; +const Sha256Mac = '6be3caa84922e12aaaaa2f16c40d44433bb081ef323db584eb616333ab4e874f'; +const Sha512Mac = '21910e341fa12106ca35758a2285374509326c9fbe0bd64e7b99c898f841dc948c58ce66d3504d8883c' + + '5ea7817a0b7c5d4d9b00364ccd214669131fc17fe4aca'; + describe('NodeCrypto Function Service', () => { describe('pbkdf2', () => { const regular256Key = 'pj9prw/OHPleXI6bRdmlaD+saJS4awrMiQsQiDjeu2I='; @@ -66,14 +71,23 @@ describe('NodeCrypto Function Service', () => { }); describe('hmac', () => { - const sha1Mac = '4d4c223f95dc577b665ec4ccbcb680b80a397038'; - const sha256Mac = '6be3caa84922e12aaaaa2f16c40d44433bb081ef323db584eb616333ab4e874f'; - const sha512Mac = '21910e341fa12106ca35758a2285374509326c9fbe0bd64e7b99c898f841dc948c58ce66d3504d8883c' + - '5ea7817a0b7c5d4d9b00364ccd214669131fc17fe4aca'; + testHmac('sha1', Sha1Mac); + testHmac('sha256', Sha256Mac); + testHmac('sha512', Sha512Mac); + }); - testHmac('sha1', sha1Mac); - testHmac('sha256', sha256Mac); - testHmac('sha512', sha512Mac); + describe('timeSafeEqual', () => { + testCompare(false); + }); + + describe('hmacFast', () => { + testHmac('sha1', Sha1Mac, true); + testHmac('sha256', Sha256Mac, true); + testHmac('sha512', Sha512Mac, true); + }); + + describe('timeSafeEqualFast', () => { + testCompare(true); }); describe('aesEncrypt', () => { @@ -227,15 +241,58 @@ function testHash(algorithm: 'sha1' | 'sha256' | 'sha512', regularHash: string, }); } -function testHmac(algorithm: 'sha1' | 'sha256' | 'sha512', mac: string) { +function testHmac(algorithm: 'sha1' | 'sha256' | 'sha512', mac: string, fast = false) { it('should create valid ' + algorithm + ' hmac', async () => { const cryptoFunctionService = new NodeCryptoFunctionService(); - const computedMac = await cryptoFunctionService.hmac(Utils.fromUtf8ToArray('SignMe!!').buffer, - Utils.fromUtf8ToArray('secretkey').buffer, algorithm); + const value = Utils.fromUtf8ToArray('SignMe!!').buffer; + const key = Utils.fromUtf8ToArray('secretkey').buffer; + let computedMac: ArrayBuffer = null; + if (fast) { + computedMac = await cryptoFunctionService.hmacFast(value, key, algorithm); + } else { + computedMac = await cryptoFunctionService.hmac(value, key, algorithm); + } expect(Utils.fromBufferToHex(computedMac)).toBe(mac); }); } +function testCompare(fast = false) { + it('should successfully compare two of the same values', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const a = new Uint8Array(2); + a[0] = 1; + a[1] = 2; + const equal = fast ? await cryptoFunctionService.timeSafeEqualFast(a.buffer, a.buffer) : + await cryptoFunctionService.timeSafeEqual(a.buffer, a.buffer); + expect(equal).toBe(true); + }); + + it('should successfully compare two different values of the same length', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const a = new Uint8Array(2); + a[0] = 1; + a[1] = 2; + const b = new Uint8Array(2); + b[0] = 3; + b[1] = 4; + const equal = fast ? await cryptoFunctionService.timeSafeEqualFast(a.buffer, b.buffer) : + await cryptoFunctionService.timeSafeEqual(a.buffer, b.buffer); + expect(equal).toBe(false); + }); + + it('should successfully compare two different values of different lengths', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const a = new Uint8Array(2); + a[0] = 1; + a[1] = 2; + const b = new Uint8Array(2); + b[0] = 3; + const equal = fast ? await cryptoFunctionService.timeSafeEqualFast(a.buffer, b.buffer) : + await cryptoFunctionService.timeSafeEqual(a.buffer, b.buffer); + expect(equal).toBe(false); + }); +} + function makeStaticByteArray(length: number) { const arr = new Uint8Array(length); for (let i = 0; i < length; i++) { diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index 3993f42b9d..52422b0b86 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -33,6 +33,11 @@ const RsaPrivateKey = 'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX '+tPVgppLcG0+tMdLjigFQiDUQk2y3WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEzXKZ' + 'BokBGnjFnTnKcs7nv/O8='; +const Sha1Mac = '4d4c223f95dc577b665ec4ccbcb680b80a397038'; +const Sha256Mac = '6be3caa84922e12aaaaa2f16c40d44433bb081ef323db584eb616333ab4e874f'; +const Sha512Mac = '21910e341fa12106ca35758a2285374509326c9fbe0bd64e7b99c898f841dc948c58ce66d3504d8883c' + + '5ea7817a0b7c5d4d9b00364ccd214669131fc17fe4aca'; + describe('WebCrypto Function Service', () => { describe('pbkdf2', () => { const regular256Key = 'pj9prw/OHPleXI6bRdmlaD+saJS4awrMiQsQiDjeu2I='; @@ -72,14 +77,9 @@ describe('WebCrypto Function Service', () => { }); describe('hmac', () => { - const sha1Mac = '4d4c223f95dc577b665ec4ccbcb680b80a397038'; - const sha256Mac = '6be3caa84922e12aaaaa2f16c40d44433bb081ef323db584eb616333ab4e874f'; - const sha512Mac = '21910e341fa12106ca35758a2285374509326c9fbe0bd64e7b99c898f841dc948c58ce66d3504d8883c' + - '5ea7817a0b7c5d4d9b00364ccd214669131fc17fe4aca'; - - testHmac('sha1', sha1Mac); - testHmac('sha256', sha256Mac); - testHmac('sha512', sha512Mac); + testHmac('sha1', Sha1Mac); + testHmac('sha256', Sha256Mac); + testHmac('sha512', Sha512Mac); }); describe('timeSafeEqual', () => { @@ -117,14 +117,9 @@ describe('WebCrypto Function Service', () => { }); describe('hmacFast', () => { - const sha1Mac = '4d4c223f95dc577b665ec4ccbcb680b80a397038'; - const sha256Mac = '6be3caa84922e12aaaaa2f16c40d44433bb081ef323db584eb616333ab4e874f'; - const sha512Mac = '21910e341fa12106ca35758a2285374509326c9fbe0bd64e7b99c898f841dc948c58ce66d3504d8883c' + - '5ea7817a0b7c5d4d9b00364ccd214669131fc17fe4aca'; - - testHmacFast('sha1', sha1Mac); - testHmacFast('sha256', sha256Mac); - testHmacFast('sha512', sha512Mac); + testHmacFast('sha1', Sha1Mac); + testHmacFast('sha256', Sha256Mac); + testHmacFast('sha512', Sha512Mac); }); describe('timeSafeEqualFast', () => { From e614cffffbaf2b7e290aa5fe511d39e387efb4d5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 7 May 2018 12:14:40 -0400 Subject: [PATCH 0230/1626] remove some crypto functions --- .../nodeCryptoFunction.service.spec.ts | 22 ++++++++-------- .../webCryptoFunction.service.spec.ts | 26 +++++++++---------- src/abstractions/cryptoFunction.service.ts | 6 ++--- src/services/crypto.service.ts | 8 +++--- src/services/nodeCryptoFunction.service.ts | 10 +++---- src/services/webCryptoFunction.service.ts | 6 ++--- 6 files changed, 39 insertions(+), 39 deletions(-) diff --git a/spec/node/services/nodeCryptoFunction.service.spec.ts b/spec/node/services/nodeCryptoFunction.service.spec.ts index c86f97e8a2..e57527918f 100644 --- a/spec/node/services/nodeCryptoFunction.service.spec.ts +++ b/spec/node/services/nodeCryptoFunction.service.spec.ts @@ -76,7 +76,7 @@ describe('NodeCrypto Function Service', () => { testHmac('sha512', Sha512Mac); }); - describe('timeSafeEqual', () => { + describe('compare', () => { testCompare(false); }); @@ -86,7 +86,7 @@ describe('NodeCrypto Function Service', () => { testHmac('sha512', Sha512Mac, true); }); - describe('timeSafeEqualFast', () => { + describe('compareFast', () => { testCompare(true); }); @@ -107,7 +107,7 @@ describe('NodeCrypto Function Service', () => { const value = 'EncryptMe!'; const data = Utils.fromUtf8ToArray(value); const encValue = await nodeCryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); - const decValue = await nodeCryptoFunctionService.aesDecryptLarge(encValue, iv.buffer, key.buffer); + const decValue = await nodeCryptoFunctionService.aesDecrypt(encValue, iv.buffer, key.buffer); expect(Utils.fromBufferToUtf8(decValue)).toBe(value); }); }); @@ -124,13 +124,13 @@ describe('NodeCrypto Function Service', () => { }); }); - describe('aesDecryptLarge', () => { + describe('aesDecrypt', () => { it('should successfully decrypt data', async () => { const nodeCryptoFunctionService = new NodeCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); const data = Utils.fromB64ToArray('ByUF8vhyX4ddU9gcooznwA=='); - const decValue = await nodeCryptoFunctionService.aesDecryptLarge(data.buffer, iv.buffer, key.buffer); + const decValue = await nodeCryptoFunctionService.aesDecrypt(data.buffer, iv.buffer, key.buffer); expect(Utils.fromBufferToUtf8(decValue)).toBe('EncryptMe!'); }); }); @@ -262,8 +262,8 @@ function testCompare(fast = false) { const a = new Uint8Array(2); a[0] = 1; a[1] = 2; - const equal = fast ? await cryptoFunctionService.timeSafeEqualFast(a.buffer, a.buffer) : - await cryptoFunctionService.timeSafeEqual(a.buffer, a.buffer); + const equal = fast ? await cryptoFunctionService.compareFast(a.buffer, a.buffer) : + await cryptoFunctionService.compare(a.buffer, a.buffer); expect(equal).toBe(true); }); @@ -275,8 +275,8 @@ function testCompare(fast = false) { const b = new Uint8Array(2); b[0] = 3; b[1] = 4; - const equal = fast ? await cryptoFunctionService.timeSafeEqualFast(a.buffer, b.buffer) : - await cryptoFunctionService.timeSafeEqual(a.buffer, b.buffer); + const equal = fast ? await cryptoFunctionService.compareFast(a.buffer, b.buffer) : + await cryptoFunctionService.compare(a.buffer, b.buffer); expect(equal).toBe(false); }); @@ -287,8 +287,8 @@ function testCompare(fast = false) { a[1] = 2; const b = new Uint8Array(2); b[0] = 3; - const equal = fast ? await cryptoFunctionService.timeSafeEqualFast(a.buffer, b.buffer) : - await cryptoFunctionService.timeSafeEqual(a.buffer, b.buffer); + const equal = fast ? await cryptoFunctionService.compareFast(a.buffer, b.buffer) : + await cryptoFunctionService.compare(a.buffer, b.buffer); expect(equal).toBe(false); }); } diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index 52422b0b86..0d2b21d0eb 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -82,13 +82,13 @@ describe('WebCrypto Function Service', () => { testHmac('sha512', Sha512Mac); }); - describe('timeSafeEqual', () => { + describe('compare', () => { it('should successfully compare two of the same values', async () => { const cryptoFunctionService = getWebCryptoFunctionService(); const a = new Uint8Array(2); a[0] = 1; a[1] = 2; - const equal = await cryptoFunctionService.timeSafeEqual(a.buffer, a.buffer); + const equal = await cryptoFunctionService.compare(a.buffer, a.buffer); expect(equal).toBe(true); }); @@ -100,7 +100,7 @@ describe('WebCrypto Function Service', () => { const b = new Uint8Array(2); b[0] = 3; b[1] = 4; - const equal = await cryptoFunctionService.timeSafeEqual(a.buffer, b.buffer); + const equal = await cryptoFunctionService.compare(a.buffer, b.buffer); expect(equal).toBe(false); }); @@ -111,7 +111,7 @@ describe('WebCrypto Function Service', () => { a[1] = 2; const b = new Uint8Array(2); b[0] = 3; - const equal = await cryptoFunctionService.timeSafeEqual(a.buffer, b.buffer); + const equal = await cryptoFunctionService.compare(a.buffer, b.buffer); expect(equal).toBe(false); }); }); @@ -122,14 +122,14 @@ describe('WebCrypto Function Service', () => { testHmacFast('sha512', Sha512Mac); }); - describe('timeSafeEqualFast', () => { + describe('compareFast', () => { it('should successfully compare two of the same values', async () => { const cryptoFunctionService = getWebCryptoFunctionService(); const a = new Uint8Array(2); a[0] = 1; a[1] = 2; const aByteString = Utils.fromBufferToByteString(a.buffer); - const equal = await cryptoFunctionService.timeSafeEqualFast(aByteString, aByteString); + const equal = await cryptoFunctionService.compareFast(aByteString, aByteString); expect(equal).toBe(true); }); @@ -143,7 +143,7 @@ describe('WebCrypto Function Service', () => { b[0] = 3; b[1] = 4; const bByteString = Utils.fromBufferToByteString(b.buffer); - const equal = await cryptoFunctionService.timeSafeEqualFast(aByteString, bByteString); + const equal = await cryptoFunctionService.compareFast(aByteString, bByteString); expect(equal).toBe(false); }); @@ -156,7 +156,7 @@ describe('WebCrypto Function Service', () => { const b = new Uint8Array(2); b[0] = 3; const bByteString = Utils.fromBufferToByteString(b.buffer); - const equal = await cryptoFunctionService.timeSafeEqualFast(aByteString, bByteString); + const equal = await cryptoFunctionService.compareFast(aByteString, bByteString); expect(equal).toBe(false); }); }); @@ -171,7 +171,7 @@ describe('WebCrypto Function Service', () => { expect(Utils.fromBufferToB64(encValue)).toBe('ByUF8vhyX4ddU9gcooznwA=='); }); - it('should successfully encrypt and then decrypt small data', async () => { + it('should successfully encrypt and then decrypt data fast', async () => { const cryptoFunctionService = getWebCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); @@ -186,14 +186,14 @@ describe('WebCrypto Function Service', () => { expect(decValue).toBe(value); }); - it('should successfully encrypt and then decrypt large data', async () => { + it('should successfully encrypt and then decrypt data', async () => { const cryptoFunctionService = getWebCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); const value = 'EncryptMe!'; const data = Utils.fromUtf8ToArray(value); const encValue = await cryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer); - const decValue = await cryptoFunctionService.aesDecryptLarge(encValue, iv.buffer, key.buffer); + const decValue = await cryptoFunctionService.aesDecrypt(encValue, iv.buffer, key.buffer); expect(Utils.fromBufferToUtf8(decValue)).toBe(value); }); }); @@ -210,13 +210,13 @@ describe('WebCrypto Function Service', () => { }); }); - describe('aesDecryptLarge', () => { + describe('aesDecrypt', () => { it('should successfully decrypt data', async () => { const cryptoFunctionService = getWebCryptoFunctionService(); const iv = makeStaticByteArray(16); const key = makeStaticByteArray(32); const data = Utils.fromB64ToArray('ByUF8vhyX4ddU9gcooznwA=='); - const decValue = await cryptoFunctionService.aesDecryptLarge(data.buffer, iv.buffer, key.buffer); + const decValue = await cryptoFunctionService.aesDecrypt(data.buffer, iv.buffer, key.buffer); expect(Utils.fromBufferToUtf8(decValue)).toBe('EncryptMe!'); }); }); diff --git a/src/abstractions/cryptoFunction.service.ts b/src/abstractions/cryptoFunction.service.ts index 8935d32aa7..553444b667 100644 --- a/src/abstractions/cryptoFunction.service.ts +++ b/src/abstractions/cryptoFunction.service.ts @@ -6,15 +6,15 @@ export abstract class CryptoFunctionService { iterations: number) => Promise; hash: (value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise; hmac: (value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise; - timeSafeEqual: (a: ArrayBuffer, b: ArrayBuffer) => Promise; + compare: (a: ArrayBuffer, b: ArrayBuffer) => Promise; hmacFast: (value: ArrayBuffer | string, key: ArrayBuffer | string, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise; - timeSafeEqualFast: (a: ArrayBuffer | string, b: ArrayBuffer | string) => Promise; + compareFast: (a: ArrayBuffer | string, b: ArrayBuffer | string) => Promise; aesEncrypt: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise; aesDecryptFastParameters: (data: string, iv: string, mac: string, key: SymmetricCryptoKey) => DecryptParameters; aesDecryptFast: (parameters: DecryptParameters) => Promise; - aesDecryptLarge: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise; + aesDecrypt: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise; rsaEncrypt: (data: ArrayBuffer, publicKey: ArrayBuffer, algorithm: 'sha1' | 'sha256') => Promise; rsaDecrypt: (data: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256') => Promise; randomBytes: (length: number) => Promise; diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 7d5a56a754..7886ab4c0d 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -430,7 +430,7 @@ export class CryptoService implements CryptoServiceAbstraction { if (fastParams.macKey != null && fastParams.mac != null) { const computedMac = await this.cryptoFunctionService.hmacFast(fastParams.macData, fastParams.macKey, 'sha256'); - const macsEqual = await this.cryptoFunctionService.timeSafeEqualFast(fastParams.mac, computedMac); + const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac); if (!macsEqual) { // tslint:disable-next-line console.error('mac failed.'); @@ -463,7 +463,7 @@ export class CryptoService implements CryptoServiceAbstraction { return null; } - const macsMatch = await this.cryptoFunctionService.timeSafeEqual(mac, computedMac); + const macsMatch = await this.cryptoFunctionService.compare(mac, computedMac); if (!macsMatch) { // tslint:disable-next-line console.error('mac failed.'); @@ -471,7 +471,7 @@ export class CryptoService implements CryptoServiceAbstraction { } } - return await this.cryptoFunctionService.aesDecryptLarge(ct, iv, theKey.encKey); + return await this.cryptoFunctionService.aesDecrypt(ct, iv, theKey.encKey); } private async rsaDecrypt(encValue: string): Promise { @@ -515,7 +515,7 @@ export class CryptoService implements CryptoServiceAbstraction { if (key != null && key.macKey != null && encPieces.length > 1) { const mac = Utils.fromB64ToArray(encPieces[1]).buffer; const computedMac = await this.cryptoFunctionService.hmac(ct, key.macKey, 'sha256'); - const macsEqual = await this.cryptoFunctionService.timeSafeEqual(mac, computedMac); + const macsEqual = await this.cryptoFunctionService.compare(mac, computedMac); if (!macsEqual) { throw new Error('MAC failed.'); } diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index c4fdc89ccd..69fb64c2e4 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -41,7 +41,7 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { return Promise.resolve(this.toArrayBuffer(hmac.digest())); } - async timeSafeEqual(a: ArrayBuffer, b: ArrayBuffer): Promise { + async compare(a: ArrayBuffer, b: ArrayBuffer): Promise { const key = await this.randomBytes(32); const mac1 = await this.hmac(a, key, 'sha256'); const mac2 = await this.hmac(b, key, 'sha256'); @@ -64,8 +64,8 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { return this.hmac(value, key, algorithm); } - timeSafeEqualFast(a: ArrayBuffer, b: ArrayBuffer): Promise { - return this.timeSafeEqual(a, b); + compareFast(a: ArrayBuffer, b: ArrayBuffer): Promise { + return this.compare(a, b); } aesEncrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { @@ -100,11 +100,11 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { } async aesDecryptFast(parameters: DecryptParameters): Promise { - const decBuf = await this.aesDecryptLarge(parameters.data, parameters.iv, parameters.encKey); + const decBuf = await this.aesDecrypt(parameters.data, parameters.iv, parameters.encKey); return Utils.fromBufferToUtf8(decBuf); } - aesDecryptLarge(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { + aesDecrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { const nodeData = this.toNodeBuffer(data); const nodeIv = this.toNodeBuffer(iv); const nodeKey = this.toNodeBuffer(key); diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index 8a0b169b0d..cc619e5192 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -69,7 +69,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { // Safely compare two values 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/ // ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy - async timeSafeEqual(a: ArrayBuffer, b: ArrayBuffer): Promise { + async compare(a: ArrayBuffer, b: ArrayBuffer): Promise { const macKey = await this.randomBytes(32); const signingAlgorithm = { name: 'HMAC', @@ -102,7 +102,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return Promise.resolve(bytes); } - async timeSafeEqualFast(a: string, b: string): Promise { + async compareFast(a: string, b: string): Promise { const rand = await this.randomBytes(32); const bytes = new Uint32Array(rand); const buffer = forge.util.createBuffer(); @@ -155,7 +155,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return Promise.resolve(val); } - async aesDecryptLarge(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { + async aesDecrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { const impKey = await this.subtle.importKey('raw', key, { name: 'AES-CBC' }, false, ['decrypt']); return await this.subtle.decrypt({ name: 'AES-CBC', iv: iv }, impKey, data); } From 4614b4a1859999326df6ab6f746babbca343d19d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 7 May 2018 12:37:36 -0400 Subject: [PATCH 0231/1626] No edge on appveyor --- spec/support/karma.conf.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/support/karma.conf.js b/spec/support/karma.conf.js index d177dd699e..d23eff033c 100644 --- a/spec/support/karma.conf.js +++ b/spec/support/karma.conf.js @@ -73,6 +73,10 @@ module.exports = (config) => { removeBrowser('Opera'); removeBrowser('SafariTechPreview'); + if (process.env.APPVEYOR === 'True') { + removeBrowser('Edge'); + } + return result; } }, From 3270d8bd0ed46b62262575f5d59fca2414aaf8c7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 8 May 2018 09:40:12 -0400 Subject: [PATCH 0232/1626] adjust base menu --- src/electron/baseMenu.ts | 54 ++++------------------------------------ 1 file changed, 5 insertions(+), 49 deletions(-) diff --git a/src/electron/baseMenu.ts b/src/electron/baseMenu.ts index 77a9aff3bd..4cd043866d 100644 --- a/src/electron/baseMenu.ts +++ b/src/electron/baseMenu.ts @@ -12,59 +12,15 @@ import { WindowMain } from './window.main'; import { isMacAppStore } from './utils'; export class BaseMenu { - protected logOutMenuItemOptions: MenuItemConstructorOptions; - protected aboutMenuItemOptions: MenuItemConstructorOptions; protected editMenuItemOptions: MenuItemConstructorOptions; protected viewSubMenuItemOptions: MenuItemConstructorOptions[]; protected windowMenuItemOptions: MenuItemConstructorOptions; protected macAppMenuItemOptions: MenuItemConstructorOptions[]; protected macWindowSubmenuOptions: MenuItemConstructorOptions[]; - constructor(protected i18nService: I18nService, protected windowMain: WindowMain, - protected appName: string, private onLogOut: () => void) { } + constructor(protected i18nService: I18nService, protected windowMain: WindowMain) { } protected initProperties() { - this.logOutMenuItemOptions = { - label: this.i18nService.t('logOut'), - id: 'logOut', - click: () => { - const result = dialog.showMessageBox(this.windowMain.win, { - title: this.i18nService.t('logOut'), - message: this.i18nService.t('logOut'), - detail: this.i18nService.t('logOutConfirmation'), - buttons: [this.i18nService.t('logOut'), this.i18nService.t('cancel')], - cancelId: 1, - defaultId: 0, - noLink: true, - }); - if (result === 0) { - this.onLogOut(); - } - }, - }; - - this.aboutMenuItemOptions = { - label: this.i18nService.t('aboutBitwarden'), - click: () => { - const aboutInformation = this.i18nService.t('version', app.getVersion()) + - '\nShell ' + process.versions.electron + - '\nRenderer ' + process.versions.chrome + - '\nNode ' + process.versions.node + - '\nArchitecture ' + process.arch; - const result = dialog.showMessageBox(this.windowMain.win, { - title: this.appName, - message: this.appName, - detail: aboutInformation, - type: 'info', - noLink: true, - buttons: [this.i18nService.t('ok'), this.i18nService.t('copy')], - }); - if (result === 1) { - clipboard.writeText(aboutInformation); - } - }, - }; - this.editMenuItemOptions = { label: this.i18nService.t('edit'), submenu: [ @@ -169,10 +125,6 @@ export class BaseMenu { ]; this.macWindowSubmenuOptions = [ - { - label: this.i18nService.t('close'), - role: isMacAppStore() ? 'quit' : 'close', - }, { label: this.i18nService.t('minimize'), role: 'minimize', @@ -186,6 +138,10 @@ export class BaseMenu { label: this.i18nService.t('bringAllToFront'), role: 'front', }, + { + label: this.i18nService.t('close'), + role: isMacAppStore() ? 'quit' : 'close', + }, ]; } } From e7779759f25d315d35b20ae85371ebe86fed3eef Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 8 May 2018 10:29:19 -0400 Subject: [PATCH 0233/1626] dont send message back to main if that's where it came from originally --- .../services/electronRendererMessaging.service.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/electron/services/electronRendererMessaging.service.ts b/src/electron/services/electronRendererMessaging.service.ts index 9dedd360a7..b967b4e530 100644 --- a/src/electron/services/electronRendererMessaging.service.ts +++ b/src/electron/services/electronRendererMessaging.service.ts @@ -8,14 +8,20 @@ export class ElectronRendererMessagingService implements MessagingService { constructor(private broadcasterService: BroadcasterService) { ipcRenderer.on('messagingService', async (event: any, message: any) => { if (message.command) { - this.send(message.command, message); + this.sendMessage(message.command, message, false); } }); } send(subscriber: string, arg: any = {}) { + this.sendMessage(subscriber, arg, true); + } + + private sendMessage(subscriber: string, arg: any = {}, toMain: boolean) { const message = Object.assign({}, { command: subscriber }, arg); - ipcRenderer.send('messagingService', message); this.broadcasterService.send(message); + if (toMain) { + ipcRenderer.send('messagingService', message); + } } } From 9de9c1655c9a325443fdabfc74630f06e20f8d9a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 8 May 2018 12:05:00 -0400 Subject: [PATCH 0234/1626] move tray to jslib --- src/electron/tray.main.ts | 129 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 src/electron/tray.main.ts diff --git a/src/electron/tray.main.ts b/src/electron/tray.main.ts new file mode 100644 index 0000000000..716a03115f --- /dev/null +++ b/src/electron/tray.main.ts @@ -0,0 +1,129 @@ +import { + Menu, + MenuItem, + MenuItemConstructorOptions, + nativeImage, + Tray, +} from 'electron'; +import * as path from 'path'; + +import { I18nService } from '../abstractions/i18n.service'; +import { StorageService } from '../abstractions/storage.service'; + +import { ElectronConstants } from './electronConstants'; +import { WindowMain } from './window.main'; + +export class TrayMain { + contextMenu: Menu; + + private appName: string; + private tray: Tray; + private icon: string | Electron.NativeImage; + private pressedIcon: Electron.NativeImage; + + constructor(private windowMain: WindowMain, private i18nService: I18nService, + private storageService: StorageService) { + if (process.platform === 'win32') { + this.icon = path.join(__dirname, '/images/icon.ico'); + } else if (process.platform === 'darwin') { + const nImage = nativeImage.createFromPath(path.join(__dirname, '/images/icon-template.png')); + nImage.setTemplateImage(true); + this.icon = nImage; + this.pressedIcon = nativeImage.createFromPath(path.join(__dirname, '/images/icon-highlight.png')); + } else { + this.icon = path.join(__dirname, '/images/icon.png'); + } + } + + async init(appName: string, additionalMenuItems: MenuItemConstructorOptions[] = null) { + this.appName = appName; + + const menuItemOptions: MenuItemConstructorOptions[] = [{ + label: this.i18nService.t('showHide'), + click: () => this.toggleWindow(), + }, + { type: 'separator' }, + { + label: process.platform === 'darwin' ? this.i18nService.t('close') : this.i18nService.t('exit'), + click: () => this.closeWindow(), + }]; + + if (additionalMenuItems != null) { + menuItemOptions.splice(1, 0, ...additionalMenuItems); + } + + this.contextMenu = Menu.buildFromTemplate(menuItemOptions); + if (await this.storageService.get(ElectronConstants.enableTrayKey)) { + this.showTray(); + } + + if (process.platform === 'win32') { + this.windowMain.win.on('minimize', async (e: Event) => { + if (await this.storageService.get(ElectronConstants.enableMinimizeToTrayKey)) { + e.preventDefault(); + this.hideToTray(); + } + }); + } + + this.windowMain.win.on('show', async (e: Event) => { + const enableTray = await this.storageService.get(ElectronConstants.enableTrayKey); + if (!enableTray) { + this.removeTray(false); + } + }); + } + + removeTray(showWindow = true) { + if (this.tray != null) { + this.tray.destroy(); + this.tray = null; + } + + if (showWindow && this.windowMain.win != null && !this.windowMain.win.isVisible()) { + this.windowMain.win.show(); + } + } + + hideToTray() { + this.showTray(); + if (this.windowMain.win != null) { + this.windowMain.win.hide(); + } + } + + showTray() { + if (this.tray != null) { + return; + } + + this.tray = new Tray(this.icon); + this.tray.setToolTip(this.appName); + this.tray.on('click', () => this.toggleWindow()); + + if (this.pressedIcon != null) { + this.tray.setPressedImage(this.pressedIcon); + } + if (this.contextMenu != null) { + this.tray.setContextMenu(this.contextMenu); + } + } + + private toggleWindow() { + if (this.windowMain.win == null) { + return; + } + + if (this.windowMain.win.isVisible()) { + this.windowMain.win.hide(); + } else { + this.windowMain.win.show(); + } + } + + private closeWindow() { + if (this.windowMain.win != null) { + this.windowMain.win.close(); + } + } +} From 22894a68765ad95a4c4101cc7688f6c8fd3882da Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 9 May 2018 15:48:17 -0400 Subject: [PATCH 0235/1626] cache key meta data for forge decryption --- src/models/domain/symmetricCryptoKey.ts | 2 ++ src/services/webCryptoFunction.service.ts | 30 +++++++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/models/domain/symmetricCryptoKey.ts b/src/models/domain/symmetricCryptoKey.ts index f2b593d402..e6e63b82ad 100644 --- a/src/models/domain/symmetricCryptoKey.ts +++ b/src/models/domain/symmetricCryptoKey.ts @@ -12,6 +12,8 @@ export class SymmetricCryptoKey { encKeyB64: string; macKeyB64: string; + meta: any; + constructor(key: ArrayBuffer, encType?: EncryptionType) { if (key == null) { throw new Error('Must provide key'); diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index cc619e5192..3938b48b9c 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -40,7 +40,8 @@ export class WebCryptoFunctionService implements CryptoFunctionService { hash: { name: this.toWebCryptoAlgorithm(algorithm) }, }; - const impKey = await this.subtle.importKey('raw', passwordBuf, { name: 'PBKDF2' }, false, ['deriveBits']); + const impKey = await this.subtle.importKey('raw', passwordBuf, { name: 'PBKDF2' } as any, + false, ['deriveBits']); return await this.subtle.deriveBits(pbkdf2Params, impKey, wcLen); } @@ -125,23 +126,42 @@ export class WebCryptoFunctionService implements CryptoFunctionService { } async aesEncrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { - const impKey = await this.subtle.importKey('raw', key, { name: 'AES-CBC' }, false, ['encrypt']); + const impKey = await this.subtle.importKey('raw', key, { name: 'AES-CBC' } as any, false, ['encrypt']); return await this.subtle.encrypt({ name: 'AES-CBC', iv: iv }, impKey, data); } aesDecryptFastParameters(data: string, iv: string, mac: string, key: SymmetricCryptoKey): DecryptParameters { const p = new DecryptParameters(); - p.encKey = forge.util.decode64(key.encKeyB64); + if (key.meta != null) { + p.encKey = key.meta.encKeyByteString; + p.macKey = key.meta.macKeyByteString; + } + + if (p.encKey == null) { + p.encKey = forge.util.decode64(key.encKeyB64); + } p.data = forge.util.decode64(data); p.iv = forge.util.decode64(iv); p.macData = p.iv + p.data; - if (key.macKeyB64 != null) { + if (p.macKey == null && key.macKeyB64 != null) { p.macKey = forge.util.decode64(key.macKeyB64); } if (mac != null) { p.mac = forge.util.decode64(mac); } + + // cache byte string keys for later + if (key.meta == null) { + key.meta = {}; + } + if (key.meta.encKeyByteString == null) { + key.meta.encKeyByteString = p.encKey; + } + if (p.macKey != null && key.meta.macKeyByteString == null) { + key.meta.macKeyByteString = p.macKey; + } + return p; } @@ -156,7 +176,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { } async aesDecrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise { - const impKey = await this.subtle.importKey('raw', key, { name: 'AES-CBC' }, false, ['decrypt']); + const impKey = await this.subtle.importKey('raw', key, { name: 'AES-CBC' } as any, false, ['decrypt']); return await this.subtle.decrypt({ name: 'AES-CBC', iv: iv }, impKey, data); } From 29556c5d3beb28eb0258e70773c5ffc316db4139 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 9 May 2018 16:00:15 -0400 Subject: [PATCH 0236/1626] rename some crypto terms --- src/models/domain/cipherString.ts | 26 ++++++++-------- src/models/domain/encryptedObject.ts | 2 +- src/services/crypto.service.ts | 44 ++++++++++++++-------------- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/models/domain/cipherString.ts b/src/models/domain/cipherString.ts index b6068edd16..93ff9e78da 100644 --- a/src/models/domain/cipherString.ts +++ b/src/models/domain/cipherString.ts @@ -6,15 +6,15 @@ export class CipherString { encryptedString?: string; encryptionType?: EncryptionType; decryptedValue?: string; - cipherText?: string; - initializationVector?: string; + data?: string; + iv?: string; mac?: string; - constructor(encryptedStringOrType: string | EncryptionType, ct?: string, iv?: string, mac?: string) { - if (ct != null) { - // ct and header + constructor(encryptedStringOrType: string | EncryptionType, data?: string, iv?: string, mac?: string) { + if (data != null) { + // data and header const encType = encryptedStringOrType as EncryptionType; - this.encryptedString = encType + '.' + ct; + this.encryptedString = encType + '.' + data; // iv if (iv != null) { @@ -27,8 +27,8 @@ export class CipherString { } this.encryptionType = encType; - this.cipherText = ct; - this.initializationVector = iv; + this.data = data; + this.iv = iv; this.mac = mac; return; @@ -62,8 +62,8 @@ export class CipherString { return; } - this.initializationVector = encPieces[0]; - this.cipherText = encPieces[1]; + this.iv = encPieces[0]; + this.data = encPieces[1]; this.mac = encPieces[2]; break; case EncryptionType.AesCbc256_B64: @@ -71,8 +71,8 @@ export class CipherString { return; } - this.initializationVector = encPieces[0]; - this.cipherText = encPieces[1]; + this.iv = encPieces[0]; + this.data = encPieces[1]; break; case EncryptionType.Rsa2048_OaepSha256_B64: case EncryptionType.Rsa2048_OaepSha1_B64: @@ -80,7 +80,7 @@ export class CipherString { return; } - this.cipherText = encPieces[0]; + this.data = encPieces[0]; break; default: return; diff --git a/src/models/domain/encryptedObject.ts b/src/models/domain/encryptedObject.ts index 10e32797a6..f21fe300f4 100644 --- a/src/models/domain/encryptedObject.ts +++ b/src/models/domain/encryptedObject.ts @@ -2,7 +2,7 @@ import { SymmetricCryptoKey } from './symmetricCryptoKey'; export class EncryptedObject { iv: ArrayBuffer; - ct: ArrayBuffer; + data: ArrayBuffer; mac: ArrayBuffer; key: SymmetricCryptoKey; } diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 7886ab4c0d..8ff31b0c6d 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -277,9 +277,9 @@ export class CryptoService implements CryptoServiceAbstraction { const encObj = await this.aesEncrypt(plainBuf, key); const iv = Utils.fromBufferToB64(encObj.iv); - const ct = Utils.fromBufferToB64(encObj.ct); + const data = Utils.fromBufferToB64(encObj.data); const mac = encObj.mac != null ? Utils.fromBufferToB64(encObj.mac) : null; - return new CipherString(encObj.key.encType, iv, ct, mac); + return new CipherString(encObj.key.encType, iv, data, mac); } async encryptToBytes(plainValue: ArrayBuffer, key?: SymmetricCryptoKey): Promise { @@ -289,22 +289,22 @@ export class CryptoService implements CryptoServiceAbstraction { macLen = encValue.mac.byteLength; } - const encBytes = new Uint8Array(1 + encValue.iv.byteLength + macLen + encValue.ct.byteLength); + const encBytes = new Uint8Array(1 + encValue.iv.byteLength + macLen + encValue.data.byteLength); encBytes.set([encValue.key.encType]); encBytes.set(new Uint8Array(encValue.iv), 1); if (encValue.mac != null) { encBytes.set(new Uint8Array(encValue.mac), 1 + encValue.iv.byteLength); } - encBytes.set(new Uint8Array(encValue.ct), 1 + encValue.iv.byteLength + macLen); + encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength + macLen); return encBytes.buffer; } async decrypt(cipherString: CipherString, key?: SymmetricCryptoKey): Promise { - const iv = Utils.fromB64ToArray(cipherString.initializationVector).buffer; - const ct = Utils.fromB64ToArray(cipherString.cipherText).buffer; + const iv = Utils.fromB64ToArray(cipherString.iv).buffer; + const data = Utils.fromB64ToArray(cipherString.data).buffer; const mac = cipherString.mac ? Utils.fromB64ToArray(cipherString.mac).buffer : null; - const decipher = await this.aesDecryptToBytes(cipherString.encryptionType, ct, iv, mac, key); + const decipher = await this.aesDecryptToBytes(cipherString.encryptionType, data, iv, mac, key); if (decipher == null) { return null; } @@ -313,8 +313,8 @@ export class CryptoService implements CryptoServiceAbstraction { } async decryptToUtf8(cipherString: CipherString, key?: SymmetricCryptoKey): Promise { - return await this.aesDecryptToUtf8(cipherString.encryptionType, cipherString.cipherText, - cipherString.initializationVector, cipherString.mac, key); + return await this.aesDecryptToUtf8(cipherString.encryptionType, cipherString.data, + cipherString.iv, cipherString.mac, key); } async decryptFromBytes(encBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise { @@ -393,23 +393,23 @@ export class CryptoService implements CryptoServiceAbstraction { // Helpers - private async aesEncrypt(plainValue: ArrayBuffer, key: SymmetricCryptoKey): Promise { + private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise { const obj = new EncryptedObject(); obj.key = await this.getKeyForEncryption(key); obj.iv = await this.cryptoFunctionService.randomBytes(16); - obj.ct = await this.cryptoFunctionService.aesEncrypt(plainValue, obj.iv, obj.key.encKey); + obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, obj.key.encKey); if (obj.key.macKey != null) { - const macData = new Uint8Array(obj.iv.byteLength + obj.ct.byteLength); + const macData = new Uint8Array(obj.iv.byteLength + obj.data.byteLength); macData.set(new Uint8Array(obj.iv), 0); - macData.set(new Uint8Array(obj.ct), obj.iv.byteLength); + macData.set(new Uint8Array(obj.data), obj.iv.byteLength); obj.mac = await this.cryptoFunctionService.hmac(macData.buffer, obj.key.macKey, 'sha256'); } return obj; } - private async aesDecryptToUtf8(encType: EncryptionType, ct: string, iv: string, mac: string, + private async aesDecryptToUtf8(encType: EncryptionType, data: string, iv: string, mac: string, key: SymmetricCryptoKey): Promise { const keyForEnc = await this.getKeyForEncryption(key); const theKey = this.resolveLegacyKey(encType, keyForEnc); @@ -426,7 +426,7 @@ export class CryptoService implements CryptoServiceAbstraction { return null; } - const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(ct, iv, mac, theKey); + const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(data, iv, mac, theKey); if (fastParams.macKey != null && fastParams.mac != null) { const computedMac = await this.cryptoFunctionService.hmacFast(fastParams.macData, fastParams.macKey, 'sha256'); @@ -441,7 +441,7 @@ export class CryptoService implements CryptoServiceAbstraction { return this.cryptoFunctionService.aesDecryptFast(fastParams); } - private async aesDecryptToBytes(encType: EncryptionType, ct: ArrayBuffer, iv: ArrayBuffer, + private async aesDecryptToBytes(encType: EncryptionType, data: ArrayBuffer, iv: ArrayBuffer, mac: ArrayBuffer, key: SymmetricCryptoKey): Promise { const keyForEnc = await this.getKeyForEncryption(key); const theKey = this.resolveLegacyKey(encType, keyForEnc); @@ -455,9 +455,9 @@ export class CryptoService implements CryptoServiceAbstraction { } if (theKey.macKey != null && mac != null) { - const macData = new Uint8Array(iv.byteLength + ct.byteLength); + const macData = new Uint8Array(iv.byteLength + data.byteLength); macData.set(new Uint8Array(iv), 0); - macData.set(new Uint8Array(ct), iv.byteLength); + macData.set(new Uint8Array(data), iv.byteLength); const computedMac = await this.cryptoFunctionService.hmac(macData.buffer, theKey.macKey, 'sha256'); if (computedMac === null) { return null; @@ -471,7 +471,7 @@ export class CryptoService implements CryptoServiceAbstraction { } } - return await this.cryptoFunctionService.aesDecrypt(ct, iv, theKey.encKey); + return await this.cryptoFunctionService.aesDecrypt(data, iv, theKey.encKey); } private async rsaDecrypt(encValue: string): Promise { @@ -510,11 +510,11 @@ export class CryptoService implements CryptoServiceAbstraction { throw new Error('encPieces unavailable.'); } - const ct = Utils.fromB64ToArray(encPieces[0]).buffer; + const data = Utils.fromB64ToArray(encPieces[0]).buffer; const key = await this.getEncKey(); if (key != null && key.macKey != null && encPieces.length > 1) { const mac = Utils.fromB64ToArray(encPieces[1]).buffer; - const computedMac = await this.cryptoFunctionService.hmac(ct, key.macKey, 'sha256'); + const computedMac = await this.cryptoFunctionService.hmac(data, key.macKey, 'sha256'); const macsEqual = await this.cryptoFunctionService.compare(mac, computedMac); if (!macsEqual) { throw new Error('MAC failed.'); @@ -539,7 +539,7 @@ export class CryptoService implements CryptoServiceAbstraction { throw new Error('encType unavailable.'); } - return this.cryptoFunctionService.rsaDecrypt(ct, privateKey, alg); + return this.cryptoFunctionService.rsaDecrypt(data, privateKey, alg); } private async getKeyForEncryption(key?: SymmetricCryptoKey): Promise { From 5850a590ceb43ad70afba688edaf930209eb1fa9 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 14 May 2018 23:41:12 -0400 Subject: [PATCH 0237/1626] utils global --- src/misc/utils.ts | 2 ++ src/models/domain/cipherString.ts | 6 ++++-- src/models/view/loginUriView.ts | 4 ++-- src/services/container.service.ts | 9 +++++++-- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 0ff14d3dbd..77535331df 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -5,6 +5,7 @@ export class Utils { static inited = false; static isNode = false; static isBrowser = true; + static global: NodeJS.Global | Window = null; static init() { if (Utils.inited) { @@ -14,6 +15,7 @@ export class Utils { Utils.inited = true; Utils.isNode = typeof window === 'undefined'; Utils.isBrowser = !Utils.isNode; + Utils.global = Utils.isNode ? global : window; } static fromB64ToArray(str: string): Uint8Array { diff --git a/src/models/domain/cipherString.ts b/src/models/domain/cipherString.ts index 93ff9e78da..6d49d1724e 100644 --- a/src/models/domain/cipherString.ts +++ b/src/models/domain/cipherString.ts @@ -2,6 +2,8 @@ import { EncryptionType } from '../../enums/encryptionType'; import { CryptoService } from '../../abstractions/crypto.service'; +import { Utils } from '../../misc/utils'; + export class CipherString { encryptedString?: string; encryptionType?: EncryptionType; @@ -93,11 +95,11 @@ export class CipherString { } let cryptoService: CryptoService; - const containerService = (window as any).bitwardenContainerService; + const containerService = (Utils.global as any).bitwardenContainerService; if (containerService) { cryptoService = containerService.getCryptoService(); } else { - throw new Error('window.bitwardenContainerService not initialized.'); + throw new Error('global bitwardenContainerService not initialized.'); } try { diff --git a/src/models/view/loginUriView.ts b/src/models/view/loginUriView.ts index 0c78bb50c8..476067959a 100644 --- a/src/models/view/loginUriView.ts +++ b/src/models/view/loginUriView.ts @@ -35,7 +35,7 @@ export class LoginUriView implements View { get domain(): string { if (this._domain == null && this.uri != null) { - const containerService = (window as any).bitwardenContainerService; + const containerService = (Utils.global as any).bitwardenContainerService; if (containerService) { const platformUtilsService: PlatformUtilsService = containerService.getPlatformUtilsService(); this._domain = platformUtilsService.getDomain(this.uri); @@ -43,7 +43,7 @@ export class LoginUriView implements View { this._domain = null; } } else { - throw new Error('window.bitwardenContainerService not initialized.'); + throw new Error('global bitwardenContainerService not initialized.'); } } diff --git a/src/services/container.service.ts b/src/services/container.service.ts index fa9a6628ce..10e3f6390e 100644 --- a/src/services/container.service.ts +++ b/src/services/container.service.ts @@ -6,9 +6,14 @@ export class ContainerService { private platformUtilsService: PlatformUtilsService) { } + // deprecated, use attachToGlobal instead attachToWindow(win: any) { - if (!win.bitwardenContainerService) { - win.bitwardenContainerService = this; + this.attachToGlobal(win); + } + + attachToGlobal(global: any) { + if (!global.bitwardenContainerService) { + global.bitwardenContainerService = this; } } From 17cf0599707090bcb2f3992e6b65f3f7809073f0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 14 May 2018 23:56:27 -0400 Subject: [PATCH 0238/1626] node fetch api --- package-lock.json | 5 +++++ package.json | 1 + src/services/api.service.ts | 4 +--- src/services/nodeApi.service.ts | 15 +++++++++++++++ 4 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 src/services/nodeApi.service.ts diff --git a/package-lock.json b/package-lock.json index 9e5eb8f861..e4490a13ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5786,6 +5786,11 @@ "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", "dev": true }, + "node-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz", + "integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=" + }, "node-forge": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz", diff --git a/package.json b/package.json index 10e66ab3bc..e3984ee5c4 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "electron-updater": "2.21.4", "keytar": "4.1.0", "lunr": "2.1.6", + "node-fetch": "2.1.2", "node-forge": "0.7.1", "rxjs": "5.5.6", "zone.js": "0.8.19" diff --git a/src/services/api.service.ts b/src/services/api.service.ts index b4274465a6..cda9888685 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -439,12 +439,10 @@ export class ApiService implements ApiServiceAbstraction { } private async handleTokenState(): Promise { - let accessToken: string; + let accessToken = await this.tokenService.getToken(); if (this.tokenService.tokenNeedsRefresh()) { const tokenResponse = await this.doRefreshToken(); accessToken = tokenResponse.accessToken; - } else { - accessToken = await this.tokenService.getToken(); } return 'Bearer ' + accessToken; diff --git a/src/services/nodeApi.service.ts b/src/services/nodeApi.service.ts new file mode 100644 index 0000000000..d75ff1fdac --- /dev/null +++ b/src/services/nodeApi.service.ts @@ -0,0 +1,15 @@ +import { Utils } from '../misc/utils'; + +import { ApiService } from './api.service'; + +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; +import { TokenService } from '../abstractions/token.service'; + +import * as fetch from 'node-fetch'; + +export class NodeApiService extends ApiService { + constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService, + logoutCallback: Function) { + super(tokenService, platformUtilsService, logoutCallback); + } +} From f173001a411acb39c0d2ebb11d17ea90d70ec784 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 15 May 2018 09:02:57 -0400 Subject: [PATCH 0239/1626] polyfill fetch globals on nodeapi --- package-lock.json | 9 +++++++++ package.json | 3 ++- src/services/nodeApi.service.ts | 7 +++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e4490a13ee..0899866723 100644 --- a/package-lock.json +++ b/package-lock.json @@ -108,6 +108,15 @@ "integrity": "sha512-VRQB+Q0L3YZWs45uRdpN9oWr82meL/8TrJ6faoKT5tp0uub2l/aRMhtm5fo68h7kjYKH60f9/bay1nF7ZpTW5g==", "dev": true }, + "@types/node-fetch": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-1.6.9.tgz", + "integrity": "sha512-n2r6WLoY7+uuPT7pnEtKJCmPUGyJ+cbyBR8Avnu4+m1nzz7DwBVuyIvvlBzCZ/nrpC7rIgb3D6pNavL7rFEa9g==", + "dev": true, + "requires": { + "@types/node": "8.0.19" + } + }, "@types/node-forge": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.7.1.tgz", diff --git a/package.json b/package.json index e3984ee5c4..4dde128f73 100644 --- a/package.json +++ b/package.json @@ -28,15 +28,16 @@ "@types/keytar": "^4.0.1", "@types/lunr": "2.1.5", "@types/node": "8.0.19", + "@types/node-fetch": "^1.6.9", "@types/node-forge": "0.7.1", "@types/papaparse": "4.1.31", "@types/webcrypto": "0.0.28", "concurrently": "3.5.1", "electron": "1.8.4", "jasmine": "^3.1.0", - "jasmine-ts-console-reporter": "^3.1.1", "jasmine-core": "^2.8.0", "jasmine-spec-reporter": "^4.2.1", + "jasmine-ts-console-reporter": "^3.1.1", "karma": "^1.7.1", "karma-chrome-launcher": "^2.2.0", "karma-cli": "^1.0.1", diff --git a/src/services/nodeApi.service.ts b/src/services/nodeApi.service.ts index d75ff1fdac..7803eafdc8 100644 --- a/src/services/nodeApi.service.ts +++ b/src/services/nodeApi.service.ts @@ -1,11 +1,14 @@ -import { Utils } from '../misc/utils'; +import * as fe from 'node-fetch'; import { ApiService } from './api.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { TokenService } from '../abstractions/token.service'; -import * as fetch from 'node-fetch'; +(global as any).fetch = fe.default; +(global as any).Request = fe.Request; +(global as any).Response = fe.Response; +(global as any).Headers = fe.Headers; export class NodeApiService extends ApiService { constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService, From 7112911cb89bcf9bf9b9de32cb05ec2d3f78e3fc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 15 May 2018 21:11:20 -0400 Subject: [PATCH 0240/1626] 2fa adjustments in auth services --- src/abstractions/auth.service.ts | 3 ++ .../two-factor-options.component.ts | 25 +-------- src/services/auth.service.ts | 54 ++++++++++++++++--- src/services/crypto.service.ts | 5 +- 4 files changed, 55 insertions(+), 32 deletions(-) diff --git a/src/abstractions/auth.service.ts b/src/abstractions/auth.service.ts index 4b2b2434d0..7b721433c0 100644 --- a/src/abstractions/auth.service.ts +++ b/src/abstractions/auth.service.ts @@ -11,6 +11,9 @@ export abstract class AuthService { logIn: (email: string, masterPassword: string) => Promise; logInTwoFactor: (twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean) => Promise; + logInComplete: (email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType, + twoFactorToken: string, remember?: boolean) => Promise; logOut: (callback: Function) => void; + getSupportedTwoFactorProviders: (win: Window) => any[]; getDefaultTwoFactorProvider: (u2fSupported: boolean) => TwoFactorProviderType; } diff --git a/src/angular/components/two-factor-options.component.ts b/src/angular/components/two-factor-options.component.ts index e28fdef321..998ad76743 100644 --- a/src/angular/components/two-factor-options.component.ts +++ b/src/angular/components/two-factor-options.component.ts @@ -29,30 +29,7 @@ export class TwoFactorOptionsComponent implements OnInit { protected win: Window) { } ngOnInit() { - if (this.authService.twoFactorProviders.has(TwoFactorProviderType.OrganizationDuo)) { - this.providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]); - } - - if (this.authService.twoFactorProviders.has(TwoFactorProviderType.Authenticator)) { - this.providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]); - } - - if (this.authService.twoFactorProviders.has(TwoFactorProviderType.Yubikey)) { - this.providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]); - } - - if (this.authService.twoFactorProviders.has(TwoFactorProviderType.Duo)) { - this.providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]); - } - - if (this.authService.twoFactorProviders.has(TwoFactorProviderType.U2f) && - this.platformUtilsService.supportsU2f(this.win)) { - this.providers.push(TwoFactorProviders[TwoFactorProviderType.U2f]); - } - - if (this.authService.twoFactorProviders.has(TwoFactorProviderType.Email)) { - this.providers.push(TwoFactorProviders[TwoFactorProviderType.Email]); - } + this.providers = this.authService.getSupportedTwoFactorProviders(this.win); } choose(p: any) { diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 07ba8a0c32..318dceebf8 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -37,6 +37,12 @@ export const TwoFactorProviders = { description: null as string, priority: 2, }, + [TwoFactorProviderType.OrganizationDuo]: { + type: TwoFactorProviderType.OrganizationDuo, + name: 'Duo (Organization)', + description: null as string, + priority: 10, + }, [TwoFactorProviderType.U2f]: { type: TwoFactorProviderType.U2f, name: null as string, @@ -49,12 +55,6 @@ export const TwoFactorProviders = { description: null as string, priority: 0, }, - [TwoFactorProviderType.OrganizationDuo]: { - type: TwoFactorProviderType.OrganizationDuo, - name: 'Duo (Organization)', - description: null as string, - priority: 10, - }, }; export class AuthService { @@ -107,11 +107,53 @@ export class AuthService { twoFactorToken, remember); } + async logInComplete(email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType, + twoFactorToken: string, remember?: boolean): Promise { + this.selectedTwoFactorProviderType = null; + email = email.toLowerCase(); + const key = await this.cryptoService.makeKey(masterPassword, email); + const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); + return await this.logInHelper(email, hashedPassword, key, twoFactorProvider, twoFactorToken, remember); + } + logOut(callback: Function) { callback(); this.messagingService.send('loggedOut'); } + getSupportedTwoFactorProviders(win: Window): any[] { + const providers: any[] = []; + if (this.twoFactorProviders == null) { + return providers; + } + + if (this.twoFactorProviders.has(TwoFactorProviderType.OrganizationDuo)) { + providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]); + } + + if (this.twoFactorProviders.has(TwoFactorProviderType.Authenticator)) { + providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]); + } + + if (this.twoFactorProviders.has(TwoFactorProviderType.Yubikey)) { + providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]); + } + + if (this.twoFactorProviders.has(TwoFactorProviderType.Duo)) { + providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]); + } + + if (this.twoFactorProviders.has(TwoFactorProviderType.U2f) && this.platformUtilsService.supportsU2f(win)) { + providers.push(TwoFactorProviders[TwoFactorProviderType.U2f]); + } + + if (this.twoFactorProviders.has(TwoFactorProviderType.Email)) { + providers.push(TwoFactorProviders[TwoFactorProviderType.Email]); + } + + return providers; + } + getDefaultTwoFactorProvider(u2fSupported: boolean): TwoFactorProviderType { if (this.twoFactorProviders == null) { return null; diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 8ff31b0c6d..5df0f61911 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -248,8 +248,9 @@ export class CryptoService implements CryptoServiceAbstraction { } async hashPassword(password: string, key: SymmetricCryptoKey): Promise { - const storedKey = await this.getKey(); - key = key || storedKey; + if (key == null) { + key = await this.getKey(); + } if (password == null || key == null) { throw new Error('Invalid parameters.'); } From 799c90af1702c681874cb8fce8f8adcf1049d0c8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 15 May 2018 23:40:15 -0400 Subject: [PATCH 0241/1626] make logout callback async --- src/abstractions/api.service.ts | 1 - src/services/api.service.ts | 6 ++---- src/services/nodeApi.service.ts | 2 +- src/services/sync.service.ts | 4 ++-- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index db6f351637..eba6d8582d 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -20,7 +20,6 @@ export abstract class ApiService { baseUrl: string; identityBaseUrl: string; deviceType: string; - logoutCallback: Function; setUrls: (urls: EnvironmentUrls) => void; postIdentityToken: (request: TokenRequest) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index cda9888685..97eed0326f 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -27,11 +27,9 @@ export class ApiService implements ApiServiceAbstraction { baseUrl: string; identityBaseUrl: string; deviceType: string; - logoutCallback: Function; constructor(private tokenService: TokenService, private platformUtilsService: PlatformUtilsService, - logoutCallback: Function) { - this.logoutCallback = logoutCallback; + private logoutCallback: (expired: boolean) => Promise) { this.deviceType = platformUtilsService.getDevice().toString(); } @@ -425,7 +423,7 @@ export class ApiService implements ApiServiceAbstraction { private async handleError(response: Response, tokenError: boolean): Promise { if ((tokenError && response.status === 400) || response.status === 401 || response.status === 403) { - this.logoutCallback(true); + await this.logoutCallback(true); return null; } diff --git a/src/services/nodeApi.service.ts b/src/services/nodeApi.service.ts index 7803eafdc8..e5900f2409 100644 --- a/src/services/nodeApi.service.ts +++ b/src/services/nodeApi.service.ts @@ -12,7 +12,7 @@ import { TokenService } from '../abstractions/token.service'; export class NodeApiService extends ApiService { constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService, - logoutCallback: Function) { + logoutCallback: (expired: boolean) => Promise) { super(tokenService, platformUtilsService, logoutCallback); } } diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index 2a001e01a8..c053a34f63 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -30,7 +30,7 @@ export class SyncService implements SyncServiceAbstraction { private settingsService: SettingsService, private folderService: FolderService, private cipherService: CipherService, private cryptoService: CryptoService, private collectionService: CollectionService, private storageService: StorageService, - private messagingService: MessagingService, private logoutCallback: Function) { + private messagingService: MessagingService, private logoutCallback: (expired: boolean) => Promise) { } async getLastSync(): Promise { @@ -134,7 +134,7 @@ export class SyncService implements SyncServiceAbstraction { const stamp = await this.userService.getSecurityStamp(); if (stamp != null && stamp !== response.securityStamp) { if (this.logoutCallback != null) { - this.logoutCallback(true); + await this.logoutCallback(true); } throw new Error('Stamp has changed'); From e6fde2e92be4be472933234daa7bae242ee17794 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 16 May 2018 10:18:05 -0400 Subject: [PATCH 0242/1626] move lock service interval to init --- src/services/lock.service.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts index 43b9e57d56..dbc26f8140 100644 --- a/src/services/lock.service.ts +++ b/src/services/lock.service.ts @@ -10,12 +10,24 @@ import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { StorageService } from '../abstractions/storage.service'; export class LockService implements LockServiceAbstraction { + private inited = false; + constructor(private cipherService: CipherService, private folderService: FolderService, private collectionService: CollectionService, private cryptoService: CryptoService, private platformUtilsService: PlatformUtilsService, private storageService: StorageService, - private messagingService: MessagingService, private lockedCallback: Function) { - this.checkLock(); - setInterval(() => this.checkLock(), 10 * 1000); // check every 10 seconds + private messagingService: MessagingService, private lockedCallback: () => Promise) { + } + + init(checkOnInterval: boolean) { + if (this.inited) { + return; + } + + this.inited = true; + if (checkOnInterval) { + this.checkLock(); + setInterval(() => this.checkLock(), 10 * 1000); // check every 10 seconds + } } async checkLock(): Promise { @@ -60,7 +72,9 @@ export class LockService implements LockServiceAbstraction { this.cipherService.clearCache(); this.collectionService.clearCache(); this.messagingService.send('locked'); - this.lockedCallback(); + if (this.lockedCallback != null) { + await this.lockedCallback(); + } } async setLockOption(lockOption: number): Promise { From cf5bce1ea91ebbc1c99a550b62114a88bfeb2036 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 16 May 2018 15:30:28 -0400 Subject: [PATCH 0243/1626] supports duo platform util --- src/abstractions/platformUtils.service.ts | 1 + src/electron/services/electronPlatformUtils.service.ts | 4 ++++ src/services/auth.service.ts | 5 +++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index 05bbfaab7d..c15da85f2c 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -18,6 +18,7 @@ export abstract class PlatformUtilsService { saveFile: (win: Window, blobData: any, blobOptions: any, fileName: string) => void; getApplicationVersion: () => string; supportsU2f: (win: Window) => boolean; + supportsDuo: () => boolean; showDialog: (text: string, title?: string, confirmText?: string, cancelText?: string, type?: string) => Promise; isDev: () => boolean; diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index 6c4509e0a1..c6444e183e 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -123,6 +123,10 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { return false; } + supportsDuo(): boolean { + return true; + } + showDialog(text: string, title?: string, confirmText?: string, cancelText?: string, type?: string): Promise { const buttons = [confirmText == null ? this.i18nService.t('ok') : confirmText]; diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 318dceebf8..2eaaa0fe14 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -127,7 +127,8 @@ export class AuthService { return providers; } - if (this.twoFactorProviders.has(TwoFactorProviderType.OrganizationDuo)) { + if (this.twoFactorProviders.has(TwoFactorProviderType.OrganizationDuo) && + this.platformUtilsService.supportsDuo()) { providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]); } @@ -139,7 +140,7 @@ export class AuthService { providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]); } - if (this.twoFactorProviders.has(TwoFactorProviderType.Duo)) { + if (this.twoFactorProviders.has(TwoFactorProviderType.Duo) && this.platformUtilsService.supportsDuo()) { providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]); } From 12b01e18946067cd79f87c775a65467a377fabd6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 16 May 2018 16:02:05 -0400 Subject: [PATCH 0244/1626] remove forge import from crypto service --- src/services/crypto.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 5df0f61911..7d72e3ee85 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -1,5 +1,3 @@ -import * as forge from 'node-forge'; - import { EncryptionType } from '../enums/encryptionType'; import { CipherString } from '../models/domain/cipherString'; From 1fdb694fae15cc1b46c4fb55ed6e37be819d859c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 16 May 2018 16:18:00 -0400 Subject: [PATCH 0245/1626] dont include search in barrel file --- src/services/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/index.ts b/src/services/index.ts index bd096fdc10..bff56b273c 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -12,7 +12,6 @@ export { FolderService } from './folder.service'; export { I18nService } from './i18n.service'; export { LockService } from './lock.service'; export { PasswordGenerationService } from './passwordGeneration.service'; -export { SearchService } from './search.service'; export { SettingsService } from './settings.service'; export { StateService } from './state.service'; export { SyncService } from './sync.service'; From ba10d0704212f2bc8fabf0d3d6ebb552fd183401 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 17 May 2018 10:52:06 -0400 Subject: [PATCH 0246/1626] export service --- src/abstractions/export.service.ts | 4 + src/angular/components/export.component.ts | 123 +-------------------- src/services/export.service.ts | 112 +++++++++++++++++++ 3 files changed, 120 insertions(+), 119 deletions(-) create mode 100644 src/abstractions/export.service.ts create mode 100644 src/services/export.service.ts diff --git a/src/abstractions/export.service.ts b/src/abstractions/export.service.ts new file mode 100644 index 0000000000..e03ce20644 --- /dev/null +++ b/src/abstractions/export.service.ts @@ -0,0 +1,4 @@ +export abstract class ExportService { + getCsv: () => Promise; + getFileName: () => string; +} diff --git a/src/angular/components/export.component.ts b/src/angular/components/export.component.ts index ac5a6a87a0..964d8f61b7 100644 --- a/src/angular/components/export.component.ts +++ b/src/angular/components/export.component.ts @@ -1,5 +1,3 @@ -import * as papa from 'papaparse'; - import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; @@ -8,14 +6,8 @@ import { Output, } from '@angular/core'; -import { CipherType } from '../../enums/cipherType'; - -import { CipherView } from '../../models/view/cipherView'; -import { FolderView } from '../../models/view/folderView'; - -import { CipherService } from '../../abstractions/cipher.service'; import { CryptoService } from '../../abstractions/crypto.service'; -import { FolderService } from '../../abstractions/folder.service'; +import { ExportService } from '../../abstractions/export.service'; import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { UserService } from '../../abstractions/user.service'; @@ -27,10 +19,9 @@ export class ExportComponent { showPassword = false; constructor(protected analytics: Angulartics2, protected toasterService: ToasterService, - protected cipherService: CipherService, protected folderService: FolderService, protected cryptoService: CryptoService, protected userService: UserService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, - protected win: Window) { } + protected exportService: ExportService, protected win: Window) { } async submit() { if (this.masterPassword == null || this.masterPassword === '') { @@ -45,7 +36,7 @@ export class ExportComponent { const storedKeyHash = await this.cryptoService.getKeyHash(); if (storedKeyHash != null && keyHash != null && storedKeyHash === keyHash) { - const csv = await this.getCsv(); + const csv = await this.exportService.getCsv(); this.analytics.eventTrack.next({ action: 'Exported Data' }); this.downloadFile(csv); this.saved(); @@ -65,114 +56,8 @@ export class ExportComponent { this.onSaved.emit(); } - private async checkPassword() { - const email = await this.userService.getEmail(); - const key = await this.cryptoService.makeKey(this.masterPassword, email); - const keyHash = await this.cryptoService.hashPassword(this.masterPassword, key); - const storedKeyHash = await this.cryptoService.getKeyHash(); - if (storedKeyHash == null || keyHash == null || storedKeyHash !== keyHash) { - throw new Error('Invalid password.'); - } - } - - private async getCsv(): Promise { - let decFolders: FolderView[] = []; - let decCiphers: CipherView[] = []; - const promises = []; - - promises.push(this.folderService.getAllDecrypted().then((folders) => { - decFolders = folders; - })); - - promises.push(this.cipherService.getAllDecrypted().then((ciphers) => { - decCiphers = ciphers; - })); - - await Promise.all(promises); - - const foldersMap = new Map(); - decFolders.forEach((f) => { - foldersMap.set(f.id, f); - }); - - const exportCiphers: any[] = []; - decCiphers.forEach((c) => { - // only export logins and secure notes - if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) { - return; - } - - const cipher: any = { - folder: c.folderId && foldersMap.has(c.folderId) ? foldersMap.get(c.folderId).name : null, - favorite: c.favorite ? 1 : null, - type: null, - name: c.name, - notes: c.notes, - fields: null, - // Login props - login_uri: null, - login_username: null, - login_password: null, - login_totp: null, - }; - - if (c.fields) { - c.fields.forEach((f: any) => { - if (!cipher.fields) { - cipher.fields = ''; - } else { - cipher.fields += '\n'; - } - - cipher.fields += ((f.name || '') + ': ' + f.value); - }); - } - - switch (c.type) { - case CipherType.Login: - cipher.type = 'login'; - cipher.login_username = c.login.username; - cipher.login_password = c.login.password; - cipher.login_totp = c.login.totp; - - if (c.login.uris) { - cipher.login_uri = []; - c.login.uris.forEach((u) => { - cipher.login_uri.push(u.uri); - }); - } - break; - case CipherType.SecureNote: - cipher.type = 'note'; - break; - default: - return; - } - - exportCiphers.push(cipher); - }); - - return papa.unparse(exportCiphers); - } - private downloadFile(csv: string): void { - const fileName = this.makeFileName(); + const fileName = this.exportService.getFileName(); this.platformUtilsService.saveFile(this.win, csv, { type: 'text/plain' }, fileName); } - - private makeFileName(): string { - const now = new Date(); - const dateString = - now.getFullYear() + '' + this.padNumber(now.getMonth() + 1, 2) + '' + this.padNumber(now.getDate(), 2) + - this.padNumber(now.getHours(), 2) + '' + this.padNumber(now.getMinutes(), 2) + - this.padNumber(now.getSeconds(), 2); - - return 'bitwarden_export_' + dateString + '.csv'; - } - - private padNumber(num: number, width: number, padCharacter: string = '0'): string { - const numString = num.toString(); - return numString.length >= width ? numString : - new Array(width - numString.length + 1).join(padCharacter) + numString; - } } diff --git a/src/services/export.service.ts b/src/services/export.service.ts new file mode 100644 index 0000000000..6f2a1ae3ee --- /dev/null +++ b/src/services/export.service.ts @@ -0,0 +1,112 @@ +import * as papa from 'papaparse'; + +import { CipherType } from '../enums/cipherType'; + +import { CipherService } from '../abstractions/cipher.service'; +import { ExportService as ExportServiceAbstraction } from '../abstractions/export.service'; +import { FolderService } from '../abstractions/folder.service'; + +import { CipherView } from '../models/view/cipherView'; +import { FolderView } from '../models/view/folderView'; + +import { Utils } from '../misc/utils'; + +export class ExportService implements ExportServiceAbstraction { + constructor(private folderService: FolderService, private cipherService: CipherService) { } + + async getCsv(): Promise { + let decFolders: FolderView[] = []; + let decCiphers: CipherView[] = []; + const promises = []; + + promises.push(this.folderService.getAllDecrypted().then((folders) => { + decFolders = folders; + })); + + promises.push(this.cipherService.getAllDecrypted().then((ciphers) => { + decCiphers = ciphers; + })); + + await Promise.all(promises); + + const foldersMap = new Map(); + decFolders.forEach((f) => { + foldersMap.set(f.id, f); + }); + + const exportCiphers: any[] = []; + decCiphers.forEach((c) => { + // only export logins and secure notes + if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) { + return; + } + + const cipher: any = { + folder: c.folderId && foldersMap.has(c.folderId) ? foldersMap.get(c.folderId).name : null, + favorite: c.favorite ? 1 : null, + type: null, + name: c.name, + notes: c.notes, + fields: null, + // Login props + login_uri: null, + login_username: null, + login_password: null, + login_totp: null, + }; + + if (c.fields) { + c.fields.forEach((f: any) => { + if (!cipher.fields) { + cipher.fields = ''; + } else { + cipher.fields += '\n'; + } + + cipher.fields += ((f.name || '') + ': ' + f.value); + }); + } + + switch (c.type) { + case CipherType.Login: + cipher.type = 'login'; + cipher.login_username = c.login.username; + cipher.login_password = c.login.password; + cipher.login_totp = c.login.totp; + + if (c.login.uris) { + cipher.login_uri = []; + c.login.uris.forEach((u) => { + cipher.login_uri.push(u.uri); + }); + } + break; + case CipherType.SecureNote: + cipher.type = 'note'; + break; + default: + return; + } + + exportCiphers.push(cipher); + }); + + return papa.unparse(exportCiphers); + } + + getFileName(): string { + const now = new Date(); + const dateString = + now.getFullYear() + '' + this.padNumber(now.getMonth() + 1, 2) + '' + this.padNumber(now.getDate(), 2) + + this.padNumber(now.getHours(), 2) + '' + this.padNumber(now.getMinutes(), 2) + + this.padNumber(now.getSeconds(), 2); + + return 'bitwarden_export_' + dateString + '.csv'; + } + + private padNumber(num: number, width: number, padCharacter: string = '0'): string { + const numString = num.toString(); + return numString.length >= width ? numString : + new Array(width - numString.length + 1).join(padCharacter) + numString; + } +} From 33d690cbd5ad806bdfbde5f04d5fce540f4fe9ac Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 17 May 2018 11:09:22 -0400 Subject: [PATCH 0247/1626] install papaparse --- package-lock.json | 5 +++++ package.json | 1 + 2 files changed, 6 insertions(+) diff --git a/package-lock.json b/package-lock.json index 0899866723..cf1699064e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6561,6 +6561,11 @@ "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", "dev": true }, + "papaparse": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-4.3.5.tgz", + "integrity": "sha1-ts31yub+nsYDsb5m8RSmOsZFoDY=" + }, "parse-asn1": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", diff --git a/package.json b/package.json index 4dde128f73..200dfff01d 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "lunr": "2.1.6", "node-fetch": "2.1.2", "node-forge": "0.7.1", + "papaparse": "4.3.5", "rxjs": "5.5.6", "zone.js": "0.8.19" } From ed89dfaba70b60925817a0ce6f0c179b3f8bd2fb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 17 May 2018 13:25:03 -0400 Subject: [PATCH 0248/1626] allow empty ctor --- src/models/view/attachmentView.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/models/view/attachmentView.ts b/src/models/view/attachmentView.ts index 49ea57326d..efb0081efa 100644 --- a/src/models/view/attachmentView.ts +++ b/src/models/view/attachmentView.ts @@ -9,7 +9,11 @@ export class AttachmentView implements View { sizeName: string; fileName: string; - constructor(a: Attachment) { + constructor(a?: Attachment) { + if (!a) { + return; + } + this.id = a.id; this.url = a.url; this.size = a.size; From a421f6e64a1f47c34806419012b3983bd0505bc6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 17 May 2018 15:35:02 -0400 Subject: [PATCH 0249/1626] raw attachment saves with node form data --- package-lock.json | 21 +++++----- package.json | 2 + src/abstractions/cipher.service.ts | 3 +- src/services/cipher.service.ts | 61 +++++++++++++++++++----------- src/services/nodeApi.service.ts | 2 + 5 files changed, 56 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index cf1699064e..3894c258a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,6 +84,15 @@ "tslib": "1.9.0" } }, + "@types/form-data": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", + "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", + "dev": true, + "requires": { + "@types/node": "8.0.19" + } + }, "@types/jasmine": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.6.tgz", @@ -383,8 +392,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "atob": { "version": "2.1.0", @@ -1249,7 +1257,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "dev": true, "requires": { "delayed-stream": "1.0.0" } @@ -1759,8 +1766,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "depd": { "version": "1.1.2", @@ -2575,7 +2581,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", - "dev": true, "requires": { "asynckit": "0.4.0", "combined-stream": "1.0.6", @@ -5642,14 +5647,12 @@ "mime-db": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", - "dev": true + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" }, "mime-types": { "version": "2.1.18", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "dev": true, "requires": { "mime-db": "1.33.0" } diff --git a/package.json b/package.json index 200dfff01d..7ff778325a 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "test:node:watch": "concurrently -k -n TSC,Node -c yellow,cyan \"npm run build:watch\" \"nodemon -w ./dist --delay 500ms --exec jasmine\"" }, "devDependencies": { + "@types/form-data": "^2.2.1", "@types/jasmine": "^2.8.2", "@types/keytar": "^4.0.1", "@types/lunr": "2.1.5", @@ -72,6 +73,7 @@ "electron-log": "2.2.14", "electron-store": "1.3.0", "electron-updater": "2.21.4", + "form-data": "2.3.2", "keytar": "4.1.0", "lunr": "2.1.6", "node-fetch": "2.1.2", diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index 2e344dffd8..73ee81047c 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -25,7 +25,8 @@ export abstract class CipherService { updateLastUsedDate: (id: string) => Promise; saveNeverDomain: (domain: string) => Promise; saveWithServer: (cipher: Cipher) => Promise; - saveAttachmentWithServer: (cipher: Cipher, unencryptedFile: any) => Promise; + saveAttachmentWithServer: (cipher: Cipher, unencryptedFile: any) => Promise; + saveAttachmentRawWithServer: (cipher: Cipher, filename: string, data: ArrayBuffer) => Promise; upsert: (cipher: CipherData | CipherData[]) => Promise; replace: (ciphers: { [id: string]: CipherData; }) => Promise; clear: (userId: string) => Promise; diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index e2be81f25f..b2e7fec063 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -322,43 +322,58 @@ export class CipherService implements CipherServiceAbstraction { await this.upsert(data); } - saveAttachmentWithServer(cipher: Cipher, unencryptedFile: any): Promise { - const self = this; - + saveAttachmentWithServer(cipher: Cipher, unencryptedFile: any): Promise { 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); - - let response: CipherResponse; try { - response = await self.apiService.postCipherAttachment(cipher.id, fd); + const cData = await this.saveAttachmentRawWithServer(cipher, + unencryptedFile.name, evt.target.result); + resolve(cData); } catch (e) { - reject((e as ErrorResponse).getSingleMessage()); - return; + reject(e); } - - const userId = await self.userService.getUserId(); - const data = new CipherData(response, userId, cipher.collectionIds); - this.upsert(data); - resolve(new Cipher(data)); - }; - reader.onerror = (evt) => { reject('Error reading file.'); }; }); } + async saveAttachmentRawWithServer(cipher: Cipher, filename: string, data: ArrayBuffer): Promise { + const key = await this.cryptoService.getOrgKey(cipher.organizationId); + const encFileName = await this.cryptoService.encrypt(filename, key); + const encData = await this.cryptoService.encryptToBytes(data, key); + + const fd = new FormData(); + try { + const blob = new Blob([encData], { type: 'application/octet-stream' }); + fd.append('data', blob, encFileName.encryptedString); + } catch (e) { + if (Utils.isNode) { + fd.append('data', new Buffer(encData) as any, { + filename: encFileName.encryptedString, + contentType: 'application/octet-stream', + } as any); + } else { + throw e; + } + } + + let response: CipherResponse; + try { + response = await this.apiService.postCipherAttachment(cipher.id, fd); + } catch (e) { + throw new Error((e as ErrorResponse).getSingleMessage()); + } + + const userId = await this.userService.getUserId(); + const cData = new CipherData(response, userId, cipher.collectionIds); + this.upsert(cData); + return new Cipher(cData); + } + async upsert(cipher: CipherData | CipherData[]): Promise { const userId = await this.userService.getUserId(); let ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( diff --git a/src/services/nodeApi.service.ts b/src/services/nodeApi.service.ts index e5900f2409..3815d46ec4 100644 --- a/src/services/nodeApi.service.ts +++ b/src/services/nodeApi.service.ts @@ -1,3 +1,4 @@ +import * as FormData from 'form-data'; import * as fe from 'node-fetch'; import { ApiService } from './api.service'; @@ -9,6 +10,7 @@ import { TokenService } from '../abstractions/token.service'; (global as any).Request = fe.Request; (global as any).Response = fe.Response; (global as any).Headers = fe.Headers; +(global as any).FormData = FormData; export class NodeApiService extends ApiService { constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService, From bf260819bb303d125379c92e712f762fe17f043c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 18 May 2018 15:26:46 -0400 Subject: [PATCH 0250/1626] sync organizations --- src/abstractions/user.service.ts | 7 +++++ src/models/data/organizationData.ts | 20 +++++++++++++ src/models/domain/organization.ts | 24 ++++++++++++++++ src/services/sync.service.ts | 8 ++++++ src/services/user.service.ts | 44 +++++++++++++++++++++++++++-- 5 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 src/models/data/organizationData.ts create mode 100644 src/models/domain/organization.ts diff --git a/src/abstractions/user.service.ts b/src/abstractions/user.service.ts index 5621c3be50..1a16c4a1fa 100644 --- a/src/abstractions/user.service.ts +++ b/src/abstractions/user.service.ts @@ -1,3 +1,6 @@ +import { OrganizationData } from '../models/data/organizationData'; +import { Organization } from '../models/domain/organization'; + export abstract class UserService { userId: string; email: string; @@ -10,4 +13,8 @@ export abstract class UserService { getSecurityStamp: () => Promise; clear: () => Promise; isAuthenticated: () => Promise; + getOrganization: (id: string) => Promise; + getAllOrganizations: () => Promise; + replaceOrganizations: (organizations: { [id: string]: OrganizationData; }) => Promise; + clearOrganizations: (userId: string) => Promise; } diff --git a/src/models/data/organizationData.ts b/src/models/data/organizationData.ts new file mode 100644 index 0000000000..1af5223391 --- /dev/null +++ b/src/models/data/organizationData.ts @@ -0,0 +1,20 @@ +import { ProfileOrganizationResponse } from '../response/profileOrganizationResponse'; + +import { OrganizationUserStatusType } from '../../enums/organizationUserStatusType'; +import { OrganizationUserType } from '../../enums/organizationUserType'; + +export class OrganizationData { + id: string; + name: string; + status: OrganizationUserStatusType; + type: OrganizationUserType; + enabled: boolean; + + constructor(response: ProfileOrganizationResponse) { + this.id = response.id; + this.name = response.name; + this.status = response.status; + this.type = response.type; + this.enabled = response.enabled; + } +} diff --git a/src/models/domain/organization.ts b/src/models/domain/organization.ts new file mode 100644 index 0000000000..69f93b34c0 --- /dev/null +++ b/src/models/domain/organization.ts @@ -0,0 +1,24 @@ +import { OrganizationData } from '../data/organizationData'; + +import { OrganizationUserStatusType } from '../../enums/organizationUserStatusType'; +import { OrganizationUserType } from '../../enums/organizationUserType'; + +export class Organization { + id: string; + name: string; + status: OrganizationUserStatusType; + type: OrganizationUserType; + enabled: boolean; + + constructor(obj?: OrganizationData) { + if (obj == null) { + return; + } + + this.id = obj.id; + this.name = obj.name; + this.status = obj.status; + this.type = obj.type; + this.enabled = obj.enabled; + } +} diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index c053a34f63..2e025772c5 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -12,6 +12,7 @@ import { UserService } from '../abstractions/user.service'; import { CipherData } from '../models/data/cipherData'; import { CollectionData } from '../models/data/collectionData'; import { FolderData } from '../models/data/folderData'; +import { OrganizationData } from '../models/data/organizationData'; import { CipherResponse } from '../models/response/cipherResponse'; import { CollectionResponse } from '../models/response/collectionResponse'; @@ -144,6 +145,13 @@ export class SyncService implements SyncServiceAbstraction { await this.cryptoService.setEncPrivateKey(response.privateKey); await this.cryptoService.setOrgKeys(response.organizations); await this.userService.setSecurityStamp(response.securityStamp); + await this.userService.setSecurityStamp(response.securityStamp); + + const organizations: { [id: string]: OrganizationData; } = {}; + response.organizations.forEach((o) => { + organizations[o.id] = new OrganizationData(o); + }); + return await this.userService.replaceOrganizations(organizations); } private async syncFolders(userId: string, response: FolderResponse[]) { diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 1949cadb48..3b149643d0 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -1,14 +1,18 @@ import { StorageService } from '../abstractions/storage.service'; import { TokenService } from '../abstractions/token.service'; -import { UserService as UserServiceAbsrtaction } from '../abstractions/user.service'; +import { UserService as UserServiceAbstraction } from '../abstractions/user.service'; + +import { OrganizationData } from '../models/data/organizationData'; +import { Organization } from '../models/domain/organization'; const Keys = { userId: 'userId', userEmail: 'userEmail', stamp: 'securityStamp', + organizationsPrefix: 'organizations_', }; -export class UserService implements UserServiceAbsrtaction { +export class UserService implements UserServiceAbstraction { userId: string; email: string; stamp: string; @@ -59,10 +63,13 @@ export class UserService implements UserServiceAbsrtaction { } async clear(): Promise { + const userId = await this.getUserId(); + await Promise.all([ this.storageService.remove(Keys.userId), this.storageService.remove(Keys.userEmail), this.storageService.remove(Keys.stamp), + this.clearOrganizations(userId), ]); this.userId = this.email = this.stamp = null; @@ -77,4 +84,37 @@ export class UserService implements UserServiceAbsrtaction { const userId = await this.getUserId(); return userId != null; } + + async getOrganization(id: string): Promise { + const userId = await this.getUserId(); + const organizations = await this.storageService.get<{ [id: string]: OrganizationData; }>( + Keys.organizationsPrefix + userId); + if (organizations == null || !organizations.hasOwnProperty(id)) { + return null; + } + + return new Organization(organizations[id]); + } + + async getAllOrganizations(): Promise { + const userId = await this.getUserId(); + const organizations = await this.storageService.get<{ [id: string]: OrganizationData; }>( + Keys.organizationsPrefix + userId); + const response: Organization[] = []; + for (const id in organizations) { + if (organizations.hasOwnProperty(id)) { + response.push(new Organization(organizations[id])); + } + } + return response; + } + + async replaceOrganizations(organizations: { [id: string]: OrganizationData; }): Promise { + const userId = await this.getUserId(); + await this.storageService.save(Keys.organizationsPrefix + userId, organizations); + } + + async clearOrganizations(userId: string): Promise { + await this.storageService.remove(Keys.organizationsPrefix + userId); + } } From 43ac05d1fc89a5132bd2eb632a85e608e2548252 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 21 May 2018 09:10:13 -0400 Subject: [PATCH 0251/1626] switch to node crypto for rsa encrypt/decrypt --- src/services/nodeCryptoFunction.service.ts | 36 ++++++++++------------ 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index 69fb64c2e4..08b19dd0dd 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -114,31 +114,23 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { } rsaEncrypt(data: ArrayBuffer, publicKey: ArrayBuffer, algorithm: 'sha1' | 'sha256'): Promise { - let md: forge.md.MessageDigest; if (algorithm === 'sha256') { - md = forge.md.sha256.create(); - } else { - md = forge.md.sha1.create(); + throw new Error('Node crypto does not support RSA-OAEP SHA-256'); } - const dataBytes = Utils.fromBufferToByteString(data); - const key = this.toForgePublicKey(publicKey); - const decBytes: string = key.encrypt(dataBytes, 'RSA-OAEP', { md: md }); - return Promise.resolve(Utils.fromByteStringToArray(decBytes).buffer); + const pem = this.toPemPublicKey(publicKey); + const decipher = crypto.publicEncrypt(pem, this.toNodeBuffer(data)); + return Promise.resolve(this.toArrayBuffer(decipher)); } rsaDecrypt(data: ArrayBuffer, privateKey: ArrayBuffer, algorithm: 'sha1' | 'sha256'): Promise { - let md: forge.md.MessageDigest; if (algorithm === 'sha256') { - md = forge.md.sha256.create(); - } else { - md = forge.md.sha1.create(); + throw new Error('Node crypto does not support RSA-OAEP SHA-256'); } - const dataBytes = Utils.fromBufferToByteString(data); - const key = this.toForgePrivateKey(privateKey); - const decBytes: string = key.decrypt(dataBytes, 'RSA-OAEP', { md: md }); - return Promise.resolve(Utils.fromByteStringToArray(decBytes).buffer); + const pem = this.toPemPrivateKey(privateKey); + const decipher = crypto.privateDecrypt(pem, this.toNodeBuffer(data)); + return Promise.resolve(this.toArrayBuffer(decipher)); } randomBytes(length: number): Promise { @@ -171,15 +163,19 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { return new Uint8Array(buf).buffer; } - private toForgePrivateKey(key: ArrayBuffer): any { + private toPemPrivateKey(key: ArrayBuffer): string { const byteString = Utils.fromBufferToByteString(key); const asn1 = forge.asn1.fromDer(byteString); - return (forge as any).pki.privateKeyFromAsn1(asn1); + const privateKey = (forge as any).pki.privateKeyFromAsn1(asn1); + const rsaPrivateKey = (forge.pki as any).privateKeyToAsn1(privateKey); + const privateKeyInfo = (forge.pki as any).wrapRsaPrivateKey(rsaPrivateKey); + return (forge.pki as any).privateKeyInfoToPem(privateKeyInfo); } - private toForgePublicKey(key: ArrayBuffer): any { + private toPemPublicKey(key: ArrayBuffer): string { const byteString = Utils.fromBufferToByteString(key); const asn1 = forge.asn1.fromDer(byteString); - return (forge as any).pki.publicKeyFromAsn1(asn1); + const publicKey = (forge as any).pki.publicKeyFromAsn1(asn1); + return (forge.pki as any).publicKeyToPem(publicKey); } } From d3f3f47286f2cb0f35b08af9afec84cdf6182994 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 25 May 2018 08:39:48 -0400 Subject: [PATCH 0252/1626] switch to filepath --- src/services/cipher.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index b2e7fec063..91966cb3f2 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -353,7 +353,7 @@ export class CipherService implements CipherServiceAbstraction { } catch (e) { if (Utils.isNode) { fd.append('data', new Buffer(encData) as any, { - filename: encFileName.encryptedString, + filepath: encFileName.encryptedString, contentType: 'application/octet-stream', } as any); } else { From 4c8f9c6b7c797a117b666a88fb9b1b218569ce3a Mon Sep 17 00:00:00 2001 From: yxzzx Date: Tue, 29 May 2018 20:34:32 +0200 Subject: [PATCH 0253/1626] make src/services/api.service.ts work with https servers requiring client certificates. (#4) --- src/services/api.service.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 97eed0326f..7eaf415f8a 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -76,6 +76,7 @@ export class ApiService implements ApiServiceAbstraction { async postIdentityToken(request: TokenRequest): Promise { const response = await fetch(new Request(this.identityBaseUrl + '/connect/token', { body: this.qsStringify(request.toIdentityToken(this.platformUtilsService.identityClientId)), + credentials: 'include', cache: 'no-cache', headers: new Headers({ 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', @@ -118,6 +119,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/two-factor/send-email-login', { body: JSON.stringify(request), cache: 'no-cache', + credentials: 'include', headers: new Headers({ 'Content-Type': 'application/json; charset=utf-8', 'Device-Type': this.deviceType, @@ -157,6 +159,7 @@ export class ApiService implements ApiServiceAbstraction { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/accounts/revision-date', { cache: 'no-cache', + credentials: 'include', headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -176,6 +179,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/accounts/password-hint', { body: JSON.stringify(request), cache: 'no-cache', + credentials: 'include', headers: new Headers({ 'Content-Type': 'application/json; charset=utf-8', 'Device-Type': this.deviceType, @@ -193,6 +197,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/accounts/register', { body: JSON.stringify(request), cache: 'no-cache', + credentials: 'include', headers: new Headers({ 'Content-Type': 'application/json; charset=utf-8', 'Device-Type': this.deviceType, @@ -213,6 +218,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/folders', { body: JSON.stringify(request), cache: 'no-cache', + credentials: 'include', headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -236,6 +242,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/folders/' + id, { body: JSON.stringify(request), cache: 'no-cache', + credentials: 'include', headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -258,6 +265,7 @@ export class ApiService implements ApiServiceAbstraction { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/folders/' + id, { cache: 'no-cache', + credentials: 'include', headers: new Headers({ 'Authorization': authHeader, 'Device-Type': this.deviceType, @@ -278,6 +286,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/ciphers', { body: JSON.stringify(request), cache: 'no-cache', + credentials: 'include', headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -301,6 +310,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { body: JSON.stringify(request), cache: 'no-cache', + credentials: 'include', headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -323,6 +333,7 @@ export class ApiService implements ApiServiceAbstraction { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { cache: 'no-cache', + credentials: 'include', headers: new Headers({ 'Authorization': authHeader, 'Device-Type': this.deviceType, @@ -343,6 +354,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment', { body: data, cache: 'no-cache', + credentials: 'include', headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -364,6 +376,7 @@ export class ApiService implements ApiServiceAbstraction { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment/' + attachmentId, { cache: 'no-cache', + credentials: 'include', headers: new Headers({ 'Authorization': authHeader, 'Device-Type': this.deviceType, @@ -383,6 +396,7 @@ export class ApiService implements ApiServiceAbstraction { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/sync', { cache: 'no-cache', + credentials: 'include', headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -460,6 +474,7 @@ export class ApiService implements ApiServiceAbstraction { refresh_token: refreshToken, }), cache: 'no-cache', + credentials: 'include', headers: new Headers({ 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 'Accept': 'application/json', From c71b9703f3a7ca6f791e5f813c418c8ed23855b7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 29 May 2018 14:36:20 -0400 Subject: [PATCH 0254/1626] credentials: 'include' --- src/services/api.service.ts | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 7eaf415f8a..5d17a6c28c 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -76,7 +76,7 @@ export class ApiService implements ApiServiceAbstraction { async postIdentityToken(request: TokenRequest): Promise { const response = await fetch(new Request(this.identityBaseUrl + '/connect/token', { body: this.qsStringify(request.toIdentityToken(this.platformUtilsService.identityClientId)), - credentials: 'include', + credentials: 'include', cache: 'no-cache', headers: new Headers({ 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', @@ -119,7 +119,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/two-factor/send-email-login', { body: JSON.stringify(request), cache: 'no-cache', - credentials: 'include', + credentials: 'include', headers: new Headers({ 'Content-Type': 'application/json; charset=utf-8', 'Device-Type': this.deviceType, @@ -139,6 +139,7 @@ export class ApiService implements ApiServiceAbstraction { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/accounts/profile', { cache: 'no-cache', + credentials: 'include', headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -159,7 +160,7 @@ export class ApiService implements ApiServiceAbstraction { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/accounts/revision-date', { cache: 'no-cache', - credentials: 'include', + credentials: 'include', headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -179,7 +180,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/accounts/password-hint', { body: JSON.stringify(request), cache: 'no-cache', - credentials: 'include', + credentials: 'include', headers: new Headers({ 'Content-Type': 'application/json; charset=utf-8', 'Device-Type': this.deviceType, @@ -197,7 +198,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/accounts/register', { body: JSON.stringify(request), cache: 'no-cache', - credentials: 'include', + credentials: 'include', headers: new Headers({ 'Content-Type': 'application/json; charset=utf-8', 'Device-Type': this.deviceType, @@ -218,7 +219,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/folders', { body: JSON.stringify(request), cache: 'no-cache', - credentials: 'include', + credentials: 'include', headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -242,7 +243,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/folders/' + id, { body: JSON.stringify(request), cache: 'no-cache', - credentials: 'include', + credentials: 'include', headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -265,7 +266,7 @@ export class ApiService implements ApiServiceAbstraction { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/folders/' + id, { cache: 'no-cache', - credentials: 'include', + credentials: 'include', headers: new Headers({ 'Authorization': authHeader, 'Device-Type': this.deviceType, @@ -286,7 +287,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/ciphers', { body: JSON.stringify(request), cache: 'no-cache', - credentials: 'include', + credentials: 'include', headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -310,7 +311,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { body: JSON.stringify(request), cache: 'no-cache', - credentials: 'include', + credentials: 'include', headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -333,7 +334,7 @@ export class ApiService implements ApiServiceAbstraction { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { cache: 'no-cache', - credentials: 'include', + credentials: 'include', headers: new Headers({ 'Authorization': authHeader, 'Device-Type': this.deviceType, @@ -354,7 +355,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment', { body: data, cache: 'no-cache', - credentials: 'include', + credentials: 'include', headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -376,7 +377,7 @@ export class ApiService implements ApiServiceAbstraction { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment/' + attachmentId, { cache: 'no-cache', - credentials: 'include', + credentials: 'include', headers: new Headers({ 'Authorization': authHeader, 'Device-Type': this.deviceType, @@ -396,7 +397,7 @@ export class ApiService implements ApiServiceAbstraction { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/sync', { cache: 'no-cache', - credentials: 'include', + credentials: 'include', headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -418,6 +419,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/organizations/' + organizationId + '/import', { body: JSON.stringify(request), cache: 'no-cache', + credentials: 'include', headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -474,7 +476,7 @@ export class ApiService implements ApiServiceAbstraction { refresh_token: refreshToken, }), cache: 'no-cache', - credentials: 'include', + credentials: 'include', headers: new Headers({ 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 'Accept': 'application/json', From f99b34d0622873b49f1cd9b8c586da563172cda1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 29 May 2018 23:06:12 -0400 Subject: [PATCH 0255/1626] no tray ctx menu for macos due to electorn bug --- src/electron/tray.main.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/electron/tray.main.ts b/src/electron/tray.main.ts index 716a03115f..5a8ec4b3fc 100644 --- a/src/electron/tray.main.ts +++ b/src/electron/tray.main.ts @@ -52,7 +52,9 @@ export class TrayMain { menuItemOptions.splice(1, 0, ...additionalMenuItems); } - this.contextMenu = Menu.buildFromTemplate(menuItemOptions); + if (process.platform !== 'darwin') { + this.contextMenu = Menu.buildFromTemplate(menuItemOptions); + } if (await this.storageService.get(ElectronConstants.enableTrayKey)) { this.showTray(); } From 49dece79834e9d93be923dbc2fa8f9edc26f9adf Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 29 May 2018 23:16:50 -0400 Subject: [PATCH 0256/1626] theme key --- src/services/constants.service.ts | 105 +----------------------------- 1 file changed, 2 insertions(+), 103 deletions(-) diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index 2326534f88..efc966759b 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -1,5 +1,3 @@ -import { PlatformUtilsService } from '../abstractions/platformUtils.service'; - export class ConstantsService { static readonly environmentUrlsKey: string = 'environmentUrls'; static readonly disableGaKey: string = 'disableGa'; @@ -13,6 +11,7 @@ export class ConstantsService { static readonly neverDomainsKey: string = 'neverDomains'; static readonly installedVersionKey: string = 'installedVersion'; static readonly localeKey: string = 'locale'; + static readonly themeKey: string = 'theme'; readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; readonly disableGaKey: string = ConstantsService.disableGaKey; @@ -26,105 +25,5 @@ export class ConstantsService { readonly neverDomainsKey: string = ConstantsService.neverDomainsKey; readonly installedVersionKey: string = ConstantsService.installedVersionKey; readonly localeKey: string = ConstantsService.localeKey; - - // 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, - organizationDuo: 6, - }; - - twoFactorProviderInfo: any[]; - - constructor(i18nService: any, delayLoad: number) { - if (delayLoad && delayLoad > 0) { - // delay for i18n fetch - setTimeout(() => { - this.bootstrap(i18nService); - }, delayLoad); - } 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, - }, - { - type: 6, - name: 'Duo (' + i18nService.organization + ')', - description: i18nService.duoOrganizationDesc, - active: true, - displayOrder: -1, - priority: 10, - }, - ]; - } + readonly themeKey: string = ConstantsService.themeKey; } From e9228a788813b8da22c71d0b8ddeda5e08702a14 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 30 May 2018 16:08:43 -0400 Subject: [PATCH 0257/1626] cleanup totpCode --- src/angular/components/view.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index 4eb02cde78..18cdfafb93 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -155,6 +155,7 @@ export class ViewComponent implements OnDestroy { } private cleanUp() { + this.totpCode = null; this.cipher = null; this.showPassword = false; if (this.totpInterval) { From 2c43a5f06a40a51d9733652e055327eff45fe009 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 30 May 2018 22:35:43 -0400 Subject: [PATCH 0258/1626] delete old sln file --- bitwarden-jslib.sln | 38 -------------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 bitwarden-jslib.sln diff --git a/bitwarden-jslib.sln b/bitwarden-jslib.sln deleted file mode 100644 index 3e98b7dc54..0000000000 --- a/bitwarden-jslib.sln +++ /dev/null @@ -1,38 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "bitwarden-jslib", ".", "{A4DE5293-DB47-41D1-8890-7C67B83F663C}" - ProjectSection(WebsiteProperties) = preProject - TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.0" - Debug.AspNetCompiler.VirtualPath = "/localhost_4405" - Debug.AspNetCompiler.PhysicalPath = "." - Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_4405\" - Debug.AspNetCompiler.Updateable = "true" - Debug.AspNetCompiler.ForceOverwrite = "true" - Debug.AspNetCompiler.FixedNames = "false" - Debug.AspNetCompiler.Debug = "True" - Release.AspNetCompiler.VirtualPath = "/localhost_4405" - Release.AspNetCompiler.PhysicalPath = "." - Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_4405\" - Release.AspNetCompiler.Updateable = "true" - Release.AspNetCompiler.ForceOverwrite = "true" - Release.AspNetCompiler.FixedNames = "false" - Release.AspNetCompiler.Debug = "False" - VWDPort = "4405" - SlnRelativePath = "." - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A4DE5293-DB47-41D1-8890-7C67B83F663C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A4DE5293-DB47-41D1-8890-7C67B83F663C}.Debug|Any CPU.Build.0 = Debug|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal From e0190f14be9f41c5e43c9d9c3f8c2c6aedb9657b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 30 May 2018 23:19:12 -0400 Subject: [PATCH 0259/1626] catch invalid regex --- src/services/cipher.service.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 91966cb3f2..cc0ee018a8 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -238,10 +238,12 @@ export class CipherService implements CipherServiceAbstraction { } break; case UriMatchType.RegularExpression: - const regex = new RegExp(u.uri, 'i'); - if (regex.test(url)) { - return true; - } + try { + const regex = new RegExp(u.uri, 'i'); + if (regex.test(url)) { + return true; + } + } catch { } break; case UriMatchType.Never: default: From 98e2e611f8407b9c16735bafabf07eb6cd91ca53 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 31 May 2018 08:09:56 -0400 Subject: [PATCH 0260/1626] update to electron 2.x --- package-lock.json | 14 +++++++------- package.json | 2 +- src/electron/baseMenu.ts | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3894c258a4..81032db08c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1885,20 +1885,20 @@ "dev": true }, "electron": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/electron/-/electron-1.8.4.tgz", - "integrity": "sha512-2f1cx0G3riMFODXFftF5AHXy+oHfhpntZHTDN66Hxtl09gmEr42B3piNEod9MEmw72f75LX2JfeYceqq1PF8cA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/electron/-/electron-2.0.2.tgz", + "integrity": "sha512-XmkGVoHLOqmjZ2nU/0zEzMl3TZEz452Q1fTJFKjylg4pLYaq7na7V2uxzydVQNQukZGbERoA7ayjxXzTsXbtdA==", "dev": true, "requires": { - "@types/node": "8.10.9", + "@types/node": "8.10.17", "electron-download": "3.3.0", "extract-zip": "1.6.6" }, "dependencies": { "@types/node": { - "version": "8.10.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.9.tgz", - "integrity": "sha512-GUUTbeDaJSRaoLkqVQ5jwwKbDiLWFX3JrKLvC078q2P51Z9Dcb5F5UdnApSYqdMk4X0VrKod1gzeoX8bGl8DMg==", + "version": "8.10.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.17.tgz", + "integrity": "sha512-3N3FRd/rA1v5glXjb90YdYUa+sOB7WrkU2rAhKZnF4TKD86Cym9swtulGuH0p9nxo7fP5woRNa8b0oFTpCO1bg==", "dev": true } } diff --git a/package.json b/package.json index 7ff778325a..55038134e1 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@types/papaparse": "4.1.31", "@types/webcrypto": "0.0.28", "concurrently": "3.5.1", - "electron": "1.8.4", + "electron": "2.0.2", "jasmine": "^3.1.0", "jasmine-core": "^2.8.0", "jasmine-spec-reporter": "^4.2.1", diff --git a/src/electron/baseMenu.ts b/src/electron/baseMenu.ts index 4cd043866d..3e7e3d3c59 100644 --- a/src/electron/baseMenu.ts +++ b/src/electron/baseMenu.ts @@ -217,11 +217,11 @@ export class BaseMenu { this.windowMain.win.webContents.on('context-menu', (e, props) => { const selected = props.selectionText && props.selectionText.trim() !== ''; if (props.isEditable && selected) { - inputSelectionMenu.popup(this.windowMain.win); + inputSelectionMenu.popup({ window: this.windowMain.win }); } else if (props.isEditable) { - inputMenu.popup(this.windowMain.win); + inputMenu.popup({ window: this.windowMain.win }); } else if (selected) { - selectionMenu.popup(this.windowMain.win); + selectionMenu.popup({ window: this.windowMain.win }); } }); } From f618c0b5ee7bcf3cc351721611fabf57dd785211 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 31 May 2018 09:07:56 -0400 Subject: [PATCH 0261/1626] replace electron store with lowdb --- package-lock.json | 50 +++++++++++++++++- package.json | 2 + .../services/electronStorage.service.ts | 36 ------------- src/misc/nodeUtils.ts | 16 ++++++ src/misc/utils.ts | 6 +-- src/services/cipher.service.ts | 2 +- src/services/lowdbStorage.service.ts | 51 +++++++++++++++++++ 7 files changed, 121 insertions(+), 42 deletions(-) delete mode 100644 src/electron/services/electronStorage.service.ts create mode 100644 src/misc/nodeUtils.ts create mode 100644 src/services/lowdbStorage.service.ts diff --git a/package-lock.json b/package-lock.json index 81032db08c..91dce5732d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -105,6 +105,21 @@ "integrity": "sha512-loKBID6UL4QjhD2scuvv6oAPlQ/WAY7aYTDyKlKo7fIgriLS8EZExqT567cHL5CY6si51MRoX1+r3mitD3eYrA==", "dev": true }, + "@types/lodash": { + "version": "4.14.109", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.109.tgz", + "integrity": "sha512-hop8SdPUEzbcJm6aTsmuwjIYQo1tqLseKCM+s2bBqTU2gErwI4fE+aqUVOlscPSQbKHKgtMMPoC+h4AIGOJYvw==", + "dev": true + }, + "@types/lowdb": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/lowdb/-/lowdb-1.0.2.tgz", + "integrity": "sha512-0lS8jOba45tcXne01LXkw06x8uqpIKuh8LTwTOo2zmIXCVoXXmIxAemAGoLJvzNc8Q0qBG+fJT0xJMx7N0FLtA==", + "dev": true, + "requires": { + "@types/lodash": "4.14.109" + } + }, "@types/lunr": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/lunr/-/lunr-2.1.5.tgz", @@ -4220,6 +4235,11 @@ "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", "dev": true }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + }, "is-redirect": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", @@ -5413,8 +5433,7 @@ "lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", - "dev": true + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" }, "lodash.isequal": { "version": "4.5.0", @@ -5494,6 +5513,25 @@ "signal-exit": "3.0.2" } }, + "lowdb": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz", + "integrity": "sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==", + "requires": { + "graceful-fs": "4.1.11", + "is-promise": "2.1.0", + "lodash": "4.17.4", + "pify": "3.0.0", + "steno": "0.4.4" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + } + } + }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -8044,6 +8082,14 @@ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", "dev": true }, + "steno": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/steno/-/steno-0.4.4.tgz", + "integrity": "sha1-BxEFvfwobmYVwEA8J+nXtdy4Vcs=", + "requires": { + "graceful-fs": "4.1.11" + } + }, "stream-browserify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", diff --git a/package.json b/package.json index 55038134e1..fbdc1ccbed 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@types/form-data": "^2.2.1", "@types/jasmine": "^2.8.2", "@types/keytar": "^4.0.1", + "@types/lowdb": "^1.0.1", "@types/lunr": "2.1.5", "@types/node": "8.0.19", "@types/node-fetch": "^1.6.9", @@ -75,6 +76,7 @@ "electron-updater": "2.21.4", "form-data": "2.3.2", "keytar": "4.1.0", + "lowdb": "1.0.0", "lunr": "2.1.6", "node-fetch": "2.1.2", "node-forge": "0.7.1", diff --git a/src/electron/services/electronStorage.service.ts b/src/electron/services/electronStorage.service.ts deleted file mode 100644 index 953e33c9d9..0000000000 --- a/src/electron/services/electronStorage.service.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { StorageService } from '../../abstractions/storage.service'; - -// tslint:disable-next-line -const Store = require('electron-store'); - -export class ElectronStorageService implements StorageService { - private store: any; - - constructor(defaults?: any) { - const storeConfig: any = { - defaults: {} as any, - name: 'data', - }; - - if (defaults != null) { - storeConfig.defaults = Object.assign({}, storeConfig.defaults, defaults); - } - - this.store = new Store(storeConfig); - } - - get(key: string): Promise { - const val = this.store.get(key) as T; - return Promise.resolve(val != null ? val : null); - } - - save(key: string, obj: any): Promise { - this.store.set(key, obj); - return Promise.resolve(); - } - - remove(key: string): Promise { - this.store.delete(key); - return Promise.resolve(); - } -} diff --git a/src/misc/nodeUtils.ts b/src/misc/nodeUtils.ts new file mode 100644 index 0000000000..e4a79276ca --- /dev/null +++ b/src/misc/nodeUtils.ts @@ -0,0 +1,16 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +export class NodeUtils { + static mkdirpSync(targetDir: string, mode = 755, relative = false, relativeDir: string = null) { + const initialDir = path.isAbsolute(targetDir) ? path.sep : ''; + const baseDir = relative ? (relativeDir != null ? relativeDir : __dirname) : '.'; + targetDir.split(path.sep).reduce((parentDir, childDir) => { + const dir = path.resolve(baseDir, parentDir, childDir); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, mode); + } + return dir; + }, initialDir); + } +} diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 77535331df..3ca7ffefdf 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -13,9 +13,9 @@ export class Utils { } Utils.inited = true; - Utils.isNode = typeof window === 'undefined'; - Utils.isBrowser = !Utils.isNode; - Utils.global = Utils.isNode ? global : window; + Utils.isNode = typeof process !== 'undefined' && (process as any).release.name === 'node'; + Utils.isBrowser = typeof window !== 'undefined'; + Utils.global = Utils.isNode && !Utils.isBrowser ? global : window; } static fromB64ToArray(str: string): Uint8Array { diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index cc0ee018a8..45b2b93e99 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -353,7 +353,7 @@ export class CipherService implements CipherServiceAbstraction { const blob = new Blob([encData], { type: 'application/octet-stream' }); fd.append('data', blob, encFileName.encryptedString); } catch (e) { - if (Utils.isNode) { + if (Utils.isNode && !Utils.isBrowser) { fd.append('data', new Buffer(encData) as any, { filepath: encFileName.encryptedString, contentType: 'application/octet-stream', diff --git a/src/services/lowdbStorage.service.ts b/src/services/lowdbStorage.service.ts new file mode 100644 index 0000000000..8aab0eae5a --- /dev/null +++ b/src/services/lowdbStorage.service.ts @@ -0,0 +1,51 @@ +import * as fs from 'fs'; +import * as lowdb from 'lowdb'; +import * as FileSync from 'lowdb/adapters/FileSync'; +import * as path from 'path'; + +import { StorageService } from '../abstractions/storage.service'; + +import { NodeUtils } from '../misc/nodeUtils'; +import { Utils } from '../misc/utils'; + +export class LowdbStorageService implements StorageService { + private db: lowdb.LowdbSync; + private defaults: any; + + constructor(defaults?: any, dir?: string) { + this.defaults = defaults; + + let adapter: lowdb.AdapterSync; + if (Utils.isNode && dir != null) { + if (!fs.existsSync(dir)) { + NodeUtils.mkdirpSync(dir, 755); + } + const p = path.join(dir, 'data.json'); + adapter = new FileSync(p); + } else if (Utils.isBrowser && !Utils.isNode) { + // local storage adapter for web + } + this.db = lowdb(adapter); + } + + init() { + if (this.defaults != null) { + this.db.defaults(this.defaults).write(); + } + } + + get(key: string): Promise { + const val = this.db.get(key).value(); + return Promise.resolve(val as T); + } + + save(key: string, obj: any): Promise { + this.db.set(key, obj).write(); + return Promise.resolve(); + } + + remove(key: string): Promise { + this.db.unset(key).write(); + return Promise.resolve(); + } +} From 79f8370f7c37104f3f207dc36d2d0c55ac73f55c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 31 May 2018 09:13:38 -0400 Subject: [PATCH 0262/1626] fix node check --- src/misc/utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 3ca7ffefdf..bd90f0d1e9 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -13,7 +13,8 @@ export class Utils { } Utils.inited = true; - Utils.isNode = typeof process !== 'undefined' && (process as any).release.name === 'node'; + Utils.isNode = typeof process !== 'undefined' && (process as any).release != null && + (process as any).release.name === 'node'; Utils.isBrowser = typeof window !== 'undefined'; Utils.global = Utils.isNode && !Utils.isBrowser ? global : window; } From e0d5a4d8b706f3b44a07d2d77e1bf127a117179d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 31 May 2018 14:40:01 -0400 Subject: [PATCH 0263/1626] show toast util --- src/abstractions/platformUtils.service.ts | 1 + src/electron/services/electronPlatformUtils.service.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index c15da85f2c..d623250bfa 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -19,6 +19,7 @@ export abstract class PlatformUtilsService { getApplicationVersion: () => string; supportsU2f: (win: Window) => boolean; supportsDuo: () => boolean; + showToast: (type: 'error' | 'success' | 'warning' | 'info', title: string, text: string) => void; showDialog: (text: string, title?: string, confirmText?: string, cancelText?: string, type?: string) => Promise; isDev: () => boolean; diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index c6444e183e..3af58fbb41 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -127,6 +127,16 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { return true; } + showToast(type: 'error' | 'success' | 'warning' | 'info', title: string, text: string, global?: any): void { + if (global == null && Utils.isBrowser) { + global = window; + } + if (global == null || global.BitwardenToasterService == null) { + throw new Error('BitwardenToasterService not available on global.'); + } + global.BitwardenToasterService.popAsync(type, title, text); + } + showDialog(text: string, title?: string, confirmText?: string, cancelText?: string, type?: string): Promise { const buttons = [confirmText == null ? this.i18nService.t('ok') : confirmText]; From c512b9b3ce9d1cbc432e2a42232526cbb8ca25f8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 1 Jun 2018 11:51:48 -0400 Subject: [PATCH 0264/1626] return null for undefined storage key --- src/services/lowdbStorage.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/lowdbStorage.service.ts b/src/services/lowdbStorage.service.ts index 8aab0eae5a..396b6e8c93 100644 --- a/src/services/lowdbStorage.service.ts +++ b/src/services/lowdbStorage.service.ts @@ -36,6 +36,9 @@ export class LowdbStorageService implements StorageService { get(key: string): Promise { const val = this.db.get(key).value(); + if (val == null) { + return Promise.resolve(null); + } return Promise.resolve(val as T); } From 9b3fddbd3308653fdd57f0cdeb23fa661ce9704d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 1 Jun 2018 12:33:38 -0400 Subject: [PATCH 0265/1626] always read from source --- src/services/lowdbStorage.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/lowdbStorage.service.ts b/src/services/lowdbStorage.service.ts index 396b6e8c93..6a699883f2 100644 --- a/src/services/lowdbStorage.service.ts +++ b/src/services/lowdbStorage.service.ts @@ -35,7 +35,7 @@ export class LowdbStorageService implements StorageService { } get(key: string): Promise { - const val = this.db.get(key).value(); + const val = this.db.read().get(key).value(); if (val == null) { return Promise.resolve(null); } From 22c12cf5c4cc1c00792a9717fbb3d85fc53bed99 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 4 Jun 2018 12:05:43 -0400 Subject: [PATCH 0266/1626] always read before writing --- src/services/lowdbStorage.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/lowdbStorage.service.ts b/src/services/lowdbStorage.service.ts index 6a699883f2..547187d5d9 100644 --- a/src/services/lowdbStorage.service.ts +++ b/src/services/lowdbStorage.service.ts @@ -30,7 +30,7 @@ export class LowdbStorageService implements StorageService { init() { if (this.defaults != null) { - this.db.defaults(this.defaults).write(); + this.db.read().defaults(this.defaults).write(); } } @@ -43,12 +43,12 @@ export class LowdbStorageService implements StorageService { } save(key: string, obj: any): Promise { - this.db.set(key, obj).write(); + this.db.read().set(key, obj).write(); return Promise.resolve(); } remove(key: string): Promise { - this.db.unset(key).write(); + this.db.read().unset(key).write(); return Promise.resolve(); } } From 66b3dbae177a795d6b5168f7cdbba2f98f309272 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 5 Jun 2018 13:28:09 -0400 Subject: [PATCH 0267/1626] element array instead of node list --- src/angular/directives/box-row.directive.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/angular/directives/box-row.directive.ts b/src/angular/directives/box-row.directive.ts index 241a5b3f82..8269a46b64 100644 --- a/src/angular/directives/box-row.directive.ts +++ b/src/angular/directives/box-row.directive.ts @@ -10,14 +10,14 @@ import { }) export class BoxRowDirective implements OnInit { el: HTMLElement = null; - formEls: NodeListOf; + formEls: Element[]; constructor(private elRef: ElementRef) { this.el = elRef.nativeElement; } ngOnInit(): void { - this.formEls = this.el.querySelectorAll('input:not([type="hidden"]), select, textarea'); + this.formEls = Array.from(this.el.querySelectorAll('input:not([type="hidden"]), select, textarea')); this.formEls.forEach((formEl) => { formEl.addEventListener('focus', (event: Event) => { this.el.classList.add('active'); From 476d21e9f07f648784e92e6e7ec4ae37910e2449 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 5 Jun 2018 14:44:13 -0400 Subject: [PATCH 0268/1626] alloe cache on lowdb --- src/services/lowdbStorage.service.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/services/lowdbStorage.service.ts b/src/services/lowdbStorage.service.ts index 547187d5d9..90943b34ff 100644 --- a/src/services/lowdbStorage.service.ts +++ b/src/services/lowdbStorage.service.ts @@ -12,7 +12,7 @@ export class LowdbStorageService implements StorageService { private db: lowdb.LowdbSync; private defaults: any; - constructor(defaults?: any, dir?: string) { + constructor(defaults?: any, dir?: string, private allowCache = false) { this.defaults = defaults; let adapter: lowdb.AdapterSync; @@ -22,20 +22,24 @@ export class LowdbStorageService implements StorageService { } const p = path.join(dir, 'data.json'); adapter = new FileSync(p); - } else if (Utils.isBrowser && !Utils.isNode) { - // local storage adapter for web } this.db = lowdb(adapter); } init() { if (this.defaults != null) { - this.db.read().defaults(this.defaults).write(); + if (!this.allowCache) { + this.db.read(); + } + this.db.defaults(this.defaults).write(); } } get(key: string): Promise { - const val = this.db.read().get(key).value(); + if (!this.allowCache) { + this.db.read(); + } + const val = this.db.get(key).value(); if (val == null) { return Promise.resolve(null); } @@ -43,12 +47,18 @@ export class LowdbStorageService implements StorageService { } save(key: string, obj: any): Promise { - this.db.read().set(key, obj).write(); + if (!this.allowCache) { + this.db.read(); + } + this.db.set(key, obj).write(); return Promise.resolve(); } remove(key: string): Promise { - this.db.read().unset(key).write(); + if (!this.allowCache) { + this.db.read(); + } + this.db.unset(key).write(); return Promise.resolve(); } } From c5fbea2341e90826833c513e2fb0fed780bc8b50 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 5 Jun 2018 15:45:19 -0400 Subject: [PATCH 0269/1626] getCredentials conditions --- src/services/api.service.ts | 45 +++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 5d17a6c28c..5a512c4509 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -27,16 +27,20 @@ export class ApiService implements ApiServiceAbstraction { baseUrl: string; identityBaseUrl: string; deviceType: string; + isWebClient = false; + usingBaseUrl = false; constructor(private tokenService: TokenService, private platformUtilsService: PlatformUtilsService, private logoutCallback: (expired: boolean) => Promise) { this.deviceType = platformUtilsService.getDevice().toString(); + this.isWebClient = platformUtilsService.identityClientId === 'web'; } setUrls(urls: EnvironmentUrls): void { this.urlsSet = true; if (urls.base != null) { + this.usingBaseUrl = true; this.baseUrl = urls.base + '/api'; this.identityBaseUrl = urls.base + '/identity'; return; @@ -76,7 +80,7 @@ export class ApiService implements ApiServiceAbstraction { async postIdentityToken(request: TokenRequest): Promise { const response = await fetch(new Request(this.identityBaseUrl + '/connect/token', { body: this.qsStringify(request.toIdentityToken(this.platformUtilsService.identityClientId)), - credentials: 'include', + credentials: this.getCredentials(), cache: 'no-cache', headers: new Headers({ 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', @@ -119,7 +123,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/two-factor/send-email-login', { body: JSON.stringify(request), cache: 'no-cache', - credentials: 'include', + credentials: this.getCredentials(), headers: new Headers({ 'Content-Type': 'application/json; charset=utf-8', 'Device-Type': this.deviceType, @@ -139,7 +143,7 @@ export class ApiService implements ApiServiceAbstraction { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/accounts/profile', { cache: 'no-cache', - credentials: 'include', + credentials: this.getCredentials(), headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -160,7 +164,7 @@ export class ApiService implements ApiServiceAbstraction { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/accounts/revision-date', { cache: 'no-cache', - credentials: 'include', + credentials: this.getCredentials(), headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -180,7 +184,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/accounts/password-hint', { body: JSON.stringify(request), cache: 'no-cache', - credentials: 'include', + credentials: this.getCredentials(), headers: new Headers({ 'Content-Type': 'application/json; charset=utf-8', 'Device-Type': this.deviceType, @@ -198,7 +202,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/accounts/register', { body: JSON.stringify(request), cache: 'no-cache', - credentials: 'include', + credentials: this.getCredentials(), headers: new Headers({ 'Content-Type': 'application/json; charset=utf-8', 'Device-Type': this.deviceType, @@ -219,7 +223,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/folders', { body: JSON.stringify(request), cache: 'no-cache', - credentials: 'include', + credentials: this.getCredentials(), headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -243,7 +247,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/folders/' + id, { body: JSON.stringify(request), cache: 'no-cache', - credentials: 'include', + credentials: this.getCredentials(), headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -266,7 +270,7 @@ export class ApiService implements ApiServiceAbstraction { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/folders/' + id, { cache: 'no-cache', - credentials: 'include', + credentials: this.getCredentials(), headers: new Headers({ 'Authorization': authHeader, 'Device-Type': this.deviceType, @@ -287,7 +291,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/ciphers', { body: JSON.stringify(request), cache: 'no-cache', - credentials: 'include', + credentials: this.getCredentials(), headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -311,7 +315,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { body: JSON.stringify(request), cache: 'no-cache', - credentials: 'include', + credentials: this.getCredentials(), headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -334,7 +338,7 @@ export class ApiService implements ApiServiceAbstraction { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { cache: 'no-cache', - credentials: 'include', + credentials: this.getCredentials(), headers: new Headers({ 'Authorization': authHeader, 'Device-Type': this.deviceType, @@ -355,7 +359,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment', { body: data, cache: 'no-cache', - credentials: 'include', + credentials: this.getCredentials(), headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -377,7 +381,7 @@ export class ApiService implements ApiServiceAbstraction { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment/' + attachmentId, { cache: 'no-cache', - credentials: 'include', + credentials: this.getCredentials(), headers: new Headers({ 'Authorization': authHeader, 'Device-Type': this.deviceType, @@ -397,7 +401,7 @@ export class ApiService implements ApiServiceAbstraction { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/sync', { cache: 'no-cache', - credentials: 'include', + credentials: this.getCredentials(), headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -419,7 +423,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await fetch(new Request(this.baseUrl + '/organizations/' + organizationId + '/import', { body: JSON.stringify(request), cache: 'no-cache', - credentials: 'include', + credentials: this.getCredentials(), headers: new Headers({ 'Accept': 'application/json', 'Authorization': authHeader, @@ -476,7 +480,7 @@ export class ApiService implements ApiServiceAbstraction { refresh_token: refreshToken, }), cache: 'no-cache', - credentials: 'include', + credentials: this.getCredentials(), headers: new Headers({ 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 'Accept': 'application/json', @@ -501,4 +505,11 @@ export class ApiService implements ApiServiceAbstraction { return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); }).join('&'); } + + private getCredentials(): RequestCredentials { + if (!this.isWebClient || this.usingBaseUrl) { + return 'include'; + } + return undefined; + } } From a7a58ae8f306fad1f72110754ed9cccf19fee482 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 6 Jun 2018 14:29:59 -0400 Subject: [PATCH 0270/1626] true false value directive --- .../directives/true-false-value.directive.ts | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/angular/directives/true-false-value.directive.ts diff --git a/src/angular/directives/true-false-value.directive.ts b/src/angular/directives/true-false-value.directive.ts new file mode 100644 index 0000000000..12729b90f1 --- /dev/null +++ b/src/angular/directives/true-false-value.directive.ts @@ -0,0 +1,54 @@ +import { + Directive, + ElementRef, + forwardRef, + HostListener, + Input, + Renderer2, +} from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + NgControl, +} from '@angular/forms'; + +// ref: https://juristr.com/blog/2018/02/ng-true-value-directive/ +@Directive({ + selector: 'input[type=checkbox][appTrueFalseValue]', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TrueFalseValueDirective), + multi: true, + }, + ], +}) +export class TrueFalseValueDirective implements ControlValueAccessor { + @Input() trueValue = true; + @Input() falseValue = false; + + constructor(private elementRef: ElementRef, private renderer: Renderer2) { } + + @HostListener('change', ['$event']) + onHostChange(ev: any) { + this.propagateChange(ev.target.checked ? this.trueValue : this.falseValue); + } + + writeValue(obj: any): void { + if (obj === this.trueValue) { + this.renderer.setProperty(this.elementRef.nativeElement, 'checked', true); + } else { + this.renderer.setProperty(this.elementRef.nativeElement, 'checked', false); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { /* nothing */ } + + setDisabledState?(isDisabled: boolean): void { /* nothing */ } + + private propagateChange = (_: any) => { /* nothing */ }; +} From c59bca05bb37395dbbc05340f795f1ad6fd906c5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 6 Jun 2018 17:26:58 -0400 Subject: [PATCH 0271/1626] fixes on modal --- src/angular/components/modal.component.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/angular/components/modal.component.ts b/src/angular/components/modal.component.ts index a56d84fd61..d63c78af7f 100644 --- a/src/angular/components/modal.component.ts +++ b/src/angular/components/modal.component.ts @@ -22,7 +22,7 @@ export class ModalComponent implements OnDestroy { parentContainer: ViewContainerRef = null; fade: boolean = true; - constructor(private componentFactoryResolver: ComponentFactoryResolver) { } + constructor(protected componentFactoryResolver: ComponentFactoryResolver) { } ngOnDestroy() { document.body.classList.remove('modal-open'); @@ -46,7 +46,8 @@ export class ModalComponent implements OnDestroy { e.stopPropagation(); }); - for (const closeElement of document.querySelectorAll('.modal, .modal *[data-dismiss="modal"]')) { + const modals = Array.from(document.querySelectorAll('.modal, .modal *[data-dismiss="modal"]')); + for (const closeElement of modals) { closeElement.addEventListener('click', (event) => { this.close(); }); From 8211e19db02a350606a970b828dbb16bc83788bb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 7 Jun 2018 17:11:17 -0400 Subject: [PATCH 0272/1626] add web device type, dont check password twice --- src/angular/components/add-edit.component.ts | 5 +++++ src/enums/deviceType.ts | 1 + src/misc/analytics.ts | 1 + 3 files changed, 7 insertions(+) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index dced8314a1..5c69e331dc 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -278,6 +278,10 @@ export class AddEditComponent { } async checkPassword() { + if (this.checkPasswordPromise != null) { + return; + } + if (this.cipher.login == null || this.cipher.login.password == null || this.cipher.login.password === '') { return; } @@ -285,6 +289,7 @@ export class AddEditComponent { this.analytics.eventTrack.next({ action: 'Check Password' }); this.checkPasswordPromise = this.auditService.passwordLeaked(this.cipher.login.password); const matches = await this.checkPasswordPromise; + this.checkPasswordPromise = null; if (matches > 0) { this.toasterService.popAsync('warning', null, this.i18nService.t('passwordExposed', matches.toString())); diff --git a/src/enums/deviceType.ts b/src/enums/deviceType.ts index c41ff92933..e3e2fecb92 100644 --- a/src/enums/deviceType.ts +++ b/src/enums/deviceType.ts @@ -8,4 +8,5 @@ export enum DeviceType { Linux = 8, Vivaldi = 19, Safari = 20, + Web = 21, } diff --git a/src/misc/analytics.ts b/src/misc/analytics.ts index 3ecf1c5b16..7f9f565932 100644 --- a/src/misc/analytics.ts +++ b/src/misc/analytics.ts @@ -18,6 +18,7 @@ export const AnalyticsIds = { [DeviceType.Windows]: 'UA-81915606-17', [DeviceType.Linux]: 'UA-81915606-19', [DeviceType.MacOs]: 'UA-81915606-18', + [DeviceType.Web]: 'UA-81915606-3', }; export class Analytics { From f40451ecc5b891139347c51d804ab314502d98e7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 7 Jun 2018 23:36:39 -0400 Subject: [PATCH 0273/1626] ie fixes --- src/abstractions/platformUtils.service.ts | 1 + src/angular/components/icon.component.ts | 5 +++-- src/electron/services/electronPlatformUtils.service.ts | 4 ++++ src/misc/utils.ts | 10 +++++++++- src/services/webCryptoFunction.service.ts | 10 ++++++---- 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index d623250bfa..7025010293 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -10,6 +10,7 @@ export abstract class PlatformUtilsService { isOpera: () => boolean; isVivaldi: () => boolean; isSafari: () => boolean; + isIE: () => boolean; isMacAppStore: () => boolean; analyticsId: () => string; getDomain: (uriString: string) => string; diff --git a/src/angular/components/icon.component.ts b/src/angular/components/icon.component.ts index 65b9339e2f..cac8d381a2 100644 --- a/src/angular/components/icon.component.ts +++ b/src/angular/components/icon.component.ts @@ -11,6 +11,8 @@ import { StateService } from '../../abstractions/state.service'; import { ConstantsService } from '../../services/constants.service'; +import { Utils } from '../../misc/utils'; + @Component({ selector: 'app-vault-icon', templateUrl: 'icon.component.html', @@ -77,8 +79,7 @@ export class IconComponent implements OnChanges { if (this.imageEnabled && isWebsite) { try { - const url = new URL(hostnameUri); - this.image = this.iconsUrl + '/' + url.hostname + '/icon.png'; + this.image = this.iconsUrl + '/' + Utils.getHostname(hostnameUri) + '/icon.png'; this.fallbackImage = 'images/fa-globe.png'; } catch (e) { } } diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index 3af58fbb41..057f61da53 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -74,6 +74,10 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { return false; } + isIE(): boolean { + return false; + } + isMacAppStore(): boolean { return isMacAppStore(); } diff --git a/src/misc/utils.ts b/src/misc/utils.ts index bd90f0d1e9..637330ea34 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -165,7 +165,15 @@ export class Utils { if (uriString.startsWith('http://') || uriString.startsWith('https://')) { try { - return nodeURL != null ? new nodeURL(uriString) : new URL(uriString); + if (nodeURL != null) { + return new nodeURL(uriString); + } else if (typeof URL === 'function') { + return new URL(uriString); + } else if (window != null) { + const anchor = window.document.createElement('a'); + anchor.href = uriString; + return anchor as any; + } } catch (e) { } } diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index 3938b48b9c..f055df3206 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -12,16 +12,18 @@ export class WebCryptoFunctionService implements CryptoFunctionService { private crypto: Crypto; private subtle: SubtleCrypto; private isEdge: boolean; + private isIE: boolean; constructor(private win: Window, private platformUtilsService: PlatformUtilsService) { - this.crypto = win.crypto; - this.subtle = win.crypto.subtle; + this.crypto = typeof win.crypto !== 'undefined' ? win.crypto : null; + this.subtle = (!!this.crypto && typeof win.crypto.subtle !== 'undefined') ? win.crypto.subtle : null; this.isEdge = platformUtilsService.isEdge(); + this.isIE = platformUtilsService.isIE(); } async pbkdf2(password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', iterations: number): Promise { - if (this.isEdge) { + if (this.isEdge || this.isIE) { const forgeLen = algorithm === 'sha256' ? 32 : 64; const passwordBytes = this.toByteString(password); const saltBytes = this.toByteString(salt); @@ -46,7 +48,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { } async hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { - if (this.isEdge && algorithm === 'sha1') { + if ((this.isEdge || this.isIE) && algorithm === 'sha1') { const md = forge.md.sha1.create(); const valueBytes = this.toByteString(value); md.update(valueBytes, 'raw'); From bfaebb6b92924046dc83b49fc2d5c7fbcc4a08c4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 7 Jun 2018 23:58:19 -0400 Subject: [PATCH 0274/1626] fix test --- spec/web/services/webCryptoFunction.service.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index 0d2b21d0eb..ca517eaec2 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -351,6 +351,8 @@ function testHmacFast(algorithm: 'sha1' | 'sha256' | 'sha512', mac: string) { function getWebCryptoFunctionService() { const platformUtilsMock = TypeMoq.Mock.ofType(PlatformUtilsServiceMock); platformUtilsMock.setup((x) => x.isEdge()).returns(() => navigator.userAgent.indexOf(' Edge/') !== -1); + platformUtilsMock.setup((x) => x.isIE()).returns(() => navigator.userAgent.indexOf(' Edge/') === -1 && + navigator.userAgent.indexOf(' Trident/') !== -1); return new WebCryptoFunctionService(window, platformUtilsMock.object); } @@ -364,4 +366,5 @@ function makeStaticByteArray(length: number) { class PlatformUtilsServiceMock extends PlatformUtilsService { isEdge = () => false; + isIE = () => false; } From 8a1bc30fc996fa787c981abdc6fc78e961ced4b5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 8 Jun 2018 10:11:28 -0400 Subject: [PATCH 0275/1626] update jslib to support modules --- src/misc/duo.js | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/misc/duo.js b/src/misc/duo.js index 8b712dcf25..bb0f58a787 100644 --- a/src/misc/duo.js +++ b/src/misc/duo.js @@ -3,16 +3,28 @@ * Copyright 2017, Duo Security */ -var Duo; (function (root, factory) { - // Browser globals (root is window) - var d = factory(); - // If the Javascript was loaded via a script tag, attempt to autoload - // the frame. - d._onReady(d.init); - // Attach Duo to the `window` object - root.Duo = Duo = d; -}(window, function () { + /*eslint-disable */ + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], factory); + /*eslint-enable */ + } else if (typeof module === 'object' && module.exports) { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); + } else { + // Browser globals (root is window) + var Duo = factory(); + // If the Javascript was loaded via a script tag, attempt to autoload + // the frame. + Duo._onReady(Duo.init); + + // Attach Duo to the `window` object + root.Duo = Duo; + } +}(this, function() { var DUO_MESSAGE_FORMAT = /^(?:AUTH|ENROLL)+\|[A-Za-z0-9\+\/=]+\|[A-Za-z0-9\+\/=]+$/; var DUO_ERROR_FORMAT = /^ERR\|[\w\s\.\(\)]+$/; var DUO_OPEN_WINDOW_FORMAT = /^DUO_OPEN_WINDOW\|/; @@ -311,10 +323,10 @@ var Duo; } for (var i = 0; i < VALID_OPEN_WINDOW_DOMAINS.length; i++) { - if (parser.hostname.endsWith("." + VALID_OPEN_WINDOW_DOMAINS[i]) || - parser.hostname === VALID_OPEN_WINDOW_DOMAINS[i]) { - return true; - } + if (parser.hostname.endsWith("." + VALID_OPEN_WINDOW_DOMAINS[i]) || + parser.hostname === VALID_OPEN_WINDOW_DOMAINS[i]) { + return true; + } } return false; } From 4c083eeb92d599bd8b85fb88ec5d435079b79395 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 8 Jun 2018 12:14:02 -0400 Subject: [PATCH 0276/1626] download attachments function from component --- .../components/attachments.component.ts | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/angular/components/attachments.component.ts b/src/angular/components/attachments.component.ts index 6a02a7c259..0d13a91305 100644 --- a/src/angular/components/attachments.component.ts +++ b/src/angular/components/attachments.component.ts @@ -32,7 +32,7 @@ export class AttachmentsComponent implements OnInit { constructor(protected cipherService: CipherService, protected analytics: Angulartics2, protected toasterService: ToasterService, protected i18nService: I18nService, protected cryptoService: CryptoService, protected tokenService: TokenService, - protected platformUtilsService: PlatformUtilsService) { } + protected platformUtilsService: PlatformUtilsService, protected win: Window) { } async ngOnInit() { this.cipherDomain = await this.cipherService.get(this.cipherId); @@ -122,4 +122,36 @@ export class AttachmentsComponent implements OnInit { this.deletePromises[attachment.id] = null; } + + async download(attachment: AttachmentView) { + const a = (attachment as any); + if (a.downloading) { + return; + } + + if (!this.canAccessAttachments) { + this.toasterService.popAsync('error', this.i18nService.t('premiumRequired'), + this.i18nService.t('premiumRequiredDesc')); + return; + } + + a.downloading = true; + const response = await fetch(new Request(attachment.url, { cache: 'no-cache' })); + if (response.status !== 200) { + this.toasterService.popAsync('error', null, this.i18nService.t('errorOccurred')); + a.downloading = false; + return; + } + + try { + const buf = await response.arrayBuffer(); + const key = await this.cryptoService.getOrgKey(this.cipher.organizationId); + const decBuf = await this.cryptoService.decryptFromBytes(buf, key); + this.platformUtilsService.saveFile(this.win, decBuf, null, attachment.fileName); + } catch (e) { + this.toasterService.popAsync('error', null, this.i18nService.t('errorOccurred')); + } + + a.downloading = false; + } } From fe3a8785422a8866dbcb91d31db6d3fd2aa2daaa Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 8 Jun 2018 16:07:21 -0400 Subject: [PATCH 0277/1626] update dummy module --- src/angular/dummy.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/angular/dummy.module.ts b/src/angular/dummy.module.ts index bdd63830b2..35a1e38142 100644 --- a/src/angular/dummy.module.ts +++ b/src/angular/dummy.module.ts @@ -1,11 +1,13 @@ import { NgModule } from '@angular/core'; import { InputVerbatimDirective } from './directives/input-verbatim.directive'; +import { TrueFalseValueDirective } from './directives/true-false-value.directive'; @NgModule({ imports: [], declarations: [ InputVerbatimDirective, + TrueFalseValueDirective, ], }) export class DummyModule { From 7b05416d556c6041e638ec5fcb58a060062cda84 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 9 Jun 2018 13:29:07 -0400 Subject: [PATCH 0278/1626] attachment events --- src/angular/components/attachments.component.ts | 4 ++++ src/angular/dummy.module.ts | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/angular/components/attachments.component.ts b/src/angular/components/attachments.component.ts index 0d13a91305..fff40c4b0b 100644 --- a/src/angular/components/attachments.component.ts +++ b/src/angular/components/attachments.component.ts @@ -21,6 +21,8 @@ import { CipherView } from '../../models/view/cipherView'; export class AttachmentsComponent implements OnInit { @Input() cipherId: string; + @Output() onUploadedAttachment = new EventEmitter(); + @Output() onDeletedAttachment = new EventEmitter(); cipher: CipherView; cipherDomain: Cipher; @@ -87,6 +89,7 @@ export class AttachmentsComponent implements OnInit { this.cipher = await this.cipherDomain.decrypt(); this.analytics.eventTrack.next({ action: 'Added Attachment' }); this.toasterService.popAsync('success', null, this.i18nService.t('attachmentSaved')); + this.onUploadedAttachment.emit(); } catch { } // reset file input @@ -121,6 +124,7 @@ export class AttachmentsComponent implements OnInit { } catch { } this.deletePromises[attachment.id] = null; + this.onDeletedAttachment.emit(); } async download(attachment: AttachmentView) { diff --git a/src/angular/dummy.module.ts b/src/angular/dummy.module.ts index 35a1e38142..1be7f1fed9 100644 --- a/src/angular/dummy.module.ts +++ b/src/angular/dummy.module.ts @@ -7,7 +7,6 @@ import { TrueFalseValueDirective } from './directives/true-false-value.directive imports: [], declarations: [ InputVerbatimDirective, - TrueFalseValueDirective, ], }) export class DummyModule { From cd3c2ddff1abd66c6318d981a389685dc82cfc0e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 9 Jun 2018 14:50:18 -0400 Subject: [PATCH 0279/1626] allow static lock timeout definition --- src/abstractions/platformUtils.service.ts | 1 + src/electron/services/electronPlatformUtils.service.ts | 4 ++++ src/services/lock.service.ts | 5 ++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index 7025010293..98eb7e349a 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -15,6 +15,7 @@ export abstract class PlatformUtilsService { analyticsId: () => string; getDomain: (uriString: string) => string; isViewOpen: () => boolean; + lockTimeout: () => number; launchUri: (uri: string, options?: any) => void; saveFile: (win: Window, blobData: any, blobOptions: any, fileName: string) => void; getApplicationVersion: () => string; diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index 057f61da53..04d8669b2f 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -103,6 +103,10 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { return false; } + lockTimeout(): number { + return null; + } + launchUri(uri: string, options?: any): void { shell.openExternal(uri); } diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts index dbc26f8140..50e46c1420 100644 --- a/src/services/lock.service.ts +++ b/src/services/lock.service.ts @@ -42,7 +42,10 @@ export class LockService implements LockServiceAbstraction { return; } - const lockOption = await this.storageService.get(ConstantsService.lockOptionKey); + let lockOption = this.platformUtilsService.lockTimeout(); + if (lockOption == null) { + lockOption = await this.storageService.get(ConstantsService.lockOptionKey); + } if (lockOption == null || lockOption < 0) { return; } From d875b9aeb0013ff0802c1a9a1e6a8eb2fb8ef156 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 11 Jun 2018 08:54:22 -0400 Subject: [PATCH 0280/1626] remove dummy module --- src/angular/dummy.module.ts | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 src/angular/dummy.module.ts diff --git a/src/angular/dummy.module.ts b/src/angular/dummy.module.ts deleted file mode 100644 index 1be7f1fed9..0000000000 --- a/src/angular/dummy.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NgModule } from '@angular/core'; - -import { InputVerbatimDirective } from './directives/input-verbatim.directive'; -import { TrueFalseValueDirective } from './directives/true-false-value.directive'; - -@NgModule({ - imports: [], - declarations: [ - InputVerbatimDirective, - ], -}) -export class DummyModule { -} From 4bd9a9fc1121cd2f9bbe3b414c6239040b4fe880 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 11 Jun 2018 13:32:53 -0400 Subject: [PATCH 0281/1626] load DuoWebSDK as a module --- src/angular/components/two-factor.component.ts | 6 ++++-- tsconfig.json | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 9c2e4ca354..3ebaa32126 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -22,6 +22,7 @@ import { SyncService } from '../../abstractions/sync.service'; import { TwoFactorProviders } from '../../services/auth.service'; +import * as DuoWebSDK from '../../misc/duo'; import { U2f } from '../../misc/u2f'; export class TwoFactorComponent implements OnInit, OnDestroy { @@ -120,7 +121,8 @@ export class TwoFactorComponent implements OnInit, OnDestroy { } setTimeout(() => { - (this.win as any).Duo.init({ + DuoWebSDK.init({ + iframe: undefined, host: params.Host, sig_request: params.Signature, submit_callback: async (f: HTMLFormElement) => { @@ -131,7 +133,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { } }, }); - }); + }, 0); break; case TwoFactorProviderType.Email: this.twoFactorEmail = params.Email; diff --git a/tsconfig.json b/tsconfig.json index a745228386..490d58e983 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,8 @@ "moduleResolution": "node", "target": "ES6", "module": "commonjs", + "lib": ["es5", "es6", "dom"], + "allowJs": true, "sourceMap": true, "declaration": true, "allowSyntheticDefaultImports": true, From 5db55496c2499c9ba21e1caa7bb04d393939c1a8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 11 Jun 2018 14:32:35 -0400 Subject: [PATCH 0282/1626] add support for readonly flag on collections --- src/models/data/collectionData.ts | 2 ++ src/models/domain/collection.ts | 4 +++- src/models/response/collectionResponse.ts | 2 ++ src/models/view/collectionView.ts | 2 ++ 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/models/data/collectionData.ts b/src/models/data/collectionData.ts index 3b05b9af22..ff226d315e 100644 --- a/src/models/data/collectionData.ts +++ b/src/models/data/collectionData.ts @@ -4,10 +4,12 @@ export class CollectionData { id: string; organizationId: string; name: string; + readOnly: boolean; constructor(response: CollectionResponse) { this.id = response.id; this.organizationId = response.organizationId; this.name = response.name; + this.readOnly = response.readOnly; } } diff --git a/src/models/domain/collection.ts b/src/models/domain/collection.ts index 45fd54cd8c..42886902d2 100644 --- a/src/models/domain/collection.ts +++ b/src/models/domain/collection.ts @@ -9,6 +9,7 @@ export class Collection extends Domain { id: string; organizationId: string; name: CipherString; + readOnly: boolean; constructor(obj?: CollectionData, alreadyEncrypted: boolean = false) { super(); @@ -20,7 +21,8 @@ export class Collection extends Domain { id: null, organizationId: null, name: null, - }, alreadyEncrypted, ['id', 'organizationId']); + readOnly: null, + }, alreadyEncrypted, ['id', 'organizationId', 'readOnly']); } decrypt(): Promise { diff --git a/src/models/response/collectionResponse.ts b/src/models/response/collectionResponse.ts index 77b80f57ca..525bf60e34 100644 --- a/src/models/response/collectionResponse.ts +++ b/src/models/response/collectionResponse.ts @@ -2,10 +2,12 @@ export class CollectionResponse { id: string; organizationId: string; name: string; + readOnly: boolean; constructor(response: any) { this.id = response.Id; this.organizationId = response.OrganizationId; this.name = response.Name; + this.readOnly = response.ReadOnly || false; } } diff --git a/src/models/view/collectionView.ts b/src/models/view/collectionView.ts index ab184a2c35..1c625182d4 100644 --- a/src/models/view/collectionView.ts +++ b/src/models/view/collectionView.ts @@ -6,6 +6,7 @@ export class CollectionView implements View { id: string; organizationId: string; name: string; + readOnly: boolean; constructor(c?: Collection) { if (!c) { @@ -14,5 +15,6 @@ export class CollectionView implements View { this.id = c.id; this.organizationId = c.organizationId; + this.readOnly = c.readOnly; } } From b3f71ed8e406008987c29b29b0366a7c0f246765 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 12 Jun 2018 11:45:02 -0400 Subject: [PATCH 0283/1626] api support for sharing --- src/abstractions/api.service.ts | 5 ++ src/abstractions/cipher.service.ts | 4 + src/models/data/attachmentData.ts | 5 +- src/models/data/cardData.ts | 6 +- src/models/data/cipherData.ts | 8 +- src/models/data/fieldData.ts | 5 +- src/models/data/identityData.ts | 6 +- src/models/data/loginData.ts | 6 +- src/models/data/loginUriData.ts | 5 +- src/models/data/secureNoteData.ts | 6 +- src/models/domain/attachment.ts | 11 +++ src/models/domain/card.ts | 13 ++++ src/models/domain/cipher.ts | 56 +++++++++++++- src/models/domain/domain.ts | 14 ++++ src/models/domain/field.ts | 10 +++ src/models/domain/identity.ts | 25 +++++++ src/models/domain/login.ts | 18 +++++ src/models/domain/loginUri.ts | 8 ++ src/models/domain/secureNote.ts | 6 ++ src/models/request/cipherBulkDeleteRequest.ts | 7 ++ src/models/request/cipherBulkMoveRequest.ts | 9 +++ .../request/cipherCollectionsRequest.ts | 7 ++ src/models/request/cipherRequest.ts | 8 ++ src/models/request/cipherShareRequest.ts | 13 ++++ src/models/response/cipherResponse.ts | 4 +- src/services/api.service.ts | 44 +++++++++++ src/services/cipher.service.ts | 74 +++++++++++++++++++ 27 files changed, 371 insertions(+), 12 deletions(-) create mode 100644 src/models/request/cipherBulkDeleteRequest.ts create mode 100644 src/models/request/cipherBulkMoveRequest.ts create mode 100644 src/models/request/cipherCollectionsRequest.ts create mode 100644 src/models/request/cipherShareRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index eba6d8582d..a1410c0fe3 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -1,6 +1,7 @@ import { EnvironmentUrls } from '../models/domain/environmentUrls'; import { CipherRequest } from '../models/request/cipherRequest'; +import { CipherShareRequest } from '../models/request/cipherShareRequest'; import { FolderRequest } from '../models/request/folderRequest'; import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; import { PasswordHintRequest } from '../models/request/passwordHintRequest'; @@ -15,6 +16,8 @@ import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorR import { ProfileResponse } from '../models/response/profileResponse'; import { SyncResponse } from '../models/response/syncResponse'; +import { AttachmentView } from '../models/view/attachmentView'; + export abstract class ApiService { urlsSet: boolean; baseUrl: string; @@ -34,8 +37,10 @@ export abstract class ApiService { deleteFolder: (id: string) => Promise; postCipher: (request: CipherRequest) => Promise; putCipher: (id: string, request: CipherRequest) => Promise; + shareCipher: (id: string, request: CipherShareRequest) => Promise; deleteCipher: (id: string) => Promise; postCipherAttachment: (id: string, data: FormData) => Promise; + shareCipherAttachment: (id: string, attachmentId: string, data: FormData, organizationId: string) => Promise; deleteCipherAttachment: (id: string, attachmentId: string) => Promise; getSync: () => Promise; postImportDirectory: (organizationId: string, request: ImportDirectoryRequest) => Promise; diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index 73ee81047c..d621443f1b 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -6,6 +6,7 @@ import { Cipher } from '../models/domain/cipher'; import { Field } from '../models/domain/field'; import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; +import { AttachmentView } from '../models/view/attachmentView'; import { CipherView } from '../models/view/cipherView'; import { FieldView } from '../models/view/fieldView'; @@ -25,6 +26,9 @@ export abstract class CipherService { updateLastUsedDate: (id: string) => Promise; saveNeverDomain: (domain: string) => Promise; saveWithServer: (cipher: Cipher) => Promise; + shareWithServer: (cipher: Cipher) => Promise; + shareAttachmentWithServer: (attachmentView: AttachmentView, cipherId: string, + organizationId: string) => Promise; saveAttachmentWithServer: (cipher: Cipher, unencryptedFile: any) => Promise; saveAttachmentRawWithServer: (cipher: Cipher, filename: string, data: ArrayBuffer) => Promise; upsert: (cipher: CipherData | CipherData[]) => Promise; diff --git a/src/models/data/attachmentData.ts b/src/models/data/attachmentData.ts index 85aa452e1e..63b822174c 100644 --- a/src/models/data/attachmentData.ts +++ b/src/models/data/attachmentData.ts @@ -7,7 +7,10 @@ export class AttachmentData { size: number; sizeName: string; - constructor(response: AttachmentResponse) { + constructor(response?: AttachmentResponse) { + if (response == null) { + return; + } this.id = response.id; this.url = response.url; this.fileName = response.fileName; diff --git a/src/models/data/cardData.ts b/src/models/data/cardData.ts index 1a4edea4ea..d87c723158 100644 --- a/src/models/data/cardData.ts +++ b/src/models/data/cardData.ts @@ -8,7 +8,11 @@ export class CardData { expYear: string; code: string; - constructor(data: CardApi) { + constructor(data?: CardApi) { + if (data == null) { + return; + } + this.cardholderName = data.cardholderName; this.brand = data.brand; this.number = data.number; diff --git a/src/models/data/cipherData.ts b/src/models/data/cipherData.ts index 1a76ed5c79..59a52fe53d 100644 --- a/src/models/data/cipherData.ts +++ b/src/models/data/cipherData.ts @@ -17,7 +17,7 @@ export class CipherData { edit: boolean; organizationUseTotp: boolean; favorite: boolean; - revisionDate: string; + revisionDate: Date; type: CipherType; sizeName: string; name: string; @@ -30,7 +30,11 @@ export class CipherData { attachments?: AttachmentData[]; collectionIds?: string[]; - constructor(response: CipherResponse, userId: string, collectionIds?: string[]) { + constructor(response?: CipherResponse, userId?: string, collectionIds?: string[]) { + if (response == null) { + return; + } + this.id = response.id; this.organizationId = response.organizationId; this.folderId = response.folderId; diff --git a/src/models/data/fieldData.ts b/src/models/data/fieldData.ts index 0632d7349d..d28d285b5c 100644 --- a/src/models/data/fieldData.ts +++ b/src/models/data/fieldData.ts @@ -7,7 +7,10 @@ export class FieldData { name: string; value: string; - constructor(response: FieldApi) { + constructor(response?: FieldApi) { + if (response == null) { + return; + } this.type = response.type; this.name = response.name; this.value = response.value; diff --git a/src/models/data/identityData.ts b/src/models/data/identityData.ts index d1ac84a7b2..50fff95785 100644 --- a/src/models/data/identityData.ts +++ b/src/models/data/identityData.ts @@ -20,7 +20,11 @@ export class IdentityData { passportNumber: string; licenseNumber: string; - constructor(data: IdentityApi) { + constructor(data?: IdentityApi) { + if (data == null) { + return; + } + this.title = data.title; this.firstName = data.firstName; this.middleName = data.middleName; diff --git a/src/models/data/loginData.ts b/src/models/data/loginData.ts index f81e8e0897..a7d504e052 100644 --- a/src/models/data/loginData.ts +++ b/src/models/data/loginData.ts @@ -9,7 +9,11 @@ export class LoginData { password: string; totp: string; - constructor(data: LoginApi) { + constructor(data?: LoginApi) { + if (data == null) { + return; + } + this.username = data.username; this.password = data.password; this.totp = data.totp; diff --git a/src/models/data/loginUriData.ts b/src/models/data/loginUriData.ts index 71ad8079c1..e696355c74 100644 --- a/src/models/data/loginUriData.ts +++ b/src/models/data/loginUriData.ts @@ -6,7 +6,10 @@ export class LoginUriData { uri: string; match: UriMatchType = null; - constructor(data: LoginUriApi) { + constructor(data?: LoginUriApi) { + if (data == null) { + return; + } this.uri = data.uri; this.match = data.match; } diff --git a/src/models/data/secureNoteData.ts b/src/models/data/secureNoteData.ts index a508c1b31d..4a24f2cf86 100644 --- a/src/models/data/secureNoteData.ts +++ b/src/models/data/secureNoteData.ts @@ -5,7 +5,11 @@ import { SecureNoteApi } from '../api/secureNoteApi'; export class SecureNoteData { type: SecureNoteType; - constructor(data: SecureNoteApi) { + constructor(data?: SecureNoteApi) { + if (data == null) { + return; + } + this.type = data.type; } } diff --git a/src/models/domain/attachment.ts b/src/models/domain/attachment.ts index 7861066e0e..be345ce72f 100644 --- a/src/models/domain/attachment.ts +++ b/src/models/domain/attachment.ts @@ -32,4 +32,15 @@ export class Attachment extends Domain { fileName: null, }, orgId); } + + toAttachmentData(): AttachmentData { + const a = new AttachmentData(); + this.buildDataModel(this, a, { + id: null, + url: null, + sizeName: null, + fileName: null, + }, ['id', 'url', 'sizeName']); + return a; + } } diff --git a/src/models/domain/card.ts b/src/models/domain/card.ts index 19a2ad9e50..a9a4791274 100644 --- a/src/models/domain/card.ts +++ b/src/models/domain/card.ts @@ -39,4 +39,17 @@ export class Card extends Domain { code: null, }, orgId); } + + toCardData(): CardData { + const c = new CardData(); + this.buildDataModel(this, c, { + cardholderName: null, + brand: null, + number: null, + expMonth: null, + expYear: null, + code: null, + }); + return c; + } } diff --git a/src/models/domain/cipher.ts b/src/models/domain/cipher.ts index 7f9c18d1f0..ad5cd75587 100644 --- a/src/models/domain/cipher.ts +++ b/src/models/domain/cipher.ts @@ -23,6 +23,7 @@ export class Cipher extends Domain { favorite: boolean; organizationUseTotp: boolean; edit: boolean; + revisionDate: Date; localData: any; login: Login; identity: Identity; @@ -40,16 +41,18 @@ export class Cipher extends Domain { this.buildDomainModel(this, obj, { id: null, + userId: null, organizationId: null, folderId: null, name: null, notes: null, - }, alreadyEncrypted, ['id', 'organizationId', 'folderId']); + }, alreadyEncrypted, ['id', 'userId', 'organizationId', 'folderId']); this.type = obj.type; this.favorite = obj.favorite; this.organizationUseTotp = obj.organizationUseTotp; this.edit = obj.edit; + this.revisionDate = obj.revisionDate; this.collectionIds = obj.collectionIds; this.localData = localData; @@ -142,4 +145,55 @@ export class Cipher extends Domain { return model; } + + toCipherData(userId: string): CipherData { + const c = new CipherData(); + c.id = this.id; + c.organizationId = this.organizationId; + c.folderId = this.folderId; + c.userId = this.organizationId != null ? userId : null; + c.edit = this.edit; + c.organizationUseTotp = this.organizationUseTotp; + c.favorite = this.favorite; + c.revisionDate = this.revisionDate; + c.type = this.type; + c.collectionIds = this.collectionIds; + + this.buildDataModel(this, c, { + name: null, + notes: null, + }); + + switch (c.type) { + case CipherType.Login: + c.login = this.login.toLoginData(); + break; + case CipherType.SecureNote: + c.secureNote = this.secureNote.toSecureNoteData(); + break; + case CipherType.Card: + c.card = this.card.toCardData(); + break; + case CipherType.Identity: + c.identity = this.identity.toIdentityData(); + break; + default: + break; + } + + if (this.fields != null) { + c.fields = []; + this.fields.forEach((field) => { + c.fields.push(field.toFieldData()); + }); + } + + if (this.attachments != null) { + c.attachments = []; + this.attachments.forEach((attachment) => { + c.attachments.push(attachment.toAttachmentData()); + }); + } + return c; + } } diff --git a/src/models/domain/domain.ts b/src/models/domain/domain.ts index 7fe644740f..8eed0f5699 100644 --- a/src/models/domain/domain.ts +++ b/src/models/domain/domain.ts @@ -18,6 +18,20 @@ export default abstract class Domain { } } } + protected buildDataModel(domain: D, dataObj: any, map: any, notCipherStringList: any[] = []) { + for (const prop in map) { + if (!map.hasOwnProperty(prop)) { + continue; + } + + const objProp = (domain as any)[(map[prop] || prop)]; + if (notCipherStringList.indexOf(prop) > -1) { + (dataObj as any)[prop] = objProp != null ? objProp : null; + } else { + (dataObj as any)[prop] = objProp != null ? (objProp as CipherString).encryptedString : null; + } + } + } protected async decryptObj(viewModel: T, map: any, orgId: string): Promise { const promises = []; diff --git a/src/models/domain/field.ts b/src/models/domain/field.ts index 599dabff1c..82a884c849 100644 --- a/src/models/domain/field.ts +++ b/src/models/domain/field.ts @@ -31,4 +31,14 @@ export class Field extends Domain { value: null, }, orgId); } + + toFieldData(): FieldData { + const f = new FieldData(); + this.buildDataModel(this, f, { + name: null, + value: null, + type: null, + }, ['type']); + return f; + } } diff --git a/src/models/domain/identity.ts b/src/models/domain/identity.ts index ade03260ea..266d697579 100644 --- a/src/models/domain/identity.ts +++ b/src/models/domain/identity.ts @@ -75,4 +75,29 @@ export class Identity extends Domain { licenseNumber: null, }, orgId); } + + toIdentityData(): IdentityData { + const i = new IdentityData(); + this.buildDataModel(this, i, { + 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, + }); + return i; + } } diff --git a/src/models/domain/login.ts b/src/models/domain/login.ts index 211a371069..3d81b0eb09 100644 --- a/src/models/domain/login.ts +++ b/src/models/domain/login.ts @@ -51,4 +51,22 @@ export class Login extends Domain { return view; } + + toLoginData(): LoginData { + const l = new LoginData(); + this.buildDataModel(this, l, { + username: null, + password: null, + totp: null, + }); + + if (this.uris != null && this.uris.length > 0) { + l.uris = []; + this.uris.forEach((u) => { + l.uris.push(u.toLoginUriData()); + }); + } + + return l; + } } diff --git a/src/models/domain/loginUri.ts b/src/models/domain/loginUri.ts index 0cb6b19a7f..1e1d9a93fc 100644 --- a/src/models/domain/loginUri.ts +++ b/src/models/domain/loginUri.ts @@ -28,4 +28,12 @@ export class LoginUri extends Domain { uri: null, }, orgId); } + + toLoginUriData(): LoginUriData { + const u = new LoginUriData(); + this.buildDataModel(this, u, { + uri: null, + }, ['match']); + return u; + } } diff --git a/src/models/domain/secureNote.ts b/src/models/domain/secureNote.ts index 79724a4193..52988f1b25 100644 --- a/src/models/domain/secureNote.ts +++ b/src/models/domain/secureNote.ts @@ -21,4 +21,10 @@ export class SecureNote extends Domain { decrypt(orgId: string): Promise { return Promise.resolve(new SecureNoteView(this)); } + + toSecureNoteData(): SecureNoteData { + const n = new SecureNoteData(); + n.type = this.type; + return n; + } } diff --git a/src/models/request/cipherBulkDeleteRequest.ts b/src/models/request/cipherBulkDeleteRequest.ts new file mode 100644 index 0000000000..ccdf8ed22b --- /dev/null +++ b/src/models/request/cipherBulkDeleteRequest.ts @@ -0,0 +1,7 @@ +export class CipherBulkDeleteRequest { + ids: string[]; + + constructor(ids: string[]) { + this.ids = ids; + } +} diff --git a/src/models/request/cipherBulkMoveRequest.ts b/src/models/request/cipherBulkMoveRequest.ts new file mode 100644 index 0000000000..0d78e4f165 --- /dev/null +++ b/src/models/request/cipherBulkMoveRequest.ts @@ -0,0 +1,9 @@ +export class CipherBulkMoveRequest { + ids: string[]; + folderId: string; + + constructor(ids: string[], folderId: string) { + this.ids = ids; + this.folderId = folderId; + } +} diff --git a/src/models/request/cipherCollectionsRequest.ts b/src/models/request/cipherCollectionsRequest.ts new file mode 100644 index 0000000000..14718e2308 --- /dev/null +++ b/src/models/request/cipherCollectionsRequest.ts @@ -0,0 +1,7 @@ +export class CipherCollectionsRequest { + collectionIds: string[]; + + constructor(collectionIds: string[]) { + this.collectionIds = collectionIds; + } +} diff --git a/src/models/request/cipherRequest.ts b/src/models/request/cipherRequest.ts index a608cecb7a..a4d478c81f 100644 --- a/src/models/request/cipherRequest.ts +++ b/src/models/request/cipherRequest.ts @@ -21,6 +21,7 @@ export class CipherRequest { card: CardApi; identity: IdentityApi; fields: FieldApi[]; + attachments: { [id: string]: string; }; constructor(cipher: Cipher) { this.type = cipher.type; @@ -101,5 +102,12 @@ export class CipherRequest { }); }); } + + if (cipher.attachments) { + this.attachments = {}; + cipher.attachments.forEach((attachment) => { + this.attachments[attachment.id] = attachment.fileName ? attachment.fileName.encryptedString : null; + }); + } } } diff --git a/src/models/request/cipherShareRequest.ts b/src/models/request/cipherShareRequest.ts new file mode 100644 index 0000000000..4646890254 --- /dev/null +++ b/src/models/request/cipherShareRequest.ts @@ -0,0 +1,13 @@ +import { CipherRequest } from './cipherRequest'; + +import { Cipher } from '../domain/cipher'; + +export class CipherShareRequest { + cipher: CipherRequest; + collectionIds: string[]; + + constructor(cipher: Cipher) { + this.cipher = new CipherRequest(cipher); + this.collectionIds = cipher.collectionIds; + } +} diff --git a/src/models/response/cipherResponse.ts b/src/models/response/cipherResponse.ts index c8ed70ffd0..61c8907310 100644 --- a/src/models/response/cipherResponse.ts +++ b/src/models/response/cipherResponse.ts @@ -21,7 +21,7 @@ export class CipherResponse { favorite: boolean; edit: boolean; organizationUseTotp: boolean; - revisionDate: string; + revisionDate: Date; attachments: AttachmentResponse[]; collectionIds: string[]; @@ -35,7 +35,7 @@ export class CipherResponse { this.favorite = response.Favorite; this.edit = response.Edit; this.organizationUseTotp = response.OrganizationUseTotp; - this.revisionDate = response.RevisionDate; + this.revisionDate = new Date(response.RevisionDate); if (response.Login != null) { this.login = new LoginApi(response.Login); diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 5a512c4509..806ffb151c 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -7,6 +7,7 @@ import { TokenService } from '../abstractions/token.service'; import { EnvironmentUrls } from '../models/domain/environmentUrls'; import { CipherRequest } from '../models/request/cipherRequest'; +import { CipherShareRequest } from '../models/request/cipherShareRequest'; import { FolderRequest } from '../models/request/folderRequest'; import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; import { PasswordHintRequest } from '../models/request/passwordHintRequest'; @@ -334,6 +335,27 @@ export class ApiService implements ApiServiceAbstraction { } } + async shareCipher(id: string, request: CipherShareRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/share', { + body: JSON.stringify(request), + cache: 'no-cache', + credentials: this.getCredentials(), + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Content-Type': 'application/json; charset=utf-8', + 'Device-Type': this.deviceType, + }), + method: 'PUT', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + async deleteCipher(id: string): Promise { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { @@ -377,6 +399,28 @@ export class ApiService implements ApiServiceAbstraction { } } + async shareCipherAttachment(id: string, attachmentId: string, data: FormData, + organizationId: string): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment/' + + attachmentId + '/share?organizationId=' + organizationId, { + body: data, + cache: 'no-cache', + credentials: this.getCredentials(), + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Device-Type': this.deviceType, + }), + method: 'POST', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + async deleteCipherAttachment(id: string, attachmentId: string): Promise { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment/' + attachmentId, { diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 45b2b93e99..6c89d23141 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -3,6 +3,7 @@ import { UriMatchType } from '../enums/uriMatchType'; import { CipherData } from '../models/data/cipherData'; +import { Attachment } from '../models/domain/attachment'; import { Card } from '../models/domain/card'; import { Cipher } from '../models/domain/cipher'; import { CipherString } from '../models/domain/cipherString'; @@ -19,6 +20,7 @@ import { CipherRequest } from '../models/request/cipherRequest'; import { CipherResponse } from '../models/response/cipherResponse'; import { ErrorResponse } from '../models/response/errorResponse'; +import { AttachmentView } from '../models/view/attachmentView'; import { CardView } from '../models/view/cardView'; import { CipherView } from '../models/view/cipherView'; import { FieldView } from '../models/view/fieldView'; @@ -38,6 +40,7 @@ import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; import { Utils } from '../misc/utils'; +import { CipherShareRequest } from '../models/request/cipherShareRequest'; const Keys = { ciphersPrefix: 'ciphers_', @@ -77,11 +80,39 @@ export class CipherService implements CipherServiceAbstraction { this.encryptFields(model.fields, key).then((fields) => { cipher.fields = fields; }), + this.encryptAttachments(model.attachments, key).then((attachments) => { + cipher.attachments = attachments; + }), ]); return cipher; } + async encryptAttachments(attachmentsModel: AttachmentView[], key: SymmetricCryptoKey): Promise { + if (attachmentsModel == null || attachmentsModel.length === 0) { + return null; + } + + const promises: Array> = []; + const encAttachments: Attachment[] = []; + attachmentsModel.forEach(async (model) => { + const attachment = new Attachment(); + attachment.id = model.id; + attachment.size = model.size; + attachment.sizeName = model.sizeName; + attachment.url = model.url; + const promise = this.encryptObjProperty(model, attachment, { + fileName: null, + }, key).then(() => { + encAttachments.push(attachment); + }); + promises.push(promise); + }); + + await Promise.all(promises); + return encAttachments; + } + async encryptFields(fieldsModel: FieldView[], key: SymmetricCryptoKey): Promise { if (!fieldsModel || !fieldsModel.length) { return null; @@ -324,6 +355,49 @@ export class CipherService implements CipherServiceAbstraction { await this.upsert(data); } + async shareWithServer(cipher: Cipher): Promise { + const request = new CipherShareRequest(cipher); + await this.apiService.shareCipher(cipher.id, request); + const userId = await this.userService.getUserId(); + await this.upsert(cipher.toCipherData(userId)); + } + + async shareAttachmentWithServer(attachmentView: AttachmentView, cipherId: string, + organizationId: string): Promise { + const attachmentResponse = await fetch(new Request(attachmentView.url, { cache: 'no-cache' })); + if (attachmentResponse.status !== 200) { + throw Error('Failed to download attachment: ' + attachmentResponse.status.toString()); + } + + const buf = await attachmentResponse.arrayBuffer(); + const decBuf = await this.cryptoService.decryptFromBytes(buf, null); + const key = await this.cryptoService.getOrgKey(organizationId); + const encData = await this.cryptoService.encryptToBytes(decBuf, key); + const encFileName = await this.cryptoService.encrypt(attachmentView.fileName, key); + + const fd = new FormData(); + try { + const blob = new Blob([encData], { type: 'application/octet-stream' }); + fd.append('data', blob, encFileName.encryptedString); + } catch (e) { + if (Utils.isNode && !Utils.isBrowser) { + fd.append('data', new Buffer(encData) as any, { + filepath: encFileName.encryptedString, + contentType: 'application/octet-stream', + } as any); + } else { + throw e; + } + } + + let response: CipherResponse; + try { + response = await this.apiService.shareCipherAttachment(cipherId, attachmentView.id, fd, organizationId); + } catch (e) { + throw new Error((e as ErrorResponse).getSingleMessage()); + } + } + saveAttachmentWithServer(cipher: Cipher, unencryptedFile: any): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); From 1021f23277db5a47ae9b7be8e8021ad5005a8c10 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 12 Jun 2018 13:07:06 -0400 Subject: [PATCH 0284/1626] cipher collection apis --- src/abstractions/api.service.ts | 7 +- src/abstractions/cipher.service.ts | 1 + .../request/cipherCollectionsRequest.ts | 2 +- src/services/api.service.ts | 68 ++++++++++++------- src/services/cipher.service.ts | 14 +++- 5 files changed, 64 insertions(+), 28 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index a1410c0fe3..4bb7a854b6 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -1,5 +1,6 @@ import { EnvironmentUrls } from '../models/domain/environmentUrls'; +import { CipherCollectionsRequest } from '../models/request/cipherCollectionsRequest'; import { CipherRequest } from '../models/request/cipherRequest'; import { CipherShareRequest } from '../models/request/cipherShareRequest'; import { FolderRequest } from '../models/request/folderRequest'; @@ -37,11 +38,13 @@ export abstract class ApiService { deleteFolder: (id: string) => Promise; postCipher: (request: CipherRequest) => Promise; putCipher: (id: string, request: CipherRequest) => Promise; - shareCipher: (id: string, request: CipherShareRequest) => Promise; deleteCipher: (id: string) => Promise; + putShareCipher: (id: string, request: CipherShareRequest) => Promise; + putCipherCollections: (id: string, request: CipherCollectionsRequest) => Promise; postCipherAttachment: (id: string, data: FormData) => Promise; - shareCipherAttachment: (id: string, attachmentId: string, data: FormData, organizationId: string) => Promise; deleteCipherAttachment: (id: string, attachmentId: string) => Promise; + postShareCipherAttachment: (id: string, attachmentId: string, data: FormData, + organizationId: string) => Promise; getSync: () => Promise; postImportDirectory: (organizationId: string, request: ImportDirectoryRequest) => Promise; } diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index d621443f1b..06e66d2df2 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -31,6 +31,7 @@ export abstract class CipherService { organizationId: string) => Promise; saveAttachmentWithServer: (cipher: Cipher, unencryptedFile: any) => Promise; saveAttachmentRawWithServer: (cipher: Cipher, filename: string, data: ArrayBuffer) => Promise; + saveCollectionsWithServer: (cipher: Cipher) => Promise; upsert: (cipher: CipherData | CipherData[]) => Promise; replace: (ciphers: { [id: string]: CipherData; }) => Promise; clear: (userId: string) => Promise; diff --git a/src/models/request/cipherCollectionsRequest.ts b/src/models/request/cipherCollectionsRequest.ts index 14718e2308..1473f4e3d6 100644 --- a/src/models/request/cipherCollectionsRequest.ts +++ b/src/models/request/cipherCollectionsRequest.ts @@ -2,6 +2,6 @@ export class CipherCollectionsRequest { collectionIds: string[]; constructor(collectionIds: string[]) { - this.collectionIds = collectionIds; + this.collectionIds = collectionIds == null ? [] : collectionIds; } } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 806ffb151c..2f2d4f8308 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -6,6 +6,7 @@ import { TokenService } from '../abstractions/token.service'; import { EnvironmentUrls } from '../models/domain/environmentUrls'; +import { CipherCollectionsRequest } from '../models/request/cipherCollectionsRequest'; import { CipherRequest } from '../models/request/cipherRequest'; import { CipherShareRequest } from '../models/request/cipherShareRequest'; import { FolderRequest } from '../models/request/folderRequest'; @@ -335,7 +336,25 @@ export class ApiService implements ApiServiceAbstraction { } } - async shareCipher(id: string, request: CipherShareRequest): Promise { + async deleteCipher(id: string): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { + cache: 'no-cache', + credentials: this.getCredentials(), + headers: new Headers({ + 'Authorization': authHeader, + 'Device-Type': this.deviceType, + }), + method: 'DELETE', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async putShareCipher(id: string, request: CipherShareRequest): Promise { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/share', { body: JSON.stringify(request), @@ -356,16 +375,19 @@ export class ApiService implements ApiServiceAbstraction { } } - async deleteCipher(id: string): Promise { + async putCipherCollections(id: string, request: CipherCollectionsRequest): Promise { const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { + const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/collections', { + body: JSON.stringify(request), cache: 'no-cache', credentials: this.getCredentials(), headers: new Headers({ + 'Accept': 'application/json', 'Authorization': authHeader, + 'Content-Type': 'application/json; charset=utf-8', 'Device-Type': this.deviceType, }), - method: 'DELETE', + method: 'PUT', })); if (response.status !== 200) { @@ -399,7 +421,25 @@ export class ApiService implements ApiServiceAbstraction { } } - async shareCipherAttachment(id: string, attachmentId: string, data: FormData, + async deleteCipherAttachment(id: string, attachmentId: string): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment/' + attachmentId, { + cache: 'no-cache', + credentials: this.getCredentials(), + headers: new Headers({ + 'Authorization': authHeader, + 'Device-Type': this.deviceType, + }), + method: 'DELETE', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async postShareCipherAttachment(id: string, attachmentId: string, data: FormData, organizationId: string): Promise { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment/' + @@ -421,24 +461,6 @@ export class ApiService implements ApiServiceAbstraction { } } - async deleteCipherAttachment(id: string, attachmentId: string): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment/' + attachmentId, { - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - method: 'DELETE', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - // Sync APIs async getSync(): Promise { diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 6c89d23141..e122b45283 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -15,6 +15,7 @@ import { LoginUri } from '../models/domain/loginUri'; import { SecureNote } from '../models/domain/secureNote'; import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; +import { CipherCollectionsRequest } from '../models/request/cipherCollectionsRequest'; import { CipherRequest } from '../models/request/cipherRequest'; import { CipherResponse } from '../models/response/cipherResponse'; @@ -357,7 +358,7 @@ export class CipherService implements CipherServiceAbstraction { async shareWithServer(cipher: Cipher): Promise { const request = new CipherShareRequest(cipher); - await this.apiService.shareCipher(cipher.id, request); + await this.apiService.putShareCipher(cipher.id, request); const userId = await this.userService.getUserId(); await this.upsert(cipher.toCipherData(userId)); } @@ -392,7 +393,8 @@ export class CipherService implements CipherServiceAbstraction { let response: CipherResponse; try { - response = await this.apiService.shareCipherAttachment(cipherId, attachmentView.id, fd, organizationId); + response = await this.apiService.postShareCipherAttachment(cipherId, attachmentView.id, fd, + organizationId); } catch (e) { throw new Error((e as ErrorResponse).getSingleMessage()); } @@ -450,6 +452,14 @@ export class CipherService implements CipherServiceAbstraction { return new Cipher(cData); } + async saveCollectionsWithServer(cipher: Cipher): Promise { + const request = new CipherCollectionsRequest(cipher.collectionIds); + const response = await this.apiService.putCipherCollections(cipher.id, request); + const userId = await this.userService.getUserId(); + const data = cipher.toCipherData(userId); + await this.upsert(data); + } + async upsert(cipher: CipherData | CipherData[]): Promise { const userId = await this.userService.getUserId(); let ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( From 7a5a4e654a76b7a7e546d33b1e2ad4e3d6c9adc3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 12 Jun 2018 17:12:27 -0400 Subject: [PATCH 0285/1626] bulk move/delete apis --- src/abstractions/api.service.ts | 4 ++ src/abstractions/cipher.service.ts | 2 + src/models/request/cipherBulkDeleteRequest.ts | 2 +- src/models/request/cipherBulkMoveRequest.ts | 2 +- src/services/api.service.ts | 44 +++++++++++++++++++ src/services/cipher.service.ts | 29 +++++++++++- 6 files changed, 80 insertions(+), 3 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 4bb7a854b6..c9c7792076 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -1,5 +1,7 @@ import { EnvironmentUrls } from '../models/domain/environmentUrls'; +import { CipherBulkDeleteRequest } from '../models/request/cipherBulkDeleteRequest'; +import { CipherBulkMoveRequest } from '../models/request/cipherBulkMoveRequest'; import { CipherCollectionsRequest } from '../models/request/cipherCollectionsRequest'; import { CipherRequest } from '../models/request/cipherRequest'; import { CipherShareRequest } from '../models/request/cipherShareRequest'; @@ -39,6 +41,8 @@ export abstract class ApiService { postCipher: (request: CipherRequest) => Promise; putCipher: (id: string, request: CipherRequest) => Promise; deleteCipher: (id: string) => Promise; + deleteManyCiphers: (request: CipherBulkDeleteRequest) => Promise; + putMoveCiphers: (request: CipherBulkMoveRequest) => Promise; putShareCipher: (id: string, request: CipherShareRequest) => Promise; putCipherCollections: (id: string, request: CipherCollectionsRequest) => Promise; postCipherAttachment: (id: string, data: FormData) => Promise; diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index 06e66d2df2..edcc6479f5 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -35,8 +35,10 @@ export abstract class CipherService { upsert: (cipher: CipherData | CipherData[]) => Promise; replace: (ciphers: { [id: string]: CipherData; }) => Promise; clear: (userId: string) => Promise; + moveManyWithServer: (ids: string[], folderId: string) => Promise; delete: (id: string | string[]) => Promise; deleteWithServer: (id: string) => Promise; + deleteManyWithServer: (ids: string[]) => Promise; deleteAttachment: (id: string, attachmentId: string) => Promise; deleteAttachmentWithServer: (id: string, attachmentId: string) => Promise; sortCiphersByLastUsed: (a: any, b: any) => number; diff --git a/src/models/request/cipherBulkDeleteRequest.ts b/src/models/request/cipherBulkDeleteRequest.ts index ccdf8ed22b..fb19523a20 100644 --- a/src/models/request/cipherBulkDeleteRequest.ts +++ b/src/models/request/cipherBulkDeleteRequest.ts @@ -2,6 +2,6 @@ export class CipherBulkDeleteRequest { ids: string[]; constructor(ids: string[]) { - this.ids = ids; + this.ids = ids == null ? [] : ids; } } diff --git a/src/models/request/cipherBulkMoveRequest.ts b/src/models/request/cipherBulkMoveRequest.ts index 0d78e4f165..c0e8c62673 100644 --- a/src/models/request/cipherBulkMoveRequest.ts +++ b/src/models/request/cipherBulkMoveRequest.ts @@ -3,7 +3,7 @@ export class CipherBulkMoveRequest { folderId: string; constructor(ids: string[], folderId: string) { - this.ids = ids; + this.ids = ids == null ? [] : ids; this.folderId = folderId; } } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 2f2d4f8308..9fb8b58248 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -6,6 +6,8 @@ import { TokenService } from '../abstractions/token.service'; import { EnvironmentUrls } from '../models/domain/environmentUrls'; +import { CipherBulkDeleteRequest } from '../models/request/cipherBulkDeleteRequest'; +import { CipherBulkMoveRequest } from '../models/request/cipherBulkMoveRequest'; import { CipherCollectionsRequest } from '../models/request/cipherCollectionsRequest'; import { CipherRequest } from '../models/request/cipherRequest'; import { CipherShareRequest } from '../models/request/cipherShareRequest'; @@ -354,6 +356,48 @@ export class ApiService implements ApiServiceAbstraction { } } + async deleteManyCiphers(request: CipherBulkDeleteRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers', { + body: JSON.stringify(request), + cache: 'no-cache', + credentials: this.getCredentials(), + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Content-Type': 'application/json; charset=utf-8', + 'Device-Type': this.deviceType, + }), + method: 'DELETE', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async putMoveCiphers(request: CipherBulkMoveRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers/move', { + body: JSON.stringify(request), + cache: 'no-cache', + credentials: this.getCredentials(), + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Content-Type': 'application/json; charset=utf-8', + 'Device-Type': this.deviceType, + }), + method: 'PUT', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + async putShareCipher(id: string, request: CipherShareRequest): Promise { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/share', { diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index e122b45283..523da8d33f 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -15,8 +15,11 @@ import { LoginUri } from '../models/domain/loginUri'; import { SecureNote } from '../models/domain/secureNote'; import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; +import { CipherBulkDeleteRequest } from '../models/request/cipherBulkDeleteRequest'; +import { CipherBulkMoveRequest } from '../models/request/cipherBulkMoveRequest'; import { CipherCollectionsRequest } from '../models/request/cipherCollectionsRequest'; import { CipherRequest } from '../models/request/cipherRequest'; +import { CipherShareRequest } from '../models/request/cipherShareRequest'; import { CipherResponse } from '../models/response/cipherResponse'; import { ErrorResponse } from '../models/response/errorResponse'; @@ -41,7 +44,6 @@ import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; import { Utils } from '../misc/utils'; -import { CipherShareRequest } from '../models/request/cipherShareRequest'; const Keys = { ciphersPrefix: 'ciphers_', @@ -492,6 +494,26 @@ export class CipherService implements CipherServiceAbstraction { this.decryptedCipherCache = null; } + async moveManyWithServer(ids: string[], folderId: string): Promise { + await this.apiService.putMoveCiphers(new CipherBulkMoveRequest(ids, folderId)); + + const userId = await this.userService.getUserId(); + let ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( + Keys.ciphersPrefix + userId); + if (ciphers == null) { + ciphers = {}; + } + + ids.forEach((id) => { + if (ciphers.hasOwnProperty(id)) { + ciphers[id].folderId = folderId; + } + }); + + await this.storageService.save(Keys.ciphersPrefix + userId, ciphers); + this.decryptedCipherCache = null; + } + async delete(id: string | string[]): Promise { const userId = await this.userService.getUserId(); const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( @@ -518,6 +540,11 @@ export class CipherService implements CipherServiceAbstraction { await this.delete(id); } + async deleteManyWithServer(ids: string[]): Promise { + await this.apiService.deleteManyCiphers(new CipherBulkDeleteRequest(ids)); + await this.delete(ids); + } + async deleteAttachment(id: string, attachmentId: string): Promise { const userId = await this.userService.getUserId(); const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( From cfad521ea8ef205abe774907d8f7a243b3adaf10 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 13 Jun 2018 00:02:15 -0400 Subject: [PATCH 0286/1626] bulk share apis --- src/abstractions/api.service.ts | 2 ++ src/abstractions/cipher.service.ts | 3 ++- src/models/request/cipherBulkShareRequest.ts | 18 +++++++++++++ src/services/api.service.ts | 22 ++++++++++++++++ src/services/cipher.service.ts | 27 +++++++++++++++++--- 5 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 src/models/request/cipherBulkShareRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index c9c7792076..2032cebe04 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -2,6 +2,7 @@ import { EnvironmentUrls } from '../models/domain/environmentUrls'; import { CipherBulkDeleteRequest } from '../models/request/cipherBulkDeleteRequest'; import { CipherBulkMoveRequest } from '../models/request/cipherBulkMoveRequest'; +import { CipherBulkShareRequest } from '../models/request/cipherBulkShareRequest'; import { CipherCollectionsRequest } from '../models/request/cipherCollectionsRequest'; import { CipherRequest } from '../models/request/cipherRequest'; import { CipherShareRequest } from '../models/request/cipherShareRequest'; @@ -44,6 +45,7 @@ export abstract class ApiService { deleteManyCiphers: (request: CipherBulkDeleteRequest) => Promise; putMoveCiphers: (request: CipherBulkMoveRequest) => Promise; putShareCipher: (id: string, request: CipherShareRequest) => Promise; + putShareCiphers: (request: CipherBulkShareRequest) => Promise; putCipherCollections: (id: string, request: CipherCollectionsRequest) => Promise; postCipherAttachment: (id: string, data: FormData) => Promise; deleteCipherAttachment: (id: string, attachmentId: string) => Promise; diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index edcc6479f5..c1072e484d 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -26,7 +26,8 @@ export abstract class CipherService { updateLastUsedDate: (id: string) => Promise; saveNeverDomain: (domain: string) => Promise; saveWithServer: (cipher: Cipher) => Promise; - shareWithServer: (cipher: Cipher) => Promise; + shareWithServer: (cipher: CipherView, organizationId: string, collectionIds: string[]) => Promise; + shareManyWithServer: (ciphers: CipherView[], organizationId: string, collectionIds: string[]) => Promise; shareAttachmentWithServer: (attachmentView: AttachmentView, cipherId: string, organizationId: string) => Promise; saveAttachmentWithServer: (cipher: Cipher, unencryptedFile: any) => Promise; diff --git a/src/models/request/cipherBulkShareRequest.ts b/src/models/request/cipherBulkShareRequest.ts new file mode 100644 index 0000000000..a4dede2875 --- /dev/null +++ b/src/models/request/cipherBulkShareRequest.ts @@ -0,0 +1,18 @@ +import { CipherRequest } from './cipherRequest'; + +import { Cipher } from '../domain/cipher'; + +export class CipherBulkShareRequest { + ciphers: CipherRequest[]; + collectionIds: string[]; + + constructor(ciphers: Cipher[], collectionIds: string[]) { + if (ciphers != null) { + this.ciphers = []; + ciphers.forEach((c) => { + this.ciphers.push(new CipherRequest(c)); + }); + } + this.collectionIds = collectionIds; + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 9fb8b58248..b365217fa7 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -8,6 +8,7 @@ import { EnvironmentUrls } from '../models/domain/environmentUrls'; import { CipherBulkDeleteRequest } from '../models/request/cipherBulkDeleteRequest'; import { CipherBulkMoveRequest } from '../models/request/cipherBulkMoveRequest'; +import { CipherBulkShareRequest } from '../models/request/cipherBulkShareRequest'; import { CipherCollectionsRequest } from '../models/request/cipherCollectionsRequest'; import { CipherRequest } from '../models/request/cipherRequest'; import { CipherShareRequest } from '../models/request/cipherShareRequest'; @@ -419,6 +420,27 @@ export class ApiService implements ApiServiceAbstraction { } } + async putShareCiphers(request: CipherBulkShareRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.baseUrl + '/ciphers/share', { + body: JSON.stringify(request), + cache: 'no-cache', + credentials: this.getCredentials(), + headers: new Headers({ + 'Accept': 'application/json', + 'Authorization': authHeader, + 'Content-Type': 'application/json; charset=utf-8', + 'Device-Type': this.deviceType, + }), + method: 'PUT', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + async putCipherCollections(id: string, request: CipherCollectionsRequest): Promise { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/collections', { diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 523da8d33f..6b60c0bd56 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -17,6 +17,7 @@ import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; import { CipherBulkDeleteRequest } from '../models/request/cipherBulkDeleteRequest'; import { CipherBulkMoveRequest } from '../models/request/cipherBulkMoveRequest'; +import { CipherBulkShareRequest } from '../models/request/cipherBulkShareRequest'; import { CipherCollectionsRequest } from '../models/request/cipherCollectionsRequest'; import { CipherRequest } from '../models/request/cipherRequest'; import { CipherShareRequest } from '../models/request/cipherShareRequest'; @@ -358,11 +359,31 @@ export class CipherService implements CipherServiceAbstraction { await this.upsert(data); } - async shareWithServer(cipher: Cipher): Promise { - const request = new CipherShareRequest(cipher); + async shareWithServer(cipher: CipherView, organizationId: string, collectionIds: string[]): Promise { + cipher.organizationId = organizationId; + cipher.collectionIds = collectionIds; + const encCipher = await this.encrypt(cipher); + const request = new CipherShareRequest(encCipher); await this.apiService.putShareCipher(cipher.id, request); const userId = await this.userService.getUserId(); - await this.upsert(cipher.toCipherData(userId)); + await this.upsert(encCipher.toCipherData(userId)); + } + + async shareManyWithServer(ciphers: CipherView[], organizationId: string, collectionIds: string[]): Promise { + const promises: Array> = []; + const encCiphers: Cipher[] = []; + for (const cipher of ciphers) { + cipher.organizationId = organizationId; + cipher.collectionIds = collectionIds; + promises.push(this.encrypt(cipher).then((c) => { + encCiphers.push(c); + })); + } + await Promise.all(promises); + const request = new CipherBulkShareRequest(encCiphers, collectionIds); + await this.apiService.putShareCiphers(request); + const userId = await this.userService.getUserId(); + await this.upsert(encCiphers.map((c) => c.toCipherData(userId))); } async shareAttachmentWithServer(attachmentView: AttachmentView, cipherId: string, From 1ab6ce6ec02217c74c3473b087868b9e0bf43f94 Mon Sep 17 00:00:00 2001 From: Neil Burrows Date: Wed, 13 Jun 2018 17:33:26 +0100 Subject: [PATCH 0287/1626] Adding mask and toggle for Card Security Code (#5) * Adding mask and toggle for Card Security Code * For Card Code Mask - show same number of characters as code --- src/angular/components/view.component.ts | 6 ++++++ src/models/view/cardView.ts | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index 18cdfafb93..9ff6b83b78 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -31,6 +31,7 @@ export class ViewComponent implements OnDestroy { cipher: CipherView; showPassword: boolean; + showCardCode: boolean; isPremium: boolean; totpCode: string; totpCodeFormatted: string; @@ -80,6 +81,11 @@ export class ViewComponent implements OnDestroy { this.showPassword = !this.showPassword; } + toggleCardCode() { + this.analytics.eventTrack.next({ action: 'Toggled Card Code' }); + this.showCardCode = !this.showCardCode; + } + async checkPassword() { if (this.cipher.login == null || this.cipher.login.password == null || this.cipher.login.password === '') { return; diff --git a/src/models/view/cardView.ts b/src/models/view/cardView.ts index 26e3308e31..1630c67482 100644 --- a/src/models/view/cardView.ts +++ b/src/models/view/cardView.ts @@ -18,6 +18,10 @@ export class CardView implements View { // ctor } + get maskedCode(): string { + return this.code != null ? '•'.repeat(this.code.length) : null; + } + get brand(): string { return this._brand; } From 149ae8a59c8412e58798afa09135928d6ba6890c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 13 Jun 2018 12:36:48 -0400 Subject: [PATCH 0288/1626] remove allowJs from tsconfig --- tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 490d58e983..9db279a707 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,6 @@ "target": "ES6", "module": "commonjs", "lib": ["es5", "es6", "dom"], - "allowJs": true, "sourceMap": true, "declaration": true, "allowSyntheticDefaultImports": true, From c42a45e9675c1f32cb68c25786af58560e79fd2c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 13 Jun 2018 14:08:28 -0400 Subject: [PATCH 0289/1626] cipher with id for bulk share request --- src/models/request/cipherBulkShareRequest.ts | 6 +++--- src/models/request/cipherWithIdRequest.ts | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 src/models/request/cipherWithIdRequest.ts diff --git a/src/models/request/cipherBulkShareRequest.ts b/src/models/request/cipherBulkShareRequest.ts index a4dede2875..413212e1ae 100644 --- a/src/models/request/cipherBulkShareRequest.ts +++ b/src/models/request/cipherBulkShareRequest.ts @@ -1,16 +1,16 @@ -import { CipherRequest } from './cipherRequest'; +import { CipherWithIdRequest } from './cipherWithIdRequest'; import { Cipher } from '../domain/cipher'; export class CipherBulkShareRequest { - ciphers: CipherRequest[]; + ciphers: CipherWithIdRequest[]; collectionIds: string[]; constructor(ciphers: Cipher[], collectionIds: string[]) { if (ciphers != null) { this.ciphers = []; ciphers.forEach((c) => { - this.ciphers.push(new CipherRequest(c)); + this.ciphers.push(new CipherWithIdRequest(c)); }); } this.collectionIds = collectionIds; diff --git a/src/models/request/cipherWithIdRequest.ts b/src/models/request/cipherWithIdRequest.ts new file mode 100644 index 0000000000..a3d5e9d808 --- /dev/null +++ b/src/models/request/cipherWithIdRequest.ts @@ -0,0 +1,12 @@ +import { CipherRequest } from './cipherRequest'; + +import { Cipher } from '../domain/cipher'; + +export class CipherWithIdRequest extends CipherRequest { + id: string; + + constructor(cipher: Cipher) { + super(cipher); + this.id = cipher.id; + } +} From 4dea46f1addd949e4f564f8d0fe8039ccc66a655 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 13 Jun 2018 14:08:43 -0400 Subject: [PATCH 0290/1626] prod urls for web client --- src/services/api.service.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/services/api.service.ts b/src/services/api.service.ts index b365217fa7..7f581859f5 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -1,4 +1,4 @@ -import { ConstantsService } from './constants.service'; +import { DeviceType } from '../enums/deviceType'; import { ApiService as ApiServiceAbstraction } from '../abstractions/api.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; @@ -37,8 +37,9 @@ export class ApiService implements ApiServiceAbstraction { constructor(private tokenService: TokenService, private platformUtilsService: PlatformUtilsService, private logoutCallback: (expired: boolean) => Promise) { - this.deviceType = platformUtilsService.getDevice().toString(); - this.isWebClient = platformUtilsService.identityClientId === 'web'; + const device = platformUtilsService.getDevice(); + this.deviceType = device.toString(); + this.isWebClient = device === DeviceType.Web; } setUrls(urls: EnvironmentUrls): void { @@ -75,8 +76,13 @@ export class ApiService implements ApiServiceAbstraction { //this.identityBaseUrl = 'https://preview-identity.bitwarden.com'; // Production - this.baseUrl = 'https://api.bitwarden.com'; - this.identityBaseUrl = 'https://identity.bitwarden.com'; + if (this.isWebClient) { + this.baseUrl = 'https://vault.bitwarden.com/api'; + this.identityBaseUrl = 'https://vault.bitwarden.com/identity'; + } else { + this.baseUrl = 'https://api.bitwarden.com'; + this.identityBaseUrl = 'https://identity.bitwarden.com'; + } /* tslint:enable */ } From 0429c0557b293ca97ea684ad8bb500c036d88ae3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 13 Jun 2018 16:48:00 -0400 Subject: [PATCH 0291/1626] hkdf key stretching for pbkdf2 key --- src/services/crypto.service.ts | 50 +++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 7d72e3ee85..d2c97f0c34 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -117,11 +117,20 @@ export class CryptoService implements CryptoServiceAbstraction { return null; } - const decEncKey = await this.decrypt(new CipherString(encKey), key); + let decEncKey: ArrayBuffer; + const encKeyCipher = new CipherString(encKey); + if (encKeyCipher.encryptionType === EncryptionType.AesCbc256_B64) { + decEncKey = await this.decrypt(encKeyCipher, key); + } else if (encKeyCipher.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) { + const newKey = await this.stretchKey(key); + decEncKey = await this.decrypt(encKeyCipher, newKey); + } else { + throw new Error('Unsupported encKey type.'); + } + if (decEncKey == null) { return null; } - this.encKey = new SymmetricCryptoKey(decEncKey); return this.encKey; } @@ -258,8 +267,23 @@ export class CryptoService implements CryptoServiceAbstraction { } async makeEncKey(key: SymmetricCryptoKey): Promise { - const bytes = await this.cryptoFunctionService.randomBytes(64); - return this.encrypt(bytes, key); + const encKey = await this.cryptoFunctionService.randomBytes(64); + // TODO: remove false/true flags when we're ready to enable key stretching + if (false && key.key.byteLength === 32) { + const newKey = await this.stretchKey(key); + return this.encrypt(encKey, newKey); + } else if (true || key.key.byteLength === 64) { + return this.encrypt(encKey, key); + } else { + throw new Error('Invalid key size.'); + } + } + + async stretchKey(key: SymmetricCryptoKey): Promise { + const newKey = new Uint8Array(64); + newKey.set(await this.hkdfExpand(key.key, Utils.fromUtf8ToArray('enc'), 32)); + newKey.set(await this.hkdfExpand(key.key, Utils.fromUtf8ToArray('mac'), 32), 32); + return new SymmetricCryptoKey(newKey.buffer); } async encrypt(plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey): Promise { @@ -566,4 +590,22 @@ export class CryptoService implements CryptoServiceAbstraction { return key; } + + // ref: https://tools.ietf.org/html/rfc5869 + private async hkdfExpand(prk: ArrayBuffer, info: Uint8Array, size: number) { + const hashLen = 32; // sha256 + const okm = new Uint8Array(size); + let previousT = new Uint8Array(0); + const n = Math.ceil(size / hashLen); + for (let i = 0; i < n; i++) { + const t = new Uint8Array(previousT.length + info.length + 1); + t.set(previousT); + t.set(info, previousT.length); + t.set([i + 1], t.length - 1); + const hmac = await this.cryptoFunctionService.hmac(t.buffer, prk, 'sha256'); + previousT = new Uint8Array(hmac); + okm.set(previousT, i * hashLen); + } + return okm; + } } From 6aa922f380c4673bb983c3a69a043daf3ee328f7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 13 Jun 2018 17:06:09 -0400 Subject: [PATCH 0292/1626] fix unreachable code --- src/services/crypto.service.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index d2c97f0c34..9b400687dc 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -268,15 +268,18 @@ export class CryptoService implements CryptoServiceAbstraction { async makeEncKey(key: SymmetricCryptoKey): Promise { const encKey = await this.cryptoFunctionService.randomBytes(64); - // TODO: remove false/true flags when we're ready to enable key stretching - if (false && key.key.byteLength === 32) { + // TODO: Uncomment when we're ready to enable key stretching + return this.encrypt(encKey, key); + /* + if (key.key.byteLength === 32) { const newKey = await this.stretchKey(key); return this.encrypt(encKey, newKey); - } else if (true || key.key.byteLength === 64) { + } else if (key.key.byteLength === 64) { return this.encrypt(encKey, key); } else { throw new Error('Invalid key size.'); } + */ } async stretchKey(key: SymmetricCryptoKey): Promise { From eda99e4f12c28922d01aa51a6329194e8b839e27 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 13 Jun 2018 17:10:52 -0400 Subject: [PATCH 0293/1626] hasKey helper --- src/abstractions/crypto.service.ts | 1 + src/services/crypto.service.ts | 18 +++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 4220a36b90..fc55466ee7 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -15,6 +15,7 @@ export abstract class CryptoService { getPrivateKey: () => Promise; getOrgKeys: () => Promise>; getOrgKey: (orgId: string) => Promise; + hasKey: () => Promise; clearKey: () => Promise; clearKeyHash: () => Promise; clearEncKey: (memoryOnly?: boolean) => Promise; diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 9b400687dc..697bf76cc4 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -192,6 +192,10 @@ export class CryptoService implements CryptoServiceAbstraction { return orgKeys.get(orgId); } + async hasKey(): Promise { + return (await this.getKey()) != null; + } + clearKey(): Promise { this.key = this.legacyEtmKey = null; return this.secureStorageService.remove(Keys.key); @@ -282,13 +286,6 @@ export class CryptoService implements CryptoServiceAbstraction { */ } - async stretchKey(key: SymmetricCryptoKey): Promise { - const newKey = new Uint8Array(64); - newKey.set(await this.hkdfExpand(key.key, Utils.fromUtf8ToArray('enc'), 32)); - newKey.set(await this.hkdfExpand(key.key, Utils.fromUtf8ToArray('mac'), 32), 32); - return new SymmetricCryptoKey(newKey.buffer); - } - async encrypt(plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey): Promise { if (plainValue == null) { return Promise.resolve(null); @@ -594,6 +591,13 @@ export class CryptoService implements CryptoServiceAbstraction { return key; } + private async stretchKey(key: SymmetricCryptoKey): Promise { + const newKey = new Uint8Array(64); + newKey.set(await this.hkdfExpand(key.key, Utils.fromUtf8ToArray('enc'), 32)); + newKey.set(await this.hkdfExpand(key.key, Utils.fromUtf8ToArray('mac'), 32), 32); + return new SymmetricCryptoKey(newKey.buffer); + } + // ref: https://tools.ietf.org/html/rfc5869 private async hkdfExpand(prk: ArrayBuffer, info: Uint8Array, size: number) { const hashLen = 32; // sha256 From 3303e60b40aec1897b22c992ee9cd74db18bcff6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 13 Jun 2018 17:14:26 -0400 Subject: [PATCH 0294/1626] implement hasKey helper --- src/angular/services/auth-guard.service.ts | 4 ++-- src/services/api.service.ts | 10 ++++++---- src/services/cipher.service.ts | 4 ++-- src/services/collection.service.ts | 4 ++-- src/services/folder.service.ts | 4 ++-- src/services/lock.service.ts | 4 ++-- src/services/passwordGeneration.service.ts | 4 ++-- 7 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/angular/services/auth-guard.service.ts b/src/angular/services/auth-guard.service.ts index c388e2094c..1db4e5726c 100644 --- a/src/angular/services/auth-guard.service.ts +++ b/src/angular/services/auth-guard.service.ts @@ -20,8 +20,8 @@ export class AuthGuardService implements CanActivate { return false; } - const key = await this.cryptoService.getKey(); - if (key == null) { + const hasKey = await this.cryptoService.hasKey(); + if (!hasKey) { this.router.navigate(['lock']); return false; } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 7f581859f5..6c735530b4 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -60,8 +60,8 @@ export class ApiService implements ApiServiceAbstraction { /* tslint:disable */ // Desktop - //this.baseUrl = 'http://localhost:4000'; - //this.identityBaseUrl = 'http://localhost:33656'; + this.baseUrl = 'http://localhost:4000'; + this.identityBaseUrl = 'http://localhost:33656'; // Desktop HTTPS //this.baseUrl = 'https://localhost:44377'; @@ -76,13 +76,15 @@ export class ApiService implements ApiServiceAbstraction { //this.identityBaseUrl = 'https://preview-identity.bitwarden.com'; // Production + /* if (this.isWebClient) { - this.baseUrl = 'https://vault.bitwarden.com/api'; - this.identityBaseUrl = 'https://vault.bitwarden.com/identity'; + this.baseUrl = 'https://api.bitwarden.com'; + this.identityBaseUrl = 'https://identity.bitwarden.com'; } else { this.baseUrl = 'https://api.bitwarden.com'; this.identityBaseUrl = 'https://identity.bitwarden.com'; } + */ /* tslint:enable */ } diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 6b60c0bd56..2544d4f1c4 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -179,8 +179,8 @@ export class CipherService implements CipherServiceAbstraction { } const decCiphers: CipherView[] = []; - const key = await this.cryptoService.getKey(); - if (key == null) { + const hasKey = await this.cryptoService.hasKey(); + if (!hasKey) { throw new Error('No key.'); } diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index caca3c8920..02821bcba9 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -54,8 +54,8 @@ export class CollectionService implements CollectionServiceAbstraction { return this.decryptedCollectionCache; } - const key = await this.cryptoService.getKey(); - if (key == null) { + const hasKey = await this.cryptoService.hasKey(); + if (!hasKey) { throw new Error('No key.'); } diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index f596941bb3..d4806e0c0f 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -67,8 +67,8 @@ export class FolderService implements FolderServiceAbstraction { return this.decryptedFolderCache; } - const key = await this.cryptoService.getKey(); - if (key == null) { + const hasKey = await this.cryptoService.hasKey(); + if (!hasKey) { throw new Error('No key.'); } diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts index 50e46c1420..bc744ce012 100644 --- a/src/services/lock.service.ts +++ b/src/services/lock.service.ts @@ -36,8 +36,8 @@ export class LockService implements LockServiceAbstraction { return; } - const key = await this.cryptoService.getKey(); - if (key == null) { + const hasKey = await this.cryptoService.hasKey(); + if (!hasKey) { // no key so no need to lock return; } diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index f39acb59cf..0e68c0c8f8 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -169,7 +169,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr } async getHistory(): Promise { - const hasKey = (await this.cryptoService.getKey()) != null; + const hasKey = await this.cryptoService.hasKey(); if (!hasKey) { return new Array(); } @@ -184,7 +184,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr async addHistory(password: string): Promise { // Cannot add new history if no key is available - const hasKey = (await this.cryptoService.getKey()) != null; + const hasKey = await this.cryptoService.hasKey(); if (!hasKey) { return; } From 755f8f93ecdb9039297807be3346be53a75395c5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 13 Jun 2018 22:09:47 -0400 Subject: [PATCH 0295/1626] apiBaseUrl and make some props private --- src/abstractions/api.service.ts | 3 -- src/services/api.service.ts | 75 ++++++++++++++++----------------- 2 files changed, 37 insertions(+), 41 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 2032cebe04..d3e9bcba7b 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -24,9 +24,6 @@ import { AttachmentView } from '../models/view/attachmentView'; export abstract class ApiService { urlsSet: boolean; - baseUrl: string; - identityBaseUrl: string; - deviceType: string; setUrls: (urls: EnvironmentUrls) => void; postIdentityToken: (request: TokenRequest) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 6c735530b4..2b7218d59c 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -29,11 +29,12 @@ import { SyncResponse } from '../models/response/syncResponse'; export class ApiService implements ApiServiceAbstraction { urlsSet: boolean = false; - baseUrl: string; - identityBaseUrl: string; - deviceType: string; - isWebClient = false; - usingBaseUrl = false; + + private apiBaseUrl: string; + private identityBaseUrl: string; + private deviceType: string; + private isWebClient = false; + private usingBaseUrl = false; constructor(private tokenService: TokenService, private platformUtilsService: PlatformUtilsService, private logoutCallback: (expired: boolean) => Promise) { @@ -47,44 +48,42 @@ export class ApiService implements ApiServiceAbstraction { if (urls.base != null) { this.usingBaseUrl = true; - this.baseUrl = urls.base + '/api'; + this.apiBaseUrl = urls.base + '/api'; this.identityBaseUrl = urls.base + '/identity'; return; } if (urls.api != null && urls.identity != null) { - this.baseUrl = urls.api; + this.apiBaseUrl = urls.api; this.identityBaseUrl = urls.identity; return; } /* tslint:disable */ // Desktop - this.baseUrl = 'http://localhost:4000'; - this.identityBaseUrl = 'http://localhost:33656'; + //this.apiBaseUrl = 'http://localhost:4000'; + //this.identityBaseUrl = 'http://localhost:33656'; // Desktop HTTPS - //this.baseUrl = 'https://localhost:44377'; + //this.apiBaseUrl = 'https://localhost:44377'; //this.identityBaseUrl = 'https://localhost:44392'; // Desktop external - //this.baseUrl = 'http://192.168.1.3:4000'; + //this.apiBaseUrl = 'http://192.168.1.3:4000'; //this.identityBaseUrl = 'http://192.168.1.3:33656'; // Preview - //this.baseUrl = 'https://preview-api.bitwarden.com'; + //this.apiBaseUrl = 'https://preview-api.bitwarden.com'; //this.identityBaseUrl = 'https://preview-identity.bitwarden.com'; // Production - /* if (this.isWebClient) { - this.baseUrl = 'https://api.bitwarden.com'; - this.identityBaseUrl = 'https://identity.bitwarden.com'; + this.apiBaseUrl = 'https://vault.bitwarden.com/api'; + this.identityBaseUrl = 'https://vault.bitwarden.com/identity'; } else { - this.baseUrl = 'https://api.bitwarden.com'; + this.apiBaseUrl = 'https://api.bitwarden.com'; this.identityBaseUrl = 'https://identity.bitwarden.com'; } - */ /* tslint:enable */ } @@ -133,7 +132,7 @@ export class ApiService implements ApiServiceAbstraction { // Two Factor APIs async postTwoFactorEmail(request: TwoFactorEmailRequest): Promise { - const response = await fetch(new Request(this.baseUrl + '/two-factor/send-email-login', { + const response = await fetch(new Request(this.apiBaseUrl + '/two-factor/send-email-login', { body: JSON.stringify(request), cache: 'no-cache', credentials: this.getCredentials(), @@ -154,7 +153,7 @@ export class ApiService implements ApiServiceAbstraction { async getProfile(): Promise { const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/accounts/profile', { + const response = await fetch(new Request(this.apiBaseUrl + '/accounts/profile', { cache: 'no-cache', credentials: this.getCredentials(), headers: new Headers({ @@ -175,7 +174,7 @@ export class ApiService implements ApiServiceAbstraction { async getAccountRevisionDate(): Promise { const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/accounts/revision-date', { + const response = await fetch(new Request(this.apiBaseUrl + '/accounts/revision-date', { cache: 'no-cache', credentials: this.getCredentials(), headers: new Headers({ @@ -194,7 +193,7 @@ export class ApiService implements ApiServiceAbstraction { } async postPasswordHint(request: PasswordHintRequest): Promise { - const response = await fetch(new Request(this.baseUrl + '/accounts/password-hint', { + const response = await fetch(new Request(this.apiBaseUrl + '/accounts/password-hint', { body: JSON.stringify(request), cache: 'no-cache', credentials: this.getCredentials(), @@ -212,7 +211,7 @@ export class ApiService implements ApiServiceAbstraction { } async postRegister(request: RegisterRequest): Promise { - const response = await fetch(new Request(this.baseUrl + '/accounts/register', { + const response = await fetch(new Request(this.apiBaseUrl + '/accounts/register', { body: JSON.stringify(request), cache: 'no-cache', credentials: this.getCredentials(), @@ -233,7 +232,7 @@ export class ApiService implements ApiServiceAbstraction { async postFolder(request: FolderRequest): Promise { const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/folders', { + const response = await fetch(new Request(this.apiBaseUrl + '/folders', { body: JSON.stringify(request), cache: 'no-cache', credentials: this.getCredentials(), @@ -257,7 +256,7 @@ export class ApiService implements ApiServiceAbstraction { async putFolder(id: string, request: FolderRequest): Promise { const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/folders/' + id, { + const response = await fetch(new Request(this.apiBaseUrl + '/folders/' + id, { body: JSON.stringify(request), cache: 'no-cache', credentials: this.getCredentials(), @@ -281,7 +280,7 @@ export class ApiService implements ApiServiceAbstraction { async deleteFolder(id: string): Promise { const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/folders/' + id, { + const response = await fetch(new Request(this.apiBaseUrl + '/folders/' + id, { cache: 'no-cache', credentials: this.getCredentials(), headers: new Headers({ @@ -301,7 +300,7 @@ export class ApiService implements ApiServiceAbstraction { async postCipher(request: CipherRequest): Promise { const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers', { + const response = await fetch(new Request(this.apiBaseUrl + '/ciphers', { body: JSON.stringify(request), cache: 'no-cache', credentials: this.getCredentials(), @@ -325,7 +324,7 @@ export class ApiService implements ApiServiceAbstraction { async putCipher(id: string, request: CipherRequest): Promise { const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { + const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/' + id, { body: JSON.stringify(request), cache: 'no-cache', credentials: this.getCredentials(), @@ -349,7 +348,7 @@ export class ApiService implements ApiServiceAbstraction { async deleteCipher(id: string): Promise { const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { + const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/' + id, { cache: 'no-cache', credentials: this.getCredentials(), headers: new Headers({ @@ -367,7 +366,7 @@ export class ApiService implements ApiServiceAbstraction { async deleteManyCiphers(request: CipherBulkDeleteRequest): Promise { const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers', { + const response = await fetch(new Request(this.apiBaseUrl + '/ciphers', { body: JSON.stringify(request), cache: 'no-cache', credentials: this.getCredentials(), @@ -388,7 +387,7 @@ export class ApiService implements ApiServiceAbstraction { async putMoveCiphers(request: CipherBulkMoveRequest): Promise { const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers/move', { + const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/move', { body: JSON.stringify(request), cache: 'no-cache', credentials: this.getCredentials(), @@ -409,7 +408,7 @@ export class ApiService implements ApiServiceAbstraction { async putShareCipher(id: string, request: CipherShareRequest): Promise { const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/share', { + const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/' + id + '/share', { body: JSON.stringify(request), cache: 'no-cache', credentials: this.getCredentials(), @@ -430,7 +429,7 @@ export class ApiService implements ApiServiceAbstraction { async putShareCiphers(request: CipherBulkShareRequest): Promise { const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers/share', { + const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/share', { body: JSON.stringify(request), cache: 'no-cache', credentials: this.getCredentials(), @@ -451,7 +450,7 @@ export class ApiService implements ApiServiceAbstraction { async putCipherCollections(id: string, request: CipherCollectionsRequest): Promise { const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/collections', { + const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/' + id + '/collections', { body: JSON.stringify(request), cache: 'no-cache', credentials: this.getCredentials(), @@ -474,7 +473,7 @@ export class ApiService implements ApiServiceAbstraction { async postCipherAttachment(id: string, data: FormData): Promise { const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment', { + const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/' + id + '/attachment', { body: data, cache: 'no-cache', credentials: this.getCredentials(), @@ -497,7 +496,7 @@ export class ApiService implements ApiServiceAbstraction { async deleteCipherAttachment(id: string, attachmentId: string): Promise { const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment/' + attachmentId, { + const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/' + id + '/attachment/' + attachmentId, { cache: 'no-cache', credentials: this.getCredentials(), headers: new Headers({ @@ -516,7 +515,7 @@ export class ApiService implements ApiServiceAbstraction { async postShareCipherAttachment(id: string, attachmentId: string, data: FormData, organizationId: string): Promise { const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment/' + + const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/' + id + '/attachment/' + attachmentId + '/share?organizationId=' + organizationId, { body: data, cache: 'no-cache', @@ -539,7 +538,7 @@ export class ApiService implements ApiServiceAbstraction { async getSync(): Promise { const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/sync', { + const response = await fetch(new Request(this.apiBaseUrl + '/sync', { cache: 'no-cache', credentials: this.getCredentials(), headers: new Headers({ @@ -560,7 +559,7 @@ export class ApiService implements ApiServiceAbstraction { async postImportDirectory(organizationId: string, request: ImportDirectoryRequest): Promise { const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/organizations/' + organizationId + '/import', { + const response = await fetch(new Request(this.apiBaseUrl + '/organizations/' + organizationId + '/import', { body: JSON.stringify(request), cache: 'no-cache', credentials: this.getCredentials(), From 4bce071498aeda43e44f9b5f7f60ae11a57c7d6f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 13 Jun 2018 22:12:23 -0400 Subject: [PATCH 0296/1626] expose api service urls --- src/abstractions/api.service.ts | 2 ++ src/services/api.service.ts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index d3e9bcba7b..cffb70f279 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -24,6 +24,8 @@ import { AttachmentView } from '../models/view/attachmentView'; export abstract class ApiService { urlsSet: boolean; + apiBaseUrl: string; + identityBaseUrl: string; setUrls: (urls: EnvironmentUrls) => void; postIdentityToken: (request: TokenRequest) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 2b7218d59c..0e7fe20833 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -29,9 +29,9 @@ import { SyncResponse } from '../models/response/syncResponse'; export class ApiService implements ApiServiceAbstraction { urlsSet: boolean = false; + apiBaseUrl: string; + identityBaseUrl: string; - private apiBaseUrl: string; - private identityBaseUrl: string; private deviceType: string; private isWebClient = false; private usingBaseUrl = false; From 6e89b503f2ee050899eb5102ab4badcf06e612cb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 13 Jun 2018 22:47:40 -0400 Subject: [PATCH 0297/1626] hmac directly into array --- src/services/crypto.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 697bf76cc4..f01f05b5c0 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -609,8 +609,7 @@ export class CryptoService implements CryptoServiceAbstraction { t.set(previousT); t.set(info, previousT.length); t.set([i + 1], t.length - 1); - const hmac = await this.cryptoFunctionService.hmac(t.buffer, prk, 'sha256'); - previousT = new Uint8Array(hmac); + previousT = new Uint8Array(await this.cryptoFunctionService.hmac(t.buffer, prk, 'sha256')); okm.set(previousT, i * hashLen); } return okm; From c3ad9b9b7df2c70ea9a80627c3c4e263886eaf8b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 13 Jun 2018 23:15:02 -0400 Subject: [PATCH 0298/1626] remove unused import --- src/models/request/cipherRequest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/models/request/cipherRequest.ts b/src/models/request/cipherRequest.ts index a4d478c81f..5d876ebaba 100644 --- a/src/models/request/cipherRequest.ts +++ b/src/models/request/cipherRequest.ts @@ -1,7 +1,6 @@ import { CipherType } from '../../enums/cipherType'; import { Cipher } from '../domain/cipher'; -import { Field } from '../domain/field'; import { CardApi } from '../api/cardApi'; import { FieldApi } from '../api/fieldApi'; From 8fcbc887a221fdb1a16446b6ad1effdfab5bc108 Mon Sep 17 00:00:00 2001 From: Neil Burrows Date: Sat, 16 Jun 2018 13:39:51 +0100 Subject: [PATCH 0299/1626] Mask Card Code on Angular Web Vault (#6) * Adding mask and toggle for Card Security Code * For Card Code Mask - show same number of characters as code * Mask Card Code in angular web vault --- src/angular/components/add-edit.component.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 5c69e331dc..3034c377f2 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -46,6 +46,7 @@ export class AddEditComponent { deletePromise: Promise; checkPasswordPromise: Promise; showPassword: boolean = false; + showCardCode: boolean = false; cipherType = CipherType; fieldType = FieldType; addFieldType: FieldType = FieldType.Text; @@ -262,6 +263,12 @@ export class AddEditComponent { document.getElementById('loginPassword').focus(); } + toggleCardCode() { + this.analytics.eventTrack.next({ action: 'Toggled CardCode on Edit' }); + this.showCardCode = !this.showCardCode; + document.getElementById('cardCode').focus(); + } + toggleFieldValue(field: FieldView) { const f = (field as any); f.showValue = !f.showValue; From e5db01083cc13df3696bb30562a83d729280ac03 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 18 Jun 2018 08:33:09 -0400 Subject: [PATCH 0300/1626] switch icons server to icons.bitwarden.net --- src/angular/components/icon.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/icon.component.ts b/src/angular/components/icon.component.ts index cac8d381a2..4b127c07db 100644 --- a/src/angular/components/icon.component.ts +++ b/src/angular/components/icon.component.ts @@ -32,7 +32,7 @@ export class IconComponent implements OnChanges { if (environmentService.baseUrl) { this.iconsUrl = environmentService.baseUrl + '/icons'; } else { - this.iconsUrl = 'https://icons.bitwarden.com'; + this.iconsUrl = 'https://icons.bitwarden.net'; } } } From d75543e6c88b220d8e3c7d66fb768d39f9480587 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 19 Jun 2018 23:40:10 -0400 Subject: [PATCH 0301/1626] totp and edit props --- src/angular/components/view.component.ts | 1 - src/models/view/cipherView.ts | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index 9ff6b83b78..a7325893dc 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -1,7 +1,6 @@ import { EventEmitter, Input, - OnChanges, OnDestroy, Output, } from '@angular/core'; diff --git a/src/models/view/cipherView.ts b/src/models/view/cipherView.ts index 9566c7d285..2b5f1499f1 100644 --- a/src/models/view/cipherView.ts +++ b/src/models/view/cipherView.ts @@ -18,6 +18,8 @@ export class CipherView implements View { notes: string; type: CipherType; favorite: boolean; + organizationUseTotp: boolean; + edit: boolean; localData: any; login: LoginView; identity: IdentityView; @@ -36,6 +38,8 @@ export class CipherView implements View { this.organizationId = c.organizationId; this.folderId = c.folderId; this.favorite = c.favorite; + this.organizationUseTotp = c.organizationUseTotp; + this.edit = c.edit; this.type = c.type; this.localData = c.localData; this.collectionIds = c.collectionIds; From 6e501dddb9411c51ae2377523171d65f67884226 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 20 Jun 2018 22:12:15 -0400 Subject: [PATCH 0302/1626] force length to 5 when < 5 --- src/angular/components/password-generator.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/password-generator.component.ts b/src/angular/components/password-generator.component.ts index 12f6267023..0a1608dbb0 100644 --- a/src/angular/components/password-generator.component.ts +++ b/src/angular/components/password-generator.component.ts @@ -88,7 +88,7 @@ export class PasswordGeneratorComponent implements OnInit { } } - if (!this.options.length) { + if (!this.options.length || this.options.length < 5) { this.options.length = 5; } else if (this.options.length > 128) { this.options.length = 128; From d98aeab0c87a944f4f6e086e01ab0eb44bda7d53 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 20 Jun 2018 23:40:59 -0400 Subject: [PATCH 0303/1626] put profile --- src/abstractions/api.service.ts | 4 ++-- src/models/request/updateProfileRequest.ts | 10 +++++++++ src/services/api.service.ts | 25 ++++++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 src/models/request/updateProfileRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index cffb70f279..9f650c1a56 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -12,6 +12,7 @@ import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { TokenRequest } from '../models/request/tokenRequest'; import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; +import { UpdateProfileRequest } from '../models/request/updateProfileRequest'; import { CipherResponse } from '../models/response/cipherResponse'; import { FolderResponse } from '../models/response/folderResponse'; @@ -20,8 +21,6 @@ import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorR import { ProfileResponse } from '../models/response/profileResponse'; import { SyncResponse } from '../models/response/syncResponse'; -import { AttachmentView } from '../models/view/attachmentView'; - export abstract class ApiService { urlsSet: boolean; apiBaseUrl: string; @@ -32,6 +31,7 @@ export abstract class ApiService { refreshIdentityToken: () => Promise; postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise; getProfile: () => Promise; + putProfile: (request: UpdateProfileRequest) => Promise; getAccountRevisionDate: () => Promise; postPasswordHint: (request: PasswordHintRequest) => Promise; postRegister: (request: RegisterRequest) => Promise; diff --git a/src/models/request/updateProfileRequest.ts b/src/models/request/updateProfileRequest.ts new file mode 100644 index 0000000000..e029f3f783 --- /dev/null +++ b/src/models/request/updateProfileRequest.ts @@ -0,0 +1,10 @@ +export class UpdateProfileRequest { + name: string; + masterPasswordHint: string; + culture = 'en-US'; // deprecated + + constructor(name: string, masterPasswordHint: string) { + this.name = name; + this.masterPasswordHint = masterPasswordHint ? masterPasswordHint : null; + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 0e7fe20833..747904ec11 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -18,6 +18,7 @@ import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { TokenRequest } from '../models/request/tokenRequest'; import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; +import { UpdateProfileRequest } from '../models/request/updateProfileRequest'; import { CipherResponse } from '../models/response/cipherResponse'; import { ErrorResponse } from '../models/response/errorResponse'; @@ -172,6 +173,30 @@ export class ApiService implements ApiServiceAbstraction { } } + async putProfile(request: UpdateProfileRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.apiBaseUrl + '/accounts/profile', { + body: JSON.stringify(request), + cache: 'no-cache', + credentials: this.getCredentials(), + headers: new Headers({ + 'Accept': 'application/json', + 'Content-Type': 'application/json; charset=utf-8', + 'Authorization': authHeader, + 'Device-Type': this.deviceType, + }), + method: 'PUT', + })); + + if (response.status === 200) { + const responseJson = await response.json(); + return new ProfileResponse(responseJson); + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + async getAccountRevisionDate(): Promise { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.apiBaseUrl + '/accounts/revision-date', { From 322dcf76ae7de4c7633d0828ec8f062ee856e5c4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 21 Jun 2018 14:28:49 -0400 Subject: [PATCH 0304/1626] apis for change email and change password --- src/abstractions/api.service.ts | 6 +++ src/models/request/emailRequest.ts | 7 +++ src/models/request/emailTokenRequest.ts | 4 ++ src/models/request/passwordRequest.ts | 5 ++ src/services/api.service.ts | 66 +++++++++++++++++++++++++ 5 files changed, 88 insertions(+) create mode 100644 src/models/request/emailRequest.ts create mode 100644 src/models/request/emailTokenRequest.ts create mode 100644 src/models/request/passwordRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 9f650c1a56..150e4eee75 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -6,9 +6,12 @@ import { CipherBulkShareRequest } from '../models/request/cipherBulkShareRequest import { CipherCollectionsRequest } from '../models/request/cipherCollectionsRequest'; import { CipherRequest } from '../models/request/cipherRequest'; import { CipherShareRequest } from '../models/request/cipherShareRequest'; +import { EmailRequest } from '../models/request/emailRequest'; +import { EmailTokenRequest } from '../models/request/emailTokenRequest'; import { FolderRequest } from '../models/request/folderRequest'; import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; import { PasswordHintRequest } from '../models/request/passwordHintRequest'; +import { PasswordRequest } from '../models/request/passwordRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { TokenRequest } from '../models/request/tokenRequest'; import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; @@ -32,6 +35,9 @@ export abstract class ApiService { postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise; getProfile: () => Promise; putProfile: (request: UpdateProfileRequest) => Promise; + postEmailToken: (request: EmailTokenRequest) => Promise; + postEmail: (request: EmailRequest) => Promise; + postPassword: (request: PasswordRequest) => Promise; getAccountRevisionDate: () => Promise; postPasswordHint: (request: PasswordHintRequest) => Promise; postRegister: (request: RegisterRequest) => Promise; diff --git a/src/models/request/emailRequest.ts b/src/models/request/emailRequest.ts new file mode 100644 index 0000000000..da9e6f7829 --- /dev/null +++ b/src/models/request/emailRequest.ts @@ -0,0 +1,7 @@ +export class EmailRequest { + newEmail: string; + masterPasswordHash: string; + newMasterPasswordHash: string; + token: string; + key: string; +} diff --git a/src/models/request/emailTokenRequest.ts b/src/models/request/emailTokenRequest.ts new file mode 100644 index 0000000000..98235680cf --- /dev/null +++ b/src/models/request/emailTokenRequest.ts @@ -0,0 +1,4 @@ +export class EmailTokenRequest { + newEmail: string; + masterPasswordHash: string; +} diff --git a/src/models/request/passwordRequest.ts b/src/models/request/passwordRequest.ts new file mode 100644 index 0000000000..5e79e83d14 --- /dev/null +++ b/src/models/request/passwordRequest.ts @@ -0,0 +1,5 @@ +export class PasswordRequest { + masterPasswordHash: string; + newMasterPasswordHash: string; + key: string; +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 747904ec11..a52b00a3b2 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -12,9 +12,12 @@ import { CipherBulkShareRequest } from '../models/request/cipherBulkShareRequest import { CipherCollectionsRequest } from '../models/request/cipherCollectionsRequest'; import { CipherRequest } from '../models/request/cipherRequest'; import { CipherShareRequest } from '../models/request/cipherShareRequest'; +import { EmailRequest } from '../models/request/emailRequest'; +import { EmailTokenRequest } from '../models/request/emailTokenRequest'; import { FolderRequest } from '../models/request/folderRequest'; import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; import { PasswordHintRequest } from '../models/request/passwordHintRequest'; +import { PasswordRequest } from '../models/request/passwordRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { TokenRequest } from '../models/request/tokenRequest'; import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; @@ -197,6 +200,69 @@ export class ApiService implements ApiServiceAbstraction { } } + async postEmailToken(request: EmailTokenRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.apiBaseUrl + '/accounts/email-token', { + body: JSON.stringify(request), + cache: 'no-cache', + credentials: this.getCredentials(), + headers: new Headers({ + 'Accept': 'application/json', + 'Content-Type': 'application/json; charset=utf-8', + 'Authorization': authHeader, + 'Device-Type': this.deviceType, + }), + method: 'POST', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async postEmail(request: EmailRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.apiBaseUrl + '/accounts/email', { + body: JSON.stringify(request), + cache: 'no-cache', + credentials: this.getCredentials(), + headers: new Headers({ + 'Accept': 'application/json', + 'Content-Type': 'application/json; charset=utf-8', + 'Authorization': authHeader, + 'Device-Type': this.deviceType, + }), + method: 'POST', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async postPassword(request: PasswordRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.apiBaseUrl + '/accounts/password', { + body: JSON.stringify(request), + cache: 'no-cache', + credentials: this.getCredentials(), + headers: new Headers({ + 'Accept': 'application/json', + 'Content-Type': 'application/json; charset=utf-8', + 'Authorization': authHeader, + 'Device-Type': this.deviceType, + }), + method: 'POST', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + async getAccountRevisionDate(): Promise { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.apiBaseUrl + '/accounts/revision-date', { From 2e486d5a7ce5422f705a398f23a78a46c9a9d42d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 21 Jun 2018 17:15:43 -0400 Subject: [PATCH 0305/1626] more apis for account settings --- src/abstractions/api.service.ts | 4 ++ src/models/request/emailRequest.ts | 6 +- src/models/request/emailTokenRequest.ts | 4 +- src/models/request/passwordRequest.ts | 5 +- .../request/passwordVerificationRequest.ts | 3 + src/services/api.service.ts | 68 ++++++++++++++++++- 6 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 src/models/request/passwordVerificationRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 150e4eee75..4267fbbdf2 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -12,6 +12,7 @@ import { FolderRequest } from '../models/request/folderRequest'; import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; +import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { TokenRequest } from '../models/request/tokenRequest'; import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; @@ -38,6 +39,8 @@ export abstract class ApiService { postEmailToken: (request: EmailTokenRequest) => Promise; postEmail: (request: EmailRequest) => Promise; postPassword: (request: PasswordRequest) => Promise; + postSecurityStamp: (request: PasswordVerificationRequest) => Promise; + postDeleteAccount: (request: PasswordVerificationRequest) => Promise; getAccountRevisionDate: () => Promise; postPasswordHint: (request: PasswordHintRequest) => Promise; postRegister: (request: RegisterRequest) => Promise; @@ -52,6 +55,7 @@ export abstract class ApiService { putShareCipher: (id: string, request: CipherShareRequest) => Promise; putShareCiphers: (request: CipherBulkShareRequest) => Promise; putCipherCollections: (id: string, request: CipherCollectionsRequest) => Promise; + postPurgeCiphers: (request: PasswordVerificationRequest) => Promise; postCipherAttachment: (id: string, data: FormData) => Promise; deleteCipherAttachment: (id: string, attachmentId: string) => Promise; postShareCipherAttachment: (id: string, attachmentId: string, data: FormData, diff --git a/src/models/request/emailRequest.ts b/src/models/request/emailRequest.ts index da9e6f7829..a2172a1efa 100644 --- a/src/models/request/emailRequest.ts +++ b/src/models/request/emailRequest.ts @@ -1,6 +1,6 @@ -export class EmailRequest { - newEmail: string; - masterPasswordHash: string; +import { EmailTokenRequest } from './emailTokenRequest'; + +export class EmailRequest extends EmailTokenRequest { newMasterPasswordHash: string; token: string; key: string; diff --git a/src/models/request/emailTokenRequest.ts b/src/models/request/emailTokenRequest.ts index 98235680cf..3e74669d4c 100644 --- a/src/models/request/emailTokenRequest.ts +++ b/src/models/request/emailTokenRequest.ts @@ -1,4 +1,6 @@ -export class EmailTokenRequest { +import { PasswordVerificationRequest } from './passwordVerificationRequest'; + +export class EmailTokenRequest extends PasswordVerificationRequest { newEmail: string; masterPasswordHash: string; } diff --git a/src/models/request/passwordRequest.ts b/src/models/request/passwordRequest.ts index 5e79e83d14..4d877da6d4 100644 --- a/src/models/request/passwordRequest.ts +++ b/src/models/request/passwordRequest.ts @@ -1,5 +1,6 @@ -export class PasswordRequest { - masterPasswordHash: string; +import { PasswordVerificationRequest } from './passwordVerificationRequest'; + +export class PasswordRequest extends PasswordVerificationRequest { newMasterPasswordHash: string; key: string; } diff --git a/src/models/request/passwordVerificationRequest.ts b/src/models/request/passwordVerificationRequest.ts new file mode 100644 index 0000000000..f9a038ac15 --- /dev/null +++ b/src/models/request/passwordVerificationRequest.ts @@ -0,0 +1,3 @@ +export class PasswordVerificationRequest { + masterPasswordHash: string; +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index a52b00a3b2..409dc21317 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -18,6 +18,7 @@ import { FolderRequest } from '../models/request/folderRequest'; import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; +import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { TokenRequest } from '../models/request/tokenRequest'; import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; @@ -82,8 +83,8 @@ export class ApiService implements ApiServiceAbstraction { // Production if (this.isWebClient) { - this.apiBaseUrl = 'https://vault.bitwarden.com/api'; - this.identityBaseUrl = 'https://vault.bitwarden.com/identity'; + this.apiBaseUrl = 'https://api.bitwarden.com'; + this.identityBaseUrl = 'https://identity.bitwarden.com'; } else { this.apiBaseUrl = 'https://api.bitwarden.com'; this.identityBaseUrl = 'https://identity.bitwarden.com'; @@ -263,6 +264,48 @@ export class ApiService implements ApiServiceAbstraction { } } + async postSecurityStamp(request: PasswordVerificationRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.apiBaseUrl + '/accounts/security-stamp', { + body: JSON.stringify(request), + cache: 'no-cache', + credentials: this.getCredentials(), + headers: new Headers({ + 'Accept': 'application/json', + 'Content-Type': 'application/json; charset=utf-8', + 'Authorization': authHeader, + 'Device-Type': this.deviceType, + }), + method: 'POST', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async postDeleteAccount(request: PasswordVerificationRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.apiBaseUrl + '/accounts/delete', { + body: JSON.stringify(request), + cache: 'no-cache', + credentials: this.getCredentials(), + headers: new Headers({ + 'Accept': 'application/json', + 'Content-Type': 'application/json; charset=utf-8', + 'Authorization': authHeader, + 'Device-Type': this.deviceType, + }), + method: 'POST', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + async getAccountRevisionDate(): Promise { const authHeader = await this.handleTokenState(); const response = await fetch(new Request(this.apiBaseUrl + '/accounts/revision-date', { @@ -560,6 +603,27 @@ export class ApiService implements ApiServiceAbstraction { } } + async postPurgeCiphers(request: PasswordVerificationRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/purge', { + body: JSON.stringify(request), + cache: 'no-cache', + credentials: this.getCredentials(), + headers: new Headers({ + 'Accept': 'application/json', + 'Content-Type': 'application/json; charset=utf-8', + 'Authorization': authHeader, + 'Device-Type': this.deviceType, + }), + method: 'POST', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + // Attachments APIs async postCipherAttachment(id: string, data: FormData): Promise { From dc01f0701ea7905d2da7c3babb19870e212d2337 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 21 Jun 2018 17:16:22 -0400 Subject: [PATCH 0306/1626] revert web client api endpoint changes --- src/services/api.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 409dc21317..180ec7b45c 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -83,8 +83,8 @@ export class ApiService implements ApiServiceAbstraction { // Production if (this.isWebClient) { - this.apiBaseUrl = 'https://api.bitwarden.com'; - this.identityBaseUrl = 'https://identity.bitwarden.com'; + this.apiBaseUrl = 'https://vault.bitwarden.com/api'; + this.identityBaseUrl = 'https://vault.bitwarden.com/identity'; } else { this.apiBaseUrl = 'https://api.bitwarden.com'; this.identityBaseUrl = 'https://identity.bitwarden.com'; From 99e522a5d119c1ac6917f1013a73d90770999b5a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 21 Jun 2018 22:12:26 -0400 Subject: [PATCH 0307/1626] cleanup api urls --- src/services/api.service.ts | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 180ec7b45c..de68749643 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -65,30 +65,13 @@ export class ApiService implements ApiServiceAbstraction { } /* tslint:disable */ - // Desktop + // Local Dev //this.apiBaseUrl = 'http://localhost:4000'; //this.identityBaseUrl = 'http://localhost:33656'; - // Desktop HTTPS - //this.apiBaseUrl = 'https://localhost:44377'; - //this.identityBaseUrl = 'https://localhost:44392'; - - // Desktop external - //this.apiBaseUrl = 'http://192.168.1.3:4000'; - //this.identityBaseUrl = 'http://192.168.1.3:33656'; - - // Preview - //this.apiBaseUrl = 'https://preview-api.bitwarden.com'; - //this.identityBaseUrl = 'https://preview-identity.bitwarden.com'; - // Production - if (this.isWebClient) { - this.apiBaseUrl = 'https://vault.bitwarden.com/api'; - this.identityBaseUrl = 'https://vault.bitwarden.com/identity'; - } else { - this.apiBaseUrl = 'https://api.bitwarden.com'; - this.identityBaseUrl = 'https://identity.bitwarden.com'; - } + this.apiBaseUrl = 'https://api.bitwarden.com'; + this.identityBaseUrl = 'https://identity.bitwarden.com'; /* tslint:enable */ } From 23e950beea3264f663e1f5bf8e1674afd21900d1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 22 Jun 2018 09:41:01 -0400 Subject: [PATCH 0308/1626] dont export organization ciphers --- src/services/export.service.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/services/export.service.ts b/src/services/export.service.ts index 6f2a1ae3ee..e62b089c97 100644 --- a/src/services/export.service.ts +++ b/src/services/export.service.ts @@ -9,8 +9,6 @@ import { FolderService } from '../abstractions/folder.service'; import { CipherView } from '../models/view/cipherView'; import { FolderView } from '../models/view/folderView'; -import { Utils } from '../misc/utils'; - export class ExportService implements ExportServiceAbstraction { constructor(private folderService: FolderService, private cipherService: CipherService) { } @@ -41,6 +39,10 @@ export class ExportService implements ExportServiceAbstraction { return; } + if (c.organizationId != null) { + return; + } + const cipher: any = { folder: c.folderId && foldersMap.has(c.folderId) ? foldersMap.get(c.folderId).name : null, favorite: c.favorite ? 1 : null, From 154c087b97f1bb8f5f8de097c317ee3f78986194 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 23 Jun 2018 09:27:30 -0400 Subject: [PATCH 0309/1626] importers for lastpass, bitwarden, and keepassx --- src/importers/baseImporter.ts | 187 +++++++++++++++++++++ src/importers/bitwardenCsvImporter.ts | 109 ++++++++++++ src/importers/importer.ts | 5 + src/importers/keepassxCsvImporter.ts | 67 ++++++++ src/importers/lastpassCsvImporter.ts | 232 ++++++++++++++++++++++++++ src/models/domain/importResult.ts | 13 ++ 6 files changed, 613 insertions(+) create mode 100644 src/importers/baseImporter.ts create mode 100644 src/importers/bitwardenCsvImporter.ts create mode 100644 src/importers/importer.ts create mode 100644 src/importers/keepassxCsvImporter.ts create mode 100644 src/importers/lastpassCsvImporter.ts create mode 100644 src/models/domain/importResult.ts diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts new file mode 100644 index 0000000000..c65813dadb --- /dev/null +++ b/src/importers/baseImporter.ts @@ -0,0 +1,187 @@ +import * as papa from 'papaparse'; + +import { LoginUriView } from '../models/view/loginUriView'; + +export abstract class BaseImporter { + protected passwordFieldNames = [ + 'password', 'pass word', 'passphrase', 'pass phrase', + 'pass', 'code', 'code word', 'codeword', + 'secret', 'secret word', 'personpwd', + 'key', 'keyword', 'key word', 'keyphrase', 'key phrase', + 'form_pw', 'wppassword', 'pin', 'pwd', 'pw', 'pword', 'passwd', + 'p', 'serial', 'serial#', 'license key', 'reg #', + + // Non-English names + 'passwort' + ]; + + protected usernameFieldNames = [ + 'user', 'name', 'user name', 'username', 'login name', + 'email', 'e-mail', 'id', 'userid', 'user id', + 'login', 'form_loginname', 'wpname', 'mail', + 'loginid', 'login id', 'log', 'personlogin', + 'first name', 'last name', 'card#', 'account #', + 'member', 'member #', + + // Non-English names + 'nom', 'benutzername' + ]; + + protected notesFieldNames = [ + "note", "notes", "comment", "comments", "memo", + "description", "free form", "freeform", + "free text", "freetext", "free", + + // Non-English names + "kommentar" + ]; + + protected uriFieldNames: string[] = [ + 'url', 'hyper link', 'hyperlink', 'link', + 'host', 'hostname', 'host name', 'server', 'address', + 'hyper ref', 'href', 'web', 'website', 'web site', 'site', + 'web-site', 'uri', + + // Non-English names + 'ort', 'adresse' + ]; + + protected parseCsv(data: string, header: boolean): any[] { + const result = papa.parse(data, { + header: header, + encoding: 'UTF-8', + }); + if (result.errors != null && result.errors.length > 0) { + result.errors.forEach((e) => { + // tslint:disable-next-line + console.warn('Error parsing row ' + e.row + ': ' + e.message); + }); + return null; + } + return result.data; + } + + protected parseSingleRowCsv(rowData: string) { + if (this.isNullOrWhitespace(rowData)) { + return null; + } + const parsedRow = this.parseCsv(rowData, false); + if (parsedRow != null && parsedRow.length > 0 && parsedRow[0].length > 0) { + return parsedRow[0]; + } + return null; + } + + protected makeUriArray(uri: string | string[]): LoginUriView[] { + if (uri == null) { + return null; + } + + if (typeof uri === 'string') { + const loginUri = new LoginUriView(); + loginUri.uri = this.fixUri(uri); + loginUri.match = null; + return [loginUri]; + } + + if (uri.length > 0) { + const returnArr: LoginUriView[] = []; + uri.forEach((u) => { + const loginUri = new LoginUriView(); + loginUri.uri = this.fixUri(u); + loginUri.match = null; + returnArr.push(loginUri); + }); + return returnArr; + } + + return null; + } + + protected fixUri(uri: string) { + if (uri == null) { + return null; + } + uri = uri.toLowerCase().trim(); + if (uri.indexOf('://') === -1 && uri.indexOf('.') >= 0) { + uri = 'http://' + uri; + } + if (uri.length > 1000) { + return uri.substring(0, 1000); + } + return uri; + } + + protected isNullOrWhitespace(str: string): boolean { + return str == null || str.trim() === ''; + } + + protected getValueOrDefault(str: string, defaultValue: string = null): string { + if (this.isNullOrWhitespace(str)) { + return defaultValue; + } + return str; + } + + protected splitNewLine(str: string): string[] { + return str.split(/(?:\r\n|\r|\n)/); + } + + // ref https://stackoverflow.com/a/5911300 + protected getCardBrand(cardNum: string) { + if (this.isNullOrWhitespace(cardNum)) { + return null; + } + + // Visa + let re = new RegExp('^4'); + if (cardNum.match(re) != null) { + return 'Visa'; + } + + // Mastercard + // Updated for Mastercard 2017 BINs expansion + if (/^(5[1-5][0-9]{14}|2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12}))$/ + .test(cardNum)) { + return 'Mastercard'; + } + + // AMEX + re = new RegExp('^3[47]'); + if (cardNum.match(re) != null) { + return 'Amex'; + } + + // Discover + re = new RegExp('^(6011|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5]|64[4-9])|65)'); + if (cardNum.match(re) != null) { + return 'Discover'; + } + + // Diners + re = new RegExp('^36'); + if (cardNum.match(re) != null) { + return 'Diners Club'; + } + + // Diners - Carte Blanche + re = new RegExp('^30[0-5]'); + if (cardNum.match(re) != null) { + return 'Diners Club'; + } + + // JCB + re = new RegExp('^35(2[89]|[3-8][0-9])'); + if (cardNum.match(re) != null) { + return 'JCB'; + } + + // Visa Electron + re = new RegExp('^(4026|417500|4508|4844|491(3|7))'); + if (cardNum.match(re) != null) { + return 'Visa'; + } + + return null; + } +} diff --git a/src/importers/bitwardenCsvImporter.ts b/src/importers/bitwardenCsvImporter.ts new file mode 100644 index 0000000000..ce3729468e --- /dev/null +++ b/src/importers/bitwardenCsvImporter.ts @@ -0,0 +1,109 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CipherView } from '../models/view/cipherView'; +import { FieldView } from '../models/view/fieldView'; +import { FolderView } from '../models/view/folderView'; +import { LoginView } from '../models/view/loginView'; +import { SecureNoteView } from '../models/view/secureNoteView'; + +import { CipherType } from '../enums/cipherType'; +import { FieldType } from '../enums/fieldType'; +import { SecureNoteType } from '../enums/secureNoteType'; + +export class BitwardenCsvImporter extends BaseImporter implements Importer { + import(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + let folderIndex = result.folders.length; + const cipherIndex = result.ciphers.length; + const hasFolder = !this.isNullOrWhitespace(value.folder); + let addFolder = hasFolder; + + if (hasFolder) { + for (let i = 0; i < result.folders.length; i++) { + if (result.folders[i].name === value.folder) { + addFolder = false; + folderIndex = i; + break; + } + } + } + + const cipher = new CipherView(); + cipher.type = CipherType.Login; + cipher.favorite = this.getValueOrDefault(value.favorite, '0') !== '0' ? true : false; + cipher.notes = this.getValueOrDefault(value.notes); + cipher.name = this.getValueOrDefault(value.name, '--'); + + if (!this.isNullOrWhitespace(value.fields)) { + const fields = this.splitNewLine(value.fields); + for (let i = 0; i < fields.length; i++) { + if (this.isNullOrWhitespace(fields[i])) { + continue; + } + + const delimPosition = fields[i].lastIndexOf(': '); + if (delimPosition === -1) { + continue; + } + + if (cipher.fields == null) { + cipher.fields = []; + } + + const field = new FieldView(); + field.name = fields[i].substr(0, delimPosition); + field.value = null; + field.type = FieldType.Text; + if (fields[i].length > (delimPosition + 2)) { + field.value = fields[i].substr(delimPosition + 2); + } + cipher.fields.push(field); + } + } + + const valueType = value.type != null ? value.type.toLowerCase() : null; + switch (valueType) { + case 'login': + case null: + cipher.type = CipherType.Login; + cipher.login = new LoginView(); + cipher.login.totp = this.getValueOrDefault(value.login_totp || value.totp); + cipher.login.username = this.getValueOrDefault(value.login_username || value.username); + cipher.login.password = this.getValueOrDefault(value.login_password || value.password); + const uris = this.parseSingleRowCsv(value.login_uri || value.uri); + cipher.login.uris = this.makeUriArray(uris); + break; + case 'note': + cipher.type = CipherType.SecureNote; + cipher.secureNote = new SecureNoteView(); + cipher.secureNote.type = SecureNoteType.Generic; + break; + default: + break; + } + + result.ciphers.push(cipher); + + if (addFolder) { + const f = new FolderView(); + f.name = value.folder; + result.folders.push(f); + } + if (hasFolder) { + result.folderRelationships.set(cipherIndex, folderIndex); + } + }); + + return result; + } +} diff --git a/src/importers/importer.ts b/src/importers/importer.ts new file mode 100644 index 0000000000..453f88440d --- /dev/null +++ b/src/importers/importer.ts @@ -0,0 +1,5 @@ +import { ImportResult } from '../models/domain/importResult'; + +export interface Importer { + import(data: string): ImportResult; +} diff --git a/src/importers/keepassxCsvImporter.ts b/src/importers/keepassxCsvImporter.ts new file mode 100644 index 0000000000..30eee8f115 --- /dev/null +++ b/src/importers/keepassxCsvImporter.ts @@ -0,0 +1,67 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CipherView } from '../models/view/cipherView'; +import { FolderView } from '../models/view/folderView'; +import { LoginView } from '../models/view/loginView'; + +import { CipherType } from '../enums/cipherType'; + +export class KeePassXCsvImporter extends BaseImporter implements Importer { + import(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + value.Group = !this.isNullOrWhitespace(value.Group) && value.Group.startsWith('Root/') ? + value.Group.replace('Root/', '') : value.Group; + const groupName = !this.isNullOrWhitespace(value.Group) ? value.Group.split('/').join(' > ') : null; + + let folderIndex = result.folders.length; + const cipherIndex = result.ciphers.length; + const hasFolder = groupName != null; + let addFolder = hasFolder; + + if (hasFolder) { + for (let i = 0; i < result.folders.length; i++) { + if (result.folders[i].name === groupName) { + addFolder = false; + folderIndex = i; + break; + } + } + } + + const cipher = new CipherView(); + cipher.type = CipherType.Login; + cipher.favorite = false; + cipher.notes = this.getValueOrDefault(value.Notes); + cipher.name = this.getValueOrDefault(value.Title, '--'); + cipher.login = new LoginView(); + cipher.login.username = this.getValueOrDefault(value.Username); + cipher.login.password = this.getValueOrDefault(value.Password); + cipher.login.uris = this.makeUriArray(value.URL); + + if (!this.isNullOrWhitespace(value.Title)) { + result.ciphers.push(cipher); + } + + if (addFolder) { + const f = new FolderView(); + f.name = groupName; + result.folders.push(f); + } + if (hasFolder) { + result.folderRelationships.set(cipherIndex, folderIndex); + } + }); + + return result; + } +} diff --git a/src/importers/lastpassCsvImporter.ts b/src/importers/lastpassCsvImporter.ts new file mode 100644 index 0000000000..38809d1d49 --- /dev/null +++ b/src/importers/lastpassCsvImporter.ts @@ -0,0 +1,232 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CardView } from '../models/view/cardView'; +import { CipherView } from '../models/view/cipherView'; +import { FolderView } from '../models/view/folderView'; +import { IdentityView } from '../models/view/identityView'; +import { LoginView } from '../models/view/loginView'; +import { SecureNoteView } from '../models/view/secureNoteView'; + +import { CipherType } from '../enums/cipherType'; +import { SecureNoteType } from '../enums/secureNoteType'; + +export class LastPassCsvImporter extends BaseImporter implements Importer { + import(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + let folderIndex = result.folders.length; + const cipherIndex = result.ciphers.length; + const hasFolder = this.getValueOrDefault(value.grouping, '(none)') !== '(none)'; + let addFolder = hasFolder; + + if (hasFolder) { + for (let i = 0; i < result.folders.length; i++) { + if (result.folders[i].name === value.grouping) { + addFolder = false; + folderIndex = i; + break; + } + } + } + + const cipher = this.buildBaseCipher(value); + if (cipher.type === CipherType.Login) { + cipher.notes = this.getValueOrDefault(value.extra); + cipher.login = new LoginView(); + cipher.login.uris = this.makeUriArray(value.url); + cipher.login.username = this.getValueOrDefault(value.username); + cipher.login.password = this.getValueOrDefault(value.password); + } else if (cipher.type === CipherType.SecureNote) { + this.parseSecureNote(value, cipher); + } else if (cipher.type === CipherType.Card) { + cipher.card = this.parseCard(value); + cipher.notes = this.getValueOrDefault(value.notes); + } else if (cipher.type === CipherType.Identity) { + cipher.identity = this.parseIdentity(value); + cipher.notes = this.getValueOrDefault(value.notes); + if (!this.isNullOrWhitespace(value.ccnum)) { + // there is a card on this identity too + const cardCipher = this.buildBaseCipher(value); + cardCipher.identity = null; + cardCipher.type = CipherType.Card; + cardCipher.card = this.parseCard(value); + result.ciphers.push(cardCipher); + } + } + + result.ciphers.push(cipher); + + if (addFolder) { + const f = new FolderView(); + f.name = value.grouping; + result.folders.push(f); + } + if (hasFolder) { + result.folderRelationships.set(cipherIndex, folderIndex); + } + }); + + return result; + } + + private buildBaseCipher(value: any) { + const cipher = new CipherView(); + if (value.hasOwnProperty('profilename') && value.hasOwnProperty('profilelanguage')) { + // form fill + cipher.favorite = false; + cipher.name = this.getValueOrDefault(value.profilename, '--'); + cipher.type = CipherType.Card; + + if (!this.isNullOrWhitespace(value.title) || !this.isNullOrWhitespace(value.firstname) || + !this.isNullOrWhitespace(value.lastname) || !this.isNullOrWhitespace(value.address1) || + !this.isNullOrWhitespace(value.phone) || !this.isNullOrWhitespace(value.username) || + !this.isNullOrWhitespace(value.email)) { + cipher.type = CipherType.Identity; + } + } else { + // site or secure note + cipher.favorite = this.getValueOrDefault(value.fav, '0') === '1'; // TODO: if org, always false + cipher.name = this.getValueOrDefault(value.name, '--'); + cipher.type = value.url === 'http://sn' ? CipherType.SecureNote : CipherType.Login; + } + return cipher; + } + + private parseCard(value: any): CardView { + const card = new CardView(); + card.cardholderName = this.getValueOrDefault(value.ccname); + card.number = this.getValueOrDefault(value.ccnum); + card.code = this.getValueOrDefault(value.cccsc); + card.brand = this.getCardBrand(value.ccnum); + + if (!this.isNullOrWhitespace(value.ccexp) && value.ccexp.indexOf('-') > -1) { + const ccexpParts = (value.ccexp as string).split('-'); + if (ccexpParts.length > 1) { + card.expYear = ccexpParts[0]; + card.expMonth = ccexpParts[1]; + if (card.expMonth.length === 2 && card.expMonth[0] === '0') { + card.expMonth = card.expMonth[1]; + } + } + } + + return card; + } + + private parseIdentity(value: any): IdentityView { + const identity = new IdentityView(); + identity.title = this.getValueOrDefault(value.title); + identity.firstName = this.getValueOrDefault(value.firstname); + identity.middleName = this.getValueOrDefault(value.middlename); + identity.lastName = this.getValueOrDefault(value.lastname); + identity.username = this.getValueOrDefault(value.username); + identity.company = this.getValueOrDefault(value.company); + identity.ssn = this.getValueOrDefault(value.ssn); + identity.address1 = this.getValueOrDefault(value.address1); + identity.address2 = this.getValueOrDefault(value.address2); + identity.address3 = this.getValueOrDefault(value.address3); + identity.city = this.getValueOrDefault(value.city); + identity.state = this.getValueOrDefault(value.state); + identity.postalCode = this.getValueOrDefault(value.zip); + identity.country = this.getValueOrDefault(value.country); + identity.email = this.getValueOrDefault(value.email); + identity.phone = this.getValueOrDefault(value.phone); + + if (!this.isNullOrWhitespace(identity.title)) { + identity.title = identity.title.charAt(0).toUpperCase() + identity.title.slice(1); + } + + return identity; + } + + private parseSecureNote(value: any, cipher: CipherView) { + const extraParts = this.splitNewLine(value.extra); + let processedNote = false; + + if (extraParts.length) { + const typeParts = extraParts[0].split(':'); + if (typeParts.length > 1 && typeParts[0] === 'NoteType' && + (typeParts[1] === 'Credit Card' || typeParts[1] === 'Address')) { + if (typeParts[1] === 'Credit Card') { + const mappedData = this.parseSecureNoteMapping(extraParts, { + 'Number': 'number', + 'Name on Card': 'cardholderName', + 'Security Code': 'code', + }); + cipher.type = CipherType.Card; + cipher.card = mappedData[0]; + cipher.notes = mappedData[1]; + } else if (typeParts[1] === 'Address') { + const mappedData = this.parseSecureNoteMapping(extraParts, { + 'Title': 'title', + 'First Name': 'firstName', + 'Last Name': 'lastName', + 'Middle Name': 'middleName', + 'Company': 'company', + 'Address 1': 'address1', + 'Address 2': 'address2', + 'Address 3': 'address3', + 'City / Town': 'city', + 'State': 'state', + 'Zip / Postal Code': 'postalCode', + 'Country': 'country', + 'Email Address': 'email', + 'Username': 'username', + }); + cipher.type = CipherType.Identity; + cipher.identity = mappedData[0]; + cipher.notes = mappedData[1]; + } + processedNote = true; + } + } + + if (!processedNote) { + cipher.secureNote = new SecureNoteView(); + cipher.secureNote.type = SecureNoteType.Generic; + cipher.notes = this.getValueOrDefault(value.extra); + } + } + + private parseSecureNoteMapping(extraParts: string[], map: any): [T, string] { + let notes: string = null; + const dataObj: any = {}; + + extraParts.forEach((extraPart) => { + const fieldParts = extraPart.split(':'); + if (fieldParts.length < 1 || this.isNullOrWhitespace(fieldParts[0]) || + this.isNullOrWhitespace(fieldParts[1]) || fieldParts[0] === 'NoteType') { + return; + } + + if (fieldParts[0] === 'Notes') { + if (!this.isNullOrWhitespace(notes)) { + notes += ('\n' + fieldParts[1]); + } else { + notes = fieldParts[1]; + } + } else if (map.hasOwnProperty(fieldParts[0])) { + dataObj[map[fieldParts[0]]] = fieldParts[1]; + } else { + if (!this.isNullOrWhitespace(notes)) { + notes += '\n'; + } else { + notes = ''; + } + + notes += (fieldParts[0] + ': ' + fieldParts[1]); + } + }); + + return [dataObj as T, notes]; + } +} diff --git a/src/models/domain/importResult.ts b/src/models/domain/importResult.ts new file mode 100644 index 0000000000..152d08462e --- /dev/null +++ b/src/models/domain/importResult.ts @@ -0,0 +1,13 @@ +import { CipherView } from '../view/cipherView'; +import { CollectionView } from '../view/collectionView'; +import { FolderView } from '../view/folderView'; + +export class ImportResult { + success = false; + errorMessage: string; + ciphers: CipherView[] = []; + folders: FolderView[] = []; + folderRelationships: Map = new Map(); + collections: CollectionView[] = []; + collectionRelationships: Map = new Map(); +} From ce40a803d85f90b5078ecd4d7c374714c43940c8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 23 Jun 2018 14:46:23 -0400 Subject: [PATCH 0310/1626] importers parse --- src/importers/baseImporter.ts | 4 ++-- src/importers/bitwardenCsvImporter.ts | 3 ++- src/importers/importer.ts | 2 +- src/importers/keepassxCsvImporter.ts | 3 ++- src/importers/lastpassCsvImporter.ts | 10 ++++++++-- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index c65813dadb..dccd7114e7 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -50,15 +50,15 @@ export abstract class BaseImporter { const result = papa.parse(data, { header: header, encoding: 'UTF-8', + skipEmptyLines: false, }); if (result.errors != null && result.errors.length > 0) { result.errors.forEach((e) => { // tslint:disable-next-line console.warn('Error parsing row ' + e.row + ': ' + e.message); }); - return null; } - return result.data; + return result.data && result.data.length > 0 ? result.data : null; } protected parseSingleRowCsv(rowData: string) { diff --git a/src/importers/bitwardenCsvImporter.ts b/src/importers/bitwardenCsvImporter.ts index ce3729468e..4643e17021 100644 --- a/src/importers/bitwardenCsvImporter.ts +++ b/src/importers/bitwardenCsvImporter.ts @@ -14,7 +14,7 @@ import { FieldType } from '../enums/fieldType'; import { SecureNoteType } from '../enums/secureNoteType'; export class BitwardenCsvImporter extends BaseImporter implements Importer { - import(data: string): ImportResult { + parse(data: string): ImportResult { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { @@ -104,6 +104,7 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer { } }); + result.success = true; return result; } } diff --git a/src/importers/importer.ts b/src/importers/importer.ts index 453f88440d..e6df305dde 100644 --- a/src/importers/importer.ts +++ b/src/importers/importer.ts @@ -1,5 +1,5 @@ import { ImportResult } from '../models/domain/importResult'; export interface Importer { - import(data: string): ImportResult; + parse(data: string): ImportResult; } diff --git a/src/importers/keepassxCsvImporter.ts b/src/importers/keepassxCsvImporter.ts index 30eee8f115..c8ab05ec9f 100644 --- a/src/importers/keepassxCsvImporter.ts +++ b/src/importers/keepassxCsvImporter.ts @@ -10,7 +10,7 @@ import { LoginView } from '../models/view/loginView'; import { CipherType } from '../enums/cipherType'; export class KeePassXCsvImporter extends BaseImporter implements Importer { - import(data: string): ImportResult { + parse(data: string): ImportResult { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { @@ -62,6 +62,7 @@ export class KeePassXCsvImporter extends BaseImporter implements Importer { } }); + result.success = true; return result; } } diff --git a/src/importers/lastpassCsvImporter.ts b/src/importers/lastpassCsvImporter.ts index 38809d1d49..1177686e5f 100644 --- a/src/importers/lastpassCsvImporter.ts +++ b/src/importers/lastpassCsvImporter.ts @@ -14,7 +14,7 @@ import { CipherType } from '../enums/cipherType'; import { SecureNoteType } from '../enums/secureNoteType'; export class LastPassCsvImporter extends BaseImporter implements Importer { - import(data: string): ImportResult { + parse(data: string): ImportResult { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { @@ -22,7 +22,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { return result; } - results.forEach((value) => { + results.forEach((value, index) => { let folderIndex = result.folders.length; const cipherIndex = result.ciphers.length; const hasFolder = this.getValueOrDefault(value.grouping, '(none)') !== '(none)'; @@ -39,6 +39,11 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { } const cipher = this.buildBaseCipher(value); + if (cipher.name === '--' && results.length > 2 && index >= (results.length - 2)) { + // LastPass file traditionally has two empty lines at the end + return; + } + if (cipher.type === CipherType.Login) { cipher.notes = this.getValueOrDefault(value.extra); cipher.login = new LoginView(); @@ -75,6 +80,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { } }); + result.success = true; return result; } From 1609ed54199769f7e03b19c2201039799cf2434f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 23 Jun 2018 15:41:22 -0400 Subject: [PATCH 0311/1626] import apis --- src/abstractions/api.service.ts | 4 ++ src/models/request/collectionRequest.ts | 9 ++++ src/models/request/importCiphersRequest.ts | 9 ++++ .../importOrganizationCiphersRequest.ts | 9 ++++ src/models/request/kvpRequest.ts | 9 ++++ src/services/api.service.ts | 44 +++++++++++++++++++ 6 files changed, 84 insertions(+) create mode 100644 src/models/request/collectionRequest.ts create mode 100644 src/models/request/importCiphersRequest.ts create mode 100644 src/models/request/importOrganizationCiphersRequest.ts create mode 100644 src/models/request/kvpRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 4267fbbdf2..fe8600ec38 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -9,7 +9,9 @@ import { CipherShareRequest } from '../models/request/cipherShareRequest'; import { EmailRequest } from '../models/request/emailRequest'; import { EmailTokenRequest } from '../models/request/emailTokenRequest'; import { FolderRequest } from '../models/request/folderRequest'; +import { ImportCiphersRequest } from '../models/request/importCiphersRequest'; import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; +import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest'; import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; @@ -56,6 +58,8 @@ export abstract class ApiService { putShareCiphers: (request: CipherBulkShareRequest) => Promise; putCipherCollections: (id: string, request: CipherCollectionsRequest) => Promise; postPurgeCiphers: (request: PasswordVerificationRequest) => Promise; + postImportCiphers: (request: ImportCiphersRequest) => Promise; + postImportOrganizationCiphers: (request: ImportOrganizationCiphersRequest) => Promise; postCipherAttachment: (id: string, data: FormData) => Promise; deleteCipherAttachment: (id: string, attachmentId: string) => Promise; postShareCipherAttachment: (id: string, attachmentId: string, data: FormData, diff --git a/src/models/request/collectionRequest.ts b/src/models/request/collectionRequest.ts new file mode 100644 index 0000000000..38e62a449f --- /dev/null +++ b/src/models/request/collectionRequest.ts @@ -0,0 +1,9 @@ +import { Collection } from '../domain/collection'; + +export class CollectionRequest { + name: string; + + constructor(collection: Collection) { + this.name = collection.name ? collection.name.encryptedString : null; + } +} diff --git a/src/models/request/importCiphersRequest.ts b/src/models/request/importCiphersRequest.ts new file mode 100644 index 0000000000..40f073d5f2 --- /dev/null +++ b/src/models/request/importCiphersRequest.ts @@ -0,0 +1,9 @@ +import { CipherRequest } from './cipherRequest'; +import { FolderRequest } from './folderRequest'; +import { KvpRequest } from './kvpRequest'; + +export class ImportCiphersRequest { + ciphers: CipherRequest[]; + folders: FolderRequest[]; + folderRelationships: Array>; +} diff --git a/src/models/request/importOrganizationCiphersRequest.ts b/src/models/request/importOrganizationCiphersRequest.ts new file mode 100644 index 0000000000..3a63693235 --- /dev/null +++ b/src/models/request/importOrganizationCiphersRequest.ts @@ -0,0 +1,9 @@ +import { CipherRequest } from './cipherRequest'; +import { CollectionRequest } from './collectionRequest'; +import { KvpRequest } from './kvpRequest'; + +export class ImportOrganizationCiphersRequest { + ciphers: CipherRequest[]; + collections: CollectionRequest[]; + collectionRelationships: Array>; +} diff --git a/src/models/request/kvpRequest.ts b/src/models/request/kvpRequest.ts new file mode 100644 index 0000000000..0611a4e01e --- /dev/null +++ b/src/models/request/kvpRequest.ts @@ -0,0 +1,9 @@ +export class KvpRequest { + key: TK; + value: TV; + + constructor(key: TK, value: TV) { + this.key = key; + this.value = value; + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index de68749643..c03025c281 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -15,7 +15,9 @@ import { CipherShareRequest } from '../models/request/cipherShareRequest'; import { EmailRequest } from '../models/request/emailRequest'; import { EmailTokenRequest } from '../models/request/emailTokenRequest'; import { FolderRequest } from '../models/request/folderRequest'; +import { ImportCiphersRequest } from '../models/request/importCiphersRequest'; import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; +import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest'; import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; @@ -607,6 +609,48 @@ export class ApiService implements ApiServiceAbstraction { } } + async postImportCiphers(request: ImportCiphersRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/import', { + body: JSON.stringify(request), + cache: 'no-cache', + credentials: this.getCredentials(), + headers: new Headers({ + 'Accept': 'application/json', + 'Content-Type': 'application/json; charset=utf-8', + 'Authorization': authHeader, + 'Device-Type': this.deviceType, + }), + method: 'POST', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + + async postImportOrganizationCiphers(request: ImportOrganizationCiphersRequest): Promise { + const authHeader = await this.handleTokenState(); + const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/import-organization', { + body: JSON.stringify(request), + cache: 'no-cache', + credentials: this.getCredentials(), + headers: new Headers({ + 'Accept': 'application/json', + 'Content-Type': 'application/json; charset=utf-8', + 'Authorization': authHeader, + 'Device-Type': this.deviceType, + }), + method: 'POST', + })); + + if (response.status !== 200) { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + // Attachments APIs async postCipherAttachment(id: string, data: FormData): Promise { From ca3b1c739c6a7ef503845a1485cc76643276a259 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 25 Jun 2018 08:06:19 -0400 Subject: [PATCH 0312/1626] get web vault url --- src/abstractions/environment.service.ts | 1 + src/services/environment.service.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/abstractions/environment.service.ts b/src/abstractions/environment.service.ts index 1f6bc390bc..8c27710f2c 100644 --- a/src/abstractions/environment.service.ts +++ b/src/abstractions/environment.service.ts @@ -5,6 +5,7 @@ export abstract class EnvironmentService { identityUrl: string; iconsUrl: string; + getWebVaultUrl: () => string; setUrlsFromStorage: () => Promise; setUrls: (urls: any) => Promise; } diff --git a/src/services/environment.service.ts b/src/services/environment.service.ts index e1544489f1..e41a27c54c 100644 --- a/src/services/environment.service.ts +++ b/src/services/environment.service.ts @@ -13,7 +13,15 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { identityUrl: string; iconsUrl: string; - constructor(private apiService: ApiService, private storageService: StorageService) { + constructor(private apiService: ApiService, private storageService: StorageService) {} + + getWebVaultUrl(): string { + if (this.webVaultUrl != null) { + return this.webVaultUrl; + } else if (this.baseUrl) { + return this.baseUrl; + } + return null; } async setUrlsFromStorage(): Promise { From 95337651fcb4d7fd56898cf14649497510e5d2a7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 25 Jun 2018 11:40:03 -0400 Subject: [PATCH 0313/1626] init arrays --- src/models/request/importCiphersRequest.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/models/request/importCiphersRequest.ts b/src/models/request/importCiphersRequest.ts index 40f073d5f2..cfa5df17ed 100644 --- a/src/models/request/importCiphersRequest.ts +++ b/src/models/request/importCiphersRequest.ts @@ -3,7 +3,7 @@ import { FolderRequest } from './folderRequest'; import { KvpRequest } from './kvpRequest'; export class ImportCiphersRequest { - ciphers: CipherRequest[]; - folders: FolderRequest[]; - folderRelationships: Array>; + ciphers: CipherRequest[] = []; + folders: FolderRequest[] = []; + folderRelationships: Array> = []; } From 8a91e5b4d63a0a8cf08b864119894e3bda5f1f4b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 25 Jun 2018 14:15:34 -0400 Subject: [PATCH 0314/1626] avira csv importer --- src/importers/aviraCsvImporter.ts | 41 +++++++++++++++++++++++++++++++ src/models/view/cipherView.ts | 6 ++--- 2 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 src/importers/aviraCsvImporter.ts diff --git a/src/importers/aviraCsvImporter.ts b/src/importers/aviraCsvImporter.ts new file mode 100644 index 0000000000..d704028624 --- /dev/null +++ b/src/importers/aviraCsvImporter.ts @@ -0,0 +1,41 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CipherView } from '../models/view/cipherView'; +import { LoginView } from '../models/view/loginView'; + +import { CipherType } from '../enums/cipherType'; + +export class AviraCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + const cipher = new CipherView(); + cipher.type = CipherType.Login; + cipher.name = this.getValueOrDefault(value.name, '--'); + cipher.login = new LoginView(); + cipher.login.uris = this.makeUriArray(value.website); + cipher.login.password = this.getValueOrDefault(value.password); + + if (this.isNullOrWhitespace(value.username) && !this.isNullOrWhitespace(value.secondary_username)) { + cipher.login.username = value.secondary_username; + } else { + cipher.login.username = this.getValueOrDefault(value.username); + cipher.notes = this.getValueOrDefault(value.secondary_username); + } + + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} diff --git a/src/models/view/cipherView.ts b/src/models/view/cipherView.ts index 2b5f1499f1..72e68eaced 100644 --- a/src/models/view/cipherView.ts +++ b/src/models/view/cipherView.ts @@ -17,9 +17,9 @@ export class CipherView implements View { name: string; notes: string; type: CipherType; - favorite: boolean; - organizationUseTotp: boolean; - edit: boolean; + favorite = false; + organizationUseTotp = false; + edit = false; localData: any; login: LoginView; identity: IdentityView; From 1aa774b99f73123b0bcf2654e4ba59fe95f39563 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 25 Jun 2018 14:54:47 -0400 Subject: [PATCH 0315/1626] update folder of items when folder is deleted --- src/services/cipher.service.ts | 5 ----- src/services/folder.service.ts | 25 +++++++++++++++++++++---- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 2544d4f1c4..e1d6152bf3 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -26,15 +26,10 @@ import { CipherResponse } from '../models/response/cipherResponse'; import { ErrorResponse } from '../models/response/errorResponse'; import { AttachmentView } from '../models/view/attachmentView'; -import { CardView } from '../models/view/cardView'; import { CipherView } from '../models/view/cipherView'; import { FieldView } from '../models/view/fieldView'; -import { IdentityView } from '../models/view/identityView'; -import { LoginView } from '../models/view/loginView'; import { View } from '../models/view/view'; -import { ConstantsService } from './constants.service'; - import { ApiService } from '../abstractions/api.service'; import { CipherService as CipherServiceAbstraction } from '../abstractions/cipher.service'; import { CryptoService } from '../abstractions/crypto.service'; diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index d4806e0c0f..51ec8e4bb8 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -9,23 +9,25 @@ import { FolderResponse } from '../models/response/folderResponse'; import { FolderView } from '../models/view/folderView'; import { ApiService } from '../abstractions/api.service'; +import { CipherService } from '../abstractions/cipher.service'; import { CryptoService } from '../abstractions/crypto.service'; import { FolderService as FolderServiceAbstraction } from '../abstractions/folder.service'; import { I18nService } from '../abstractions/i18n.service'; import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; +import { CipherData } from '../models/data/cipherData'; const Keys = { foldersPrefix: 'folders_', + ciphersPrefix: 'ciphers_', }; export class FolderService implements FolderServiceAbstraction { decryptedFolderCache: FolderView[]; constructor(private cryptoService: CryptoService, private userService: UserService, - private noneFolder: () => string, private apiService: ApiService, - private storageService: StorageService, private i18nService: I18nService) { - } + private apiService: ApiService, private storageService: StorageService, + private i18nService: I18nService, private cipherService: CipherService) { } clearCache(): void { this.decryptedFolderCache = null; @@ -83,7 +85,7 @@ export class FolderService implements FolderServiceAbstraction { decFolders.sort(this.getLocaleSortingFunction()); const noneFolder = new FolderView(); - noneFolder.name = this.noneFolder(); + noneFolder.name = this.i18nService.t('noneFolder'); decFolders.push(noneFolder); this.decryptedFolderCache = decFolders; @@ -157,6 +159,21 @@ export class FolderService implements FolderServiceAbstraction { await this.storageService.save(Keys.foldersPrefix + userId, folders); this.decryptedFolderCache = null; + + // Items in a deleted folder are re-assigned to "No Folder" + const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>(Keys.ciphersPrefix + userId); + if (ciphers != null) { + const updates: CipherData[] = []; + for (const cId in ciphers) { + if (ciphers[cId].folderId === id) { + ciphers[cId].folderId = null; + updates.push(ciphers[cId]); + } + } + if (updates.length > 0) { + this.cipherService.upsert(updates); + } + } } async deleteWithServer(id: string): Promise { From 0d2cd4c482c19ee3385677e957bbd99ae2eafbb6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 25 Jun 2018 15:19:51 -0400 Subject: [PATCH 0316/1626] name from url --- src/importers/aviraCsvImporter.ts | 3 ++- src/importers/baseImporter.ts | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/importers/aviraCsvImporter.ts b/src/importers/aviraCsvImporter.ts index d704028624..bb10ad3927 100644 --- a/src/importers/aviraCsvImporter.ts +++ b/src/importers/aviraCsvImporter.ts @@ -20,7 +20,8 @@ export class AviraCsvImporter extends BaseImporter implements Importer { results.forEach((value) => { const cipher = new CipherView(); cipher.type = CipherType.Login; - cipher.name = this.getValueOrDefault(value.name, '--'); + cipher.name = this.getValueOrDefault(value.name, + this.getValueOrDefault(this.nameFromUrl(value.website), '--')); cipher.login = new LoginView(); cipher.login.uris = this.makeUriArray(value.website); cipher.login.password = this.getValueOrDefault(value.password); diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index dccd7114e7..c1a245e510 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -2,6 +2,8 @@ import * as papa from 'papaparse'; import { LoginUriView } from '../models/view/loginUriView'; +import { Utils } from '../misc/utils'; + export abstract class BaseImporter { protected passwordFieldNames = [ 'password', 'pass word', 'passphrase', 'pass phrase', @@ -112,6 +114,14 @@ export abstract class BaseImporter { return uri; } + protected nameFromUrl(url: string) { + const hostname = Utils.getHostname(url); + if (this.isNullOrWhitespace(hostname)) { + return null; + } + return hostname.startsWith('www.') ? hostname.replace('www.', '') : hostname; + } + protected isNullOrWhitespace(str: string): boolean { return str == null || str.trim() === ''; } From 79fd5d98323bade873d8e474f0fdee8dc6cf7a63 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 25 Jun 2018 22:42:57 -0400 Subject: [PATCH 0317/1626] send api helper --- src/services/api.service.ts | 655 ++++++------------------------------ 1 file changed, 101 insertions(+), 554 deletions(-) diff --git a/src/services/api.service.ts b/src/services/api.service.ts index c03025c281..e784a71df1 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -121,647 +121,194 @@ export class ApiService implements ApiServiceAbstraction { // Two Factor APIs - async postTwoFactorEmail(request: TwoFactorEmailRequest): Promise { - const response = await fetch(new Request(this.apiBaseUrl + '/two-factor/send-email-login', { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + postTwoFactorEmail(request: TwoFactorEmailRequest): Promise { + return this.send('POST', '/two-factor/send-email-login', request, false, false); } // Account APIs async getProfile(): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/accounts/profile', { - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - })); - - if (response.status === 200) { - const responseJson = await response.json(); - return new ProfileResponse(responseJson); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + const r = await this.send('GET', '/accounts/profile', null, true, true); + return new ProfileResponse(r); } async putProfile(request: UpdateProfileRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/accounts/profile', { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Content-Type': 'application/json; charset=utf-8', - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - method: 'PUT', - })); - - if (response.status === 200) { - const responseJson = await response.json(); - return new ProfileResponse(responseJson); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + const r = await this.send('PUT', '/accounts/profile', request, true, true); + return new ProfileResponse(r); } - async postEmailToken(request: EmailTokenRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/accounts/email-token', { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Content-Type': 'application/json; charset=utf-8', - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + postEmailToken(request: EmailTokenRequest): Promise { + return this.send('POST', '/accounts/email-token', request, true, false); } - async postEmail(request: EmailRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/accounts/email', { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Content-Type': 'application/json; charset=utf-8', - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + postEmail(request: EmailRequest): Promise { + return this.send('POST', '/accounts/email', request, true, false); } - async postPassword(request: PasswordRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/accounts/password', { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Content-Type': 'application/json; charset=utf-8', - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + postPassword(request: PasswordRequest): Promise { + return this.send('POST', '/accounts/password', request, true, false); } - async postSecurityStamp(request: PasswordVerificationRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/accounts/security-stamp', { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Content-Type': 'application/json; charset=utf-8', - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + postSecurityStamp(request: PasswordVerificationRequest): Promise { + return this.send('POST', '/accounts/security-stamp', request, true, false); } - async postDeleteAccount(request: PasswordVerificationRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/accounts/delete', { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Content-Type': 'application/json; charset=utf-8', - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + postDeleteAccount(request: PasswordVerificationRequest): Promise { + return this.send('POST', '/accounts/delete', request, true, false); } async getAccountRevisionDate(): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/accounts/revision-date', { - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - })); - - if (response.status === 200) { - return (await response.json() as number); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + const r = await this.send('GET', '/accounts/revision-date', null, true, true); + return r as number; } - async postPasswordHint(request: PasswordHintRequest): Promise { - const response = await fetch(new Request(this.apiBaseUrl + '/accounts/password-hint', { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + postPasswordHint(request: PasswordHintRequest): Promise { + return this.send('POST', '/accounts/password-hint', request, false, false); } - async postRegister(request: RegisterRequest): Promise { - const response = await fetch(new Request(this.apiBaseUrl + '/accounts/register', { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + postRegister(request: RegisterRequest): Promise { + return this.send('POST', '/accounts/register', request, false, false); } // Folder APIs async postFolder(request: FolderRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/folders', { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status === 200) { - const responseJson = await response.json(); - return new FolderResponse(responseJson); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + const r = await this.send('POST', '/folders', request, true, true); + return new FolderResponse(r); } async putFolder(id: string, request: FolderRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/folders/' + id, { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.deviceType, - }), - method: 'PUT', - })); - - if (response.status === 200) { - const responseJson = await response.json(); - return new FolderResponse(responseJson); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + const r = await this.send('PUT', '/folders/' + id, request, true, true); + return new FolderResponse(r); } - async deleteFolder(id: string): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/folders/' + id, { - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - method: 'DELETE', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + deleteFolder(id: string): Promise { + return this.send('DELETE', '/folders/' + id, null, true, false); } // Cipher APIs async postCipher(request: CipherRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/ciphers', { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status === 200) { - const responseJson = await response.json(); - return new CipherResponse(responseJson); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + const r = await this.send('POST', '/ciphers', request, true, true); + return new CipherResponse(r); } async putCipher(id: string, request: CipherRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/' + id, { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.deviceType, - }), - method: 'PUT', - })); - - if (response.status === 200) { - const responseJson = await response.json(); - return new CipherResponse(responseJson); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + const r = await this.send('PUT', '/ciphers/' + id, request, true, true); + return new CipherResponse(r); } - async deleteCipher(id: string): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/' + id, { - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - method: 'DELETE', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + deleteCipher(id: string): Promise { + return this.send('DELETE', '/ciphers/' + id, null, true, false); } - async deleteManyCiphers(request: CipherBulkDeleteRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/ciphers', { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.deviceType, - }), - method: 'DELETE', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + deleteManyCiphers(request: CipherBulkDeleteRequest): Promise { + return this.send('DELETE', '/ciphers', request, true, false); } - async putMoveCiphers(request: CipherBulkMoveRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/move', { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.deviceType, - }), - method: 'PUT', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + putMoveCiphers(request: CipherBulkMoveRequest): Promise { + return this.send('PUT', '/ciphers/move', request, true, false); } - async putShareCipher(id: string, request: CipherShareRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/' + id + '/share', { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.deviceType, - }), - method: 'PUT', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + putShareCipher(id: string, request: CipherShareRequest): Promise { + return this.send('PUT', '/ciphers/' + id + '/share', request, true, false); } - async putShareCiphers(request: CipherBulkShareRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/share', { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.deviceType, - }), - method: 'PUT', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + putShareCiphers(request: CipherBulkShareRequest): Promise { + return this.send('PUT', '/ciphers/share', request, true, false); } - async putCipherCollections(id: string, request: CipherCollectionsRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/' + id + '/collections', { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.deviceType, - }), - method: 'PUT', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + putCipherCollections(id: string, request: CipherCollectionsRequest): Promise { + return this.send('PUT', '/ciphers/' + id + '/collections', request, true, false); } - async postPurgeCiphers(request: PasswordVerificationRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/purge', { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Content-Type': 'application/json; charset=utf-8', - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + postPurgeCiphers(request: PasswordVerificationRequest): Promise { + return this.send('POST', '/ciphers/purge', request, true, false); } - async postImportCiphers(request: ImportCiphersRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/import', { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Content-Type': 'application/json; charset=utf-8', - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + postImportCiphers(request: ImportCiphersRequest): Promise { + return this.send('POST', '/ciphers/import', request, true, false); } - async postImportOrganizationCiphers(request: ImportOrganizationCiphersRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/import-organization', { - body: JSON.stringify(request), - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Content-Type': 'application/json; charset=utf-8', - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + postImportOrganizationCiphers(request: ImportOrganizationCiphersRequest): Promise { + return this.send('POST', '/ciphers/import-organization', request, true, false); } // Attachments APIs async postCipherAttachment(id: string, data: FormData): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/' + id + '/attachment', { - body: data, - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status === 200) { - const responseJson = await response.json(); - return new CipherResponse(responseJson); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + const r = await this.send('POST', '/ciphers/' + id + '/attachment', data, true, true); + return new CipherResponse(r); } - async deleteCipherAttachment(id: string, attachmentId: string): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/' + id + '/attachment/' + attachmentId, { - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - method: 'DELETE', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + deleteCipherAttachment(id: string, attachmentId: string): Promise { + return this.send('DELETE', '/ciphers/' + id + '/attachment/' + attachmentId, null, true, false); } - async postShareCipherAttachment(id: string, attachmentId: string, data: FormData, + postShareCipherAttachment(id: string, attachmentId: string, data: FormData, organizationId: string): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/ciphers/' + id + '/attachment/' + - attachmentId + '/share?organizationId=' + organizationId, { - body: data, - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + return this.send('POST', '/ciphers/' + id + '/attachment/' + + attachmentId + '/share?organizationId=' + organizationId, data, true, false); } // Sync APIs async getSync(): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/sync', { - cache: 'no-cache', - credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - })); - - if (response.status === 200) { - const responseJson = await response.json(); - return new SyncResponse(responseJson); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } + const r = await this.send('GET', '/sync', null, true, true); + return new SyncResponse(r); } async postImportDirectory(organizationId: string, request: ImportDirectoryRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.apiBaseUrl + '/organizations/' + organizationId + '/import', { - body: JSON.stringify(request), + return this.send('POST', '/organizations/' + organizationId + '/import', request, true, false); + } + + // Helpers + + private async send(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body: any, + authed: boolean, hasResponse: boolean): Promise { + const headers = new Headers({ + 'Device-Type': this.deviceType, + }); + + const requestInit: RequestInit = { cache: 'no-cache', credentials: this.getCredentials(), - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); + method: method, + }; - if (response.status !== 200) { + if (authed) { + const authHeader = await this.handleTokenState(); + headers.set('Authorization', authHeader); + } + if (body != null) { + if (typeof body === 'string') { + requestInit.body = body; + headers.set('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8'); + } else if (typeof body === 'object') { + if (body instanceof FormData) { + requestInit.body = body; + } else { + headers.set('Content-Type', 'application/json; charset=utf-8'); + requestInit.body = JSON.stringify(body); + } + } + } + if (hasResponse) { + headers.set('Accept', 'application/json'); + } + + requestInit.headers = headers; + const response = await fetch(new Request(this.apiBaseUrl + path, requestInit)); + + if (hasResponse && response.status === 200) { + const responseJson = await response.json(); + return responseJson; + } else if (response.status !== 200) { const error = await this.handleError(response, false); return Promise.reject(error); } } - // Helpers - private async handleError(response: Response, tokenError: boolean): Promise { if ((tokenError && response.status === 400) || response.status === 401 || response.status === 403) { await this.logoutCallback(true); From 6ae6a79f43c105f5a48b7eee230b1e1a673cfc86 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 25 Jun 2018 22:47:01 -0400 Subject: [PATCH 0318/1626] exclude domains from sync on web and desktop --- src/services/api.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/services/api.service.ts b/src/services/api.service.ts index e784a71df1..f222244e45 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -41,6 +41,7 @@ export class ApiService implements ApiServiceAbstraction { private deviceType: string; private isWebClient = false; + private isDesktopClient = false; private usingBaseUrl = false; constructor(private tokenService: TokenService, private platformUtilsService: PlatformUtilsService, @@ -48,6 +49,8 @@ export class ApiService implements ApiServiceAbstraction { const device = platformUtilsService.getDevice(); this.deviceType = device.toString(); this.isWebClient = device === DeviceType.Web; + this.isDesktopClient = device === DeviceType.Windows || device === DeviceType.MacOs || + device === DeviceType.Linux; } setUrls(urls: EnvironmentUrls): void { @@ -254,7 +257,8 @@ export class ApiService implements ApiServiceAbstraction { // Sync APIs async getSync(): Promise { - const r = await this.send('GET', '/sync', null, true, true); + const path = this.isDesktopClient || this.isWebClient ? '/sync?excludeDomains=true' : '/sync'; + const r = await this.send('GET', path, null, true, true); return new SyncResponse(r); } From 3cf8ffab8d4b7db1efa35e6ad406d3e11948597e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 25 Jun 2018 23:04:59 -0400 Subject: [PATCH 0319/1626] add domain rules apis --- src/models/request/updateDomainsRequest.ts | 4 ++++ src/services/api.service.ts | 14 ++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 src/models/request/updateDomainsRequest.ts diff --git a/src/models/request/updateDomainsRequest.ts b/src/models/request/updateDomainsRequest.ts new file mode 100644 index 0000000000..d624369dd2 --- /dev/null +++ b/src/models/request/updateDomainsRequest.ts @@ -0,0 +1,4 @@ +export class UpdateDomainsRequest { + equivalentDomains: string[][]; + excludedGlobalEquivalentDomains: number[]; +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index f222244e45..cf8fafe11a 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -24,9 +24,11 @@ import { PasswordVerificationRequest } from '../models/request/passwordVerificat import { RegisterRequest } from '../models/request/registerRequest'; import { TokenRequest } from '../models/request/tokenRequest'; import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; +import { UpdateDomainsRequest } from '../models/request/updateDomainsRequest'; import { UpdateProfileRequest } from '../models/request/updateProfileRequest'; import { CipherResponse } from '../models/response/cipherResponse'; +import { DomainsResponse } from '../models/response/domainsResponse'; import { ErrorResponse } from '../models/response/errorResponse'; import { FolderResponse } from '../models/response/folderResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; @@ -266,6 +268,18 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/organizations/' + organizationId + '/import', request, true, false); } + // Settings + + async getSettingsDomains(): Promise { + const r = await this.send('GET', '/settings/domains', null, true, true); + return new DomainsResponse(r); + } + + async putSettingsDomains(request: UpdateDomainsRequest): Promise { + const r = await this.send('PUT', '/settings/domains', request, true, true); + return new DomainsResponse(r); + } + // Helpers private async send(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body: any, From 32a636e5a5068f705b167f8f16dd15b53493b821 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 25 Jun 2018 23:09:32 -0400 Subject: [PATCH 0320/1626] add settings domains apis to abstraction --- src/abstractions/api.service.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index fe8600ec38..6db188686b 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -18,9 +18,11 @@ import { PasswordVerificationRequest } from '../models/request/passwordVerificat import { RegisterRequest } from '../models/request/registerRequest'; import { TokenRequest } from '../models/request/tokenRequest'; import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; +import { UpdateDomainsRequest } from '../models/request/updateDomainsRequest'; import { UpdateProfileRequest } from '../models/request/updateProfileRequest'; import { CipherResponse } from '../models/response/cipherResponse'; +import { DomainsResponse } from '../models/response/domainsResponse'; import { FolderResponse } from '../models/response/folderResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; @@ -66,4 +68,6 @@ export abstract class ApiService { organizationId: string) => Promise; getSync: () => Promise; postImportDirectory: (organizationId: string, request: ImportDirectoryRequest) => Promise; + getSettingsDomains: () => Promise; + putSettingsDomains: (request: UpdateDomainsRequest) => Promise; } From c3b6baf72670dda16f6edefaee36893023c3dced Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 26 Jun 2018 15:17:14 -0400 Subject: [PATCH 0321/1626] add two factor apis --- src/abstractions/api.service.ts | 30 +++++++ src/models/request/twoFactorEmailRequest.ts | 8 +- .../request/twoFactorProviderRequest.ts | 7 ++ .../request/twoFactorRecoveryRequest.ts | 6 ++ .../updateTwoFactorAuthenticatorRequest.ts | 6 ++ .../request/updateTwoFactorDuoRequest.ts | 7 ++ .../request/updateTwoFactorEmailRequest.ts | 6 ++ .../request/updateTwoFactorU2fRequest.ts | 5 ++ .../request/updateTwoFactorYubioOtpRequest.ts | 10 +++ src/models/response/listResponse.ts | 10 ++- .../twoFactorAuthenticatorResponse.ts | 9 ++ src/models/response/twoFactorDuoResponse.ts | 13 +++ src/models/response/twoFactorEmailResponse.ts | 9 ++ .../response/twoFactorProviderResponse.ts | 11 +++ .../response/twoFactorRescoverResponse.ts | 7 ++ src/models/response/twoFactorU2fResponse.ts | 23 +++++ .../response/twoFactorYubiKeyResponse.ts | 19 ++++ src/services/api.service.ts | 87 +++++++++++++++++++ tslint.json | 3 +- 19 files changed, 268 insertions(+), 8 deletions(-) create mode 100644 src/models/request/twoFactorProviderRequest.ts create mode 100644 src/models/request/twoFactorRecoveryRequest.ts create mode 100644 src/models/request/updateTwoFactorAuthenticatorRequest.ts create mode 100644 src/models/request/updateTwoFactorDuoRequest.ts create mode 100644 src/models/request/updateTwoFactorEmailRequest.ts create mode 100644 src/models/request/updateTwoFactorU2fRequest.ts create mode 100644 src/models/request/updateTwoFactorYubioOtpRequest.ts create mode 100644 src/models/response/twoFactorAuthenticatorResponse.ts create mode 100644 src/models/response/twoFactorDuoResponse.ts create mode 100644 src/models/response/twoFactorEmailResponse.ts create mode 100644 src/models/response/twoFactorProviderResponse.ts create mode 100644 src/models/response/twoFactorRescoverResponse.ts create mode 100644 src/models/response/twoFactorU2fResponse.ts create mode 100644 src/models/response/twoFactorYubiKeyResponse.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 6db188686b..d6f9447b35 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -18,16 +18,31 @@ import { PasswordVerificationRequest } from '../models/request/passwordVerificat import { RegisterRequest } from '../models/request/registerRequest'; import { TokenRequest } from '../models/request/tokenRequest'; import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; +import { TwoFactorProviderRequest } from '../models/request/twoFactorProviderRequest'; +import { TwoFactorRecoveryRequest } from '../models/request/twoFactorRecoveryRequest'; import { UpdateDomainsRequest } from '../models/request/updateDomainsRequest'; import { UpdateProfileRequest } from '../models/request/updateProfileRequest'; +import { UpdateTwoFactorAuthenticatorRequest } from '../models/request/updateTwoFactorAuthenticatorRequest'; +import { UpdateTwoFactorDuoRequest } from '../models/request/updateTwoFactorDuoRequest'; +import { UpdateTwoFactorEmailRequest } from '../models/request/updateTwoFactorEmailRequest'; +import { UpdateTwoFactorU2fRequest } from '../models/request/updateTwoFactorU2fRequest'; +import { UpdateTwoFactorYubioOtpRequest } from '../models/request/updateTwoFactorYubioOtpRequest'; import { CipherResponse } from '../models/response/cipherResponse'; import { DomainsResponse } from '../models/response/domainsResponse'; import { FolderResponse } from '../models/response/folderResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; +import { ListResponse } from '../models/response/listResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SyncResponse } from '../models/response/syncResponse'; +import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; +import { TwoFactorDuoResponse } from '../models/response/twoFactorDuoResponse'; +import { TwoFactorEmailResponse } from '../models/response/twoFactorEmailResponse'; +import { TwoFactorProviderResponse } from '../models/response/twoFactorProviderResponse'; +import { TwoFactorRecoverResponse } from '../models/response/twoFactorRescoverResponse'; +import { TwoFactorU2fResponse } from '../models/response/twoFactorU2fResponse'; +import { TwoFactorYubiKeyResponse } from '../models/response/twoFactorYubiKeyResponse'; export abstract class ApiService { urlsSet: boolean; @@ -70,4 +85,19 @@ export abstract class ApiService { postImportDirectory: (organizationId: string, request: ImportDirectoryRequest) => Promise; getSettingsDomains: () => Promise; putSettingsDomains: (request: UpdateDomainsRequest) => Promise; + getTwoFactorProviders: () => Promise>; + getTwoFactorAuthenticator: (request: PasswordVerificationRequest) => Promise; + getTwoFactorEmail: (request: PasswordVerificationRequest) => Promise; + getTwoFactorDuo: (request: PasswordVerificationRequest) => Promise; + getTwoFactorYubiKey: (request: PasswordVerificationRequest) => Promise; + getTwoFactorU2f: (request: PasswordVerificationRequest) => Promise; + getTwoFactorRecover: (request: PasswordVerificationRequest) => Promise; + putTwoFactorAuthenticator: ( + request: UpdateTwoFactorAuthenticatorRequest) => Promise; + putTwoFactorEmail: (request: UpdateTwoFactorEmailRequest) => Promise; + putTwoFactorDuo: (request: UpdateTwoFactorDuoRequest) => Promise; + putTwoFactorYubiKey: (request: UpdateTwoFactorYubioOtpRequest) => Promise; + putTwoFactorU2f: (request: UpdateTwoFactorU2fRequest) => Promise; + putTwoFactorDisable: (request: TwoFactorProviderRequest) => Promise; + postTwoFactorRecover: (request: TwoFactorRecoveryRequest) => Promise; } diff --git a/src/models/request/twoFactorEmailRequest.ts b/src/models/request/twoFactorEmailRequest.ts index b506ccfa43..ced9288ffd 100644 --- a/src/models/request/twoFactorEmailRequest.ts +++ b/src/models/request/twoFactorEmailRequest.ts @@ -1,9 +1,11 @@ -export class TwoFactorEmailRequest { +import { PasswordVerificationRequest } from './passwordVerificationRequest'; + +export class TwoFactorEmailRequest extends PasswordVerificationRequest { email: string; - masterPasswordHash: string; constructor(email: string, masterPasswordHash: string) { - this.email = email; + super(); this.masterPasswordHash = masterPasswordHash; + this.email = email; } } diff --git a/src/models/request/twoFactorProviderRequest.ts b/src/models/request/twoFactorProviderRequest.ts new file mode 100644 index 0000000000..23a47f9e0e --- /dev/null +++ b/src/models/request/twoFactorProviderRequest.ts @@ -0,0 +1,7 @@ +import { PasswordVerificationRequest } from './passwordVerificationRequest'; + +import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; + +export class TwoFactorProviderRequest extends PasswordVerificationRequest { + type: TwoFactorProviderType; +} diff --git a/src/models/request/twoFactorRecoveryRequest.ts b/src/models/request/twoFactorRecoveryRequest.ts new file mode 100644 index 0000000000..ddd7c59ad7 --- /dev/null +++ b/src/models/request/twoFactorRecoveryRequest.ts @@ -0,0 +1,6 @@ +import { PasswordVerificationRequest } from './passwordVerificationRequest'; + +export class TwoFactorRecoveryRequest extends PasswordVerificationRequest { + recoveryCode: string; + email: string; +} diff --git a/src/models/request/updateTwoFactorAuthenticatorRequest.ts b/src/models/request/updateTwoFactorAuthenticatorRequest.ts new file mode 100644 index 0000000000..2dd6a96ac9 --- /dev/null +++ b/src/models/request/updateTwoFactorAuthenticatorRequest.ts @@ -0,0 +1,6 @@ +import { PasswordVerificationRequest } from './passwordVerificationRequest'; + +export class UpdateTwoFactorAuthenticatorRequest extends PasswordVerificationRequest { + token: string; + key: string; +} diff --git a/src/models/request/updateTwoFactorDuoRequest.ts b/src/models/request/updateTwoFactorDuoRequest.ts new file mode 100644 index 0000000000..1777f39ae8 --- /dev/null +++ b/src/models/request/updateTwoFactorDuoRequest.ts @@ -0,0 +1,7 @@ +import { PasswordVerificationRequest } from './passwordVerificationRequest'; + +export class UpdateTwoFactorDuoRequest extends PasswordVerificationRequest { + integrationKey: string; + secretKey: string; + host: string; +} diff --git a/src/models/request/updateTwoFactorEmailRequest.ts b/src/models/request/updateTwoFactorEmailRequest.ts new file mode 100644 index 0000000000..dabeb7d9d3 --- /dev/null +++ b/src/models/request/updateTwoFactorEmailRequest.ts @@ -0,0 +1,6 @@ +import { PasswordVerificationRequest } from './passwordVerificationRequest'; + +export class UpdateTwoFactorEmailRequest extends PasswordVerificationRequest { + token: string; + email: string; +} diff --git a/src/models/request/updateTwoFactorU2fRequest.ts b/src/models/request/updateTwoFactorU2fRequest.ts new file mode 100644 index 0000000000..a0e959bd18 --- /dev/null +++ b/src/models/request/updateTwoFactorU2fRequest.ts @@ -0,0 +1,5 @@ +import { PasswordVerificationRequest } from './passwordVerificationRequest'; + +export class UpdateTwoFactorU2fRequest extends PasswordVerificationRequest { + deviceResponse: string; +} diff --git a/src/models/request/updateTwoFactorYubioOtpRequest.ts b/src/models/request/updateTwoFactorYubioOtpRequest.ts new file mode 100644 index 0000000000..4ad0696f0c --- /dev/null +++ b/src/models/request/updateTwoFactorYubioOtpRequest.ts @@ -0,0 +1,10 @@ +import { PasswordVerificationRequest } from './passwordVerificationRequest'; + +export class UpdateTwoFactorYubioOtpRequest extends PasswordVerificationRequest { + key1: string; + key2: string; + key3: string; + key4: string; + key5: string; + nfc: boolean; +} diff --git a/src/models/response/listResponse.ts b/src/models/response/listResponse.ts index 4a9715c39c..84c2c11632 100644 --- a/src/models/response/listResponse.ts +++ b/src/models/response/listResponse.ts @@ -1,7 +1,9 @@ -export class ListResponse { - data: any; +export class ListResponse { + data: T[]; + continuationToken: string; - constructor(data: any) { - this.data = data; + constructor(response: any, t: new (dataResponse: any) => T) { + this.data = response.Data.map((dr) => new t(dr)); + this.continuationToken = response.ContinuationToken; } } diff --git a/src/models/response/twoFactorAuthenticatorResponse.ts b/src/models/response/twoFactorAuthenticatorResponse.ts new file mode 100644 index 0000000000..f84509bd93 --- /dev/null +++ b/src/models/response/twoFactorAuthenticatorResponse.ts @@ -0,0 +1,9 @@ +export class TwoFactorAuthenticatorResponse { + enabled: boolean; + key: string; + + constructor(response: any) { + this.enabled = response.Enabled; + this.key = response.Key; + } +} diff --git a/src/models/response/twoFactorDuoResponse.ts b/src/models/response/twoFactorDuoResponse.ts new file mode 100644 index 0000000000..a72b32b364 --- /dev/null +++ b/src/models/response/twoFactorDuoResponse.ts @@ -0,0 +1,13 @@ +export class TwoFactorDuoResponse { + enabled: boolean; + host: string; + secretKey: string; + integrationKey: string; + + constructor(response: any) { + this.enabled = response.Enabled; + this.host = response.Host; + this.secretKey = response.SecretKey; + this.integrationKey = response.IntegrationKey; + } +} diff --git a/src/models/response/twoFactorEmailResponse.ts b/src/models/response/twoFactorEmailResponse.ts new file mode 100644 index 0000000000..eb8840a6ec --- /dev/null +++ b/src/models/response/twoFactorEmailResponse.ts @@ -0,0 +1,9 @@ +export class TwoFactorEmailResponse { + enabled: boolean; + email: string; + + constructor(response: any) { + this.enabled = response.Enabled; + this.email = response.Email; + } +} diff --git a/src/models/response/twoFactorProviderResponse.ts b/src/models/response/twoFactorProviderResponse.ts new file mode 100644 index 0000000000..ff834f9a25 --- /dev/null +++ b/src/models/response/twoFactorProviderResponse.ts @@ -0,0 +1,11 @@ +import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; + +export class TwoFactorProviderResponse { + enabled: boolean; + type: TwoFactorProviderType; + + constructor(response: any) { + this.enabled = response.Enabled; + this.type = response.Type; + } +} diff --git a/src/models/response/twoFactorRescoverResponse.ts b/src/models/response/twoFactorRescoverResponse.ts new file mode 100644 index 0000000000..0f73778083 --- /dev/null +++ b/src/models/response/twoFactorRescoverResponse.ts @@ -0,0 +1,7 @@ +export class TwoFactorRecoverResponse { + code: string; + + constructor(response: any) { + this.code = response.Code; + } +} diff --git a/src/models/response/twoFactorU2fResponse.ts b/src/models/response/twoFactorU2fResponse.ts new file mode 100644 index 0000000000..2c98c48fa8 --- /dev/null +++ b/src/models/response/twoFactorU2fResponse.ts @@ -0,0 +1,23 @@ +export class TwoFactorU2fResponse { + enabled: boolean; + challenge: Challenge; + + constructor(response: any) { + this.enabled = response.Enabled; + this.challenge = new Challenge(response.Challenge); + } +} + +class Challenge { + userId: string; + appId: string; + challenge: string; + version: string; + + constructor(response: any) { + this.userId = response.UserId; + this.appId = response.AppId; + this.challenge = response.Challenge; + this.version = response.Version; + } +} diff --git a/src/models/response/twoFactorYubiKeyResponse.ts b/src/models/response/twoFactorYubiKeyResponse.ts new file mode 100644 index 0000000000..454d4b9919 --- /dev/null +++ b/src/models/response/twoFactorYubiKeyResponse.ts @@ -0,0 +1,19 @@ +export class TwoFactorYubiKeyResponse { + enabled: boolean; + key1: string; + key2: string; + key3: string; + key4: string; + key5: string; + nfc: boolean; + + constructor(response: any) { + this.enabled = response.Enabled; + this.key1 = response.Key1; + this.key2 = response.Key2; + this.key3 = response.Key3; + this.key4 = response.Key4; + this.key5 = response.Key5; + this.nfc = response.Nfc; + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index cf8fafe11a..75b23c9ca7 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -24,8 +24,15 @@ import { PasswordVerificationRequest } from '../models/request/passwordVerificat import { RegisterRequest } from '../models/request/registerRequest'; import { TokenRequest } from '../models/request/tokenRequest'; import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; +import { TwoFactorProviderRequest } from '../models/request/twoFactorProviderRequest'; +import { TwoFactorRecoveryRequest } from '../models/request/twoFactorRecoveryRequest'; import { UpdateDomainsRequest } from '../models/request/updateDomainsRequest'; import { UpdateProfileRequest } from '../models/request/updateProfileRequest'; +import { UpdateTwoFactorAuthenticatorRequest } from '../models/request/updateTwoFactorAuthenticatorRequest'; +import { UpdateTwoFactorDuoRequest } from '../models/request/updateTwoFactorDuoRequest'; +import { UpdateTwoFactorEmailRequest } from '../models/request/updateTwoFactorEmailRequest'; +import { UpdateTwoFactorU2fRequest } from '../models/request/updateTwoFactorU2fRequest'; +import { UpdateTwoFactorYubioOtpRequest } from '../models/request/updateTwoFactorYubioOtpRequest'; import { CipherResponse } from '../models/response/cipherResponse'; import { DomainsResponse } from '../models/response/domainsResponse'; @@ -33,8 +40,16 @@ import { ErrorResponse } from '../models/response/errorResponse'; import { FolderResponse } from '../models/response/folderResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; +import { ListResponse } from '../models/response/listResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SyncResponse } from '../models/response/syncResponse'; +import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; +import { TwoFactorDuoResponse } from '../models/response/twoFactorDuoResponse'; +import { TwoFactorEmailResponse } from '../models/response/twoFactorEmailResponse'; +import { TwoFactorProviderResponse } from '../models/response/twoFactorProviderResponse'; +import { TwoFactorRecoverResponse } from '../models/response/twoFactorRescoverResponse'; +import { TwoFactorU2fResponse } from '../models/response/twoFactorU2fResponse'; +import { TwoFactorYubiKeyResponse } from '../models/response/twoFactorYubiKeyResponse'; export class ApiService implements ApiServiceAbstraction { urlsSet: boolean = false; @@ -280,6 +295,78 @@ export class ApiService implements ApiServiceAbstraction { return new DomainsResponse(r); } + // Two-factor + + async getTwoFactorProviders(): Promise> { + const r = await this.send('GET', '/two-factor', null, true, true); + return new ListResponse(r, TwoFactorProviderResponse); + } + + async getTwoFactorAuthenticator(request: PasswordVerificationRequest): Promise { + const r = await this.send('POST', '/two-factor/get-authenticator', request, true, true); + return new TwoFactorAuthenticatorResponse(r); + } + + async getTwoFactorEmail(request: PasswordVerificationRequest): Promise { + const r = await this.send('POST', '/two-factor/get-email', request, true, true); + return new TwoFactorEmailResponse(r); + } + + async getTwoFactorDuo(request: PasswordVerificationRequest): Promise { + const r = await this.send('POST', '/two-factor/get-duo', request, true, true); + return new TwoFactorDuoResponse(r); + } + + async getTwoFactorYubiKey(request: PasswordVerificationRequest): Promise { + const r = await this.send('POST', '/two-factor/get-yubikey', request, true, true); + return new TwoFactorYubiKeyResponse(r); + } + + async getTwoFactorU2f(request: PasswordVerificationRequest): Promise { + const r = await this.send('POST', '/two-factor/get-u2f', request, true, true); + return new TwoFactorU2fResponse(r); + } + + async getTwoFactorRecover(request: PasswordVerificationRequest): Promise { + const r = await this.send('POST', '/two-factor/get-recover', request, true, true); + return new TwoFactorRecoverResponse(r); + } + + async putTwoFactorAuthenticator( + request: UpdateTwoFactorAuthenticatorRequest): Promise { + const r = await this.send('PUT', '/two-factor/authenticator', request, true, true); + return new TwoFactorAuthenticatorResponse(r); + } + + async putTwoFactorEmail(request: UpdateTwoFactorEmailRequest): Promise { + const r = await this.send('PUT', '/two-factor/email', request, true, true); + return new TwoFactorEmailResponse(r); + } + + async putTwoFactorDuo(request: UpdateTwoFactorDuoRequest): Promise { + const r = await this.send('PUT', '/two-factor/duo', request, true, true); + return new TwoFactorDuoResponse(r); + } + + async putTwoFactorYubiKey(request: UpdateTwoFactorYubioOtpRequest): Promise { + const r = await this.send('PUT', '/two-factor/yubikey', request, true, true); + return new TwoFactorYubiKeyResponse(r); + } + + async putTwoFactorU2f(request: UpdateTwoFactorU2fRequest): Promise { + const r = await this.send('PUT', '/two-factor/u2f', request, true, true); + return new TwoFactorU2fResponse(r); + } + + async putTwoFactorDisable(request: TwoFactorProviderRequest): Promise { + const r = await this.send('PUT', '/two-factor/disable', request, true, true); + return new TwoFactorProviderResponse(r); + } + + postTwoFactorRecover(request: TwoFactorRecoveryRequest): Promise { + return this.send('POST', '/two-factor/recover', request, false, false); + } + // Helpers private async send(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body: any, diff --git a/tslint.json b/tslint.json index b6d5571669..7e4320f723 100644 --- a/tslint.json +++ b/tslint.json @@ -48,6 +48,7 @@ "check-preblock", "check-separator", "check-type" - ] + ], + "max-classes-per-file": false } } From 0d30c89c5a683dadda95d91eac84ecf6d50b3a98 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 26 Jun 2018 15:55:01 -0400 Subject: [PATCH 0322/1626] export challenge response --- src/models/response/twoFactorU2fResponse.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/models/response/twoFactorU2fResponse.ts b/src/models/response/twoFactorU2fResponse.ts index 2c98c48fa8..334b7994e8 100644 --- a/src/models/response/twoFactorU2fResponse.ts +++ b/src/models/response/twoFactorU2fResponse.ts @@ -1,14 +1,14 @@ export class TwoFactorU2fResponse { enabled: boolean; - challenge: Challenge; + challenge: ChallengeResponse; constructor(response: any) { this.enabled = response.Enabled; - this.challenge = new Challenge(response.Challenge); + this.challenge = new ChallengeResponse(response.Challenge); } } -class Challenge { +export class ChallengeResponse { userId: string; appId: string; challenge: string; From 3cc759791e12b7692fc2d2b4be1a2b010eee1c8e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 26 Jun 2018 22:39:39 -0400 Subject: [PATCH 0323/1626] 2fa provider properties --- src/models/response/listResponse.ts | 2 +- src/services/auth.service.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/models/response/listResponse.ts b/src/models/response/listResponse.ts index 84c2c11632..d879e0f53f 100644 --- a/src/models/response/listResponse.ts +++ b/src/models/response/listResponse.ts @@ -3,7 +3,7 @@ export class ListResponse { continuationToken: string; constructor(response: any, t: new (dataResponse: any) => T) { - this.data = response.Data.map((dr) => new t(dr)); + this.data = response.Data.map((dr: any) => new t(dr)); this.continuationToken = response.ContinuationToken; } } diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 2eaaa0fe14..3c06bc6f4f 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -24,36 +24,48 @@ export const TwoFactorProviders = { name: null as string, description: null as string, priority: 1, + sort: 1, + premium: false, }, [TwoFactorProviderType.Yubikey]: { type: TwoFactorProviderType.Yubikey, name: null as string, description: null as string, priority: 3, + sort: 2, + premium: true, }, [TwoFactorProviderType.Duo]: { type: TwoFactorProviderType.Duo, name: 'Duo', description: null as string, priority: 2, + sort: 3, + premium: true, }, [TwoFactorProviderType.OrganizationDuo]: { type: TwoFactorProviderType.OrganizationDuo, name: 'Duo (Organization)', description: null as string, priority: 10, + sort: 4, + premium: false, }, [TwoFactorProviderType.U2f]: { type: TwoFactorProviderType.U2f, name: null as string, description: null as string, priority: 4, + sort: 5, + premium: true, }, [TwoFactorProviderType.Email]: { type: TwoFactorProviderType.Email, name: null as string, description: null as string, priority: 0, + sort: 6, + premium: false, }, }; From ec505b8c5508e4c8f6191c946b042a804c73c501 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 26 Jun 2018 22:52:12 -0400 Subject: [PATCH 0324/1626] data is never null --- src/models/response/listResponse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/response/listResponse.ts b/src/models/response/listResponse.ts index d879e0f53f..b6cfdf5feb 100644 --- a/src/models/response/listResponse.ts +++ b/src/models/response/listResponse.ts @@ -3,7 +3,7 @@ export class ListResponse { continuationToken: string; constructor(response: any, t: new (dataResponse: any) => T) { - this.data = response.Data.map((dr: any) => new t(dr)); + this.data = response.Data == null ? [] : response.Data.map((dr: any) => new t(dr)); this.continuationToken = response.ContinuationToken; } } From ccd10751e3b4228f4117fd1ca2f87f8603eac130 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 27 Jun 2018 17:50:12 -0400 Subject: [PATCH 0325/1626] u2f email send apis --- src/abstractions/api.service.ts | 3 ++- src/services/api.service.ts | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index d6f9447b35..e34da705c2 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -52,7 +52,6 @@ export abstract class ApiService { setUrls: (urls: EnvironmentUrls) => void; postIdentityToken: (request: TokenRequest) => Promise; refreshIdentityToken: () => Promise; - postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise; getProfile: () => Promise; putProfile: (request: UpdateProfileRequest) => Promise; postEmailToken: (request: EmailTokenRequest) => Promise; @@ -100,4 +99,6 @@ export abstract class ApiService { putTwoFactorU2f: (request: UpdateTwoFactorU2fRequest) => Promise; putTwoFactorDisable: (request: TwoFactorProviderRequest) => Promise; postTwoFactorRecover: (request: TwoFactorRecoveryRequest) => Promise; + postTwoFactorEmailSetup: (request: TwoFactorEmailRequest) => Promise; + postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise; } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 75b23c9ca7..525d765a66 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -139,12 +139,6 @@ export class ApiService implements ApiServiceAbstraction { } } - // Two Factor APIs - - postTwoFactorEmail(request: TwoFactorEmailRequest): Promise { - return this.send('POST', '/two-factor/send-email-login', request, false, false); - } - // Account APIs async getProfile(): Promise { @@ -283,7 +277,7 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/organizations/' + organizationId + '/import', request, true, false); } - // Settings + // Settings APIs async getSettingsDomains(): Promise { const r = await this.send('GET', '/settings/domains', null, true, true); @@ -295,7 +289,7 @@ export class ApiService implements ApiServiceAbstraction { return new DomainsResponse(r); } - // Two-factor + // Two-factor APIs async getTwoFactorProviders(): Promise> { const r = await this.send('GET', '/two-factor', null, true, true); @@ -367,6 +361,14 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/two-factor/recover', request, false, false); } + postTwoFactorEmailSetup(request: TwoFactorEmailRequest): Promise { + return this.send('POST', '/two-factor/send-email', request, true, false); + } + + postTwoFactorEmail(request: TwoFactorEmailRequest): Promise { + return this.send('POST', '/two-factor/send-email-login', request, false, false); + } + // Helpers private async send(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body: any, From 569ad25208d63928c604fdcda8ed1899c183495a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 27 Jun 2018 22:09:39 -0400 Subject: [PATCH 0326/1626] lint fixes --- src/importers/baseImporter.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index c1a245e510..620627facb 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -14,7 +14,7 @@ export abstract class BaseImporter { 'p', 'serial', 'serial#', 'license key', 'reg #', // Non-English names - 'passwort' + 'passwort', ]; protected usernameFieldNames = [ @@ -26,16 +26,16 @@ export abstract class BaseImporter { 'member', 'member #', // Non-English names - 'nom', 'benutzername' + 'nom', 'benutzername', ]; protected notesFieldNames = [ - "note", "notes", "comment", "comments", "memo", - "description", "free form", "freeform", - "free text", "freetext", "free", + 'note', 'notes', 'comment', 'comments', 'memo', + 'description', 'free form', 'freeform', + 'free text', 'freetext', 'free', // Non-English names - "kommentar" + 'kommentar', ]; protected uriFieldNames: string[] = [ @@ -45,7 +45,7 @@ export abstract class BaseImporter { 'web-site', 'uri', // Non-English names - 'ort', 'adresse' + 'ort', 'adresse', ]; protected parseCsv(data: string, header: boolean): any[] { @@ -149,7 +149,7 @@ export abstract class BaseImporter { return 'Visa'; } - // Mastercard + // Mastercard // Updated for Mastercard 2017 BINs expansion if (/^(5[1-5][0-9]{14}|2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12}))$/ .test(cardNum)) { From a097ef9beabe7da67c4c338e85a15bf3ad02a7f1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 28 Jun 2018 11:57:29 -0400 Subject: [PATCH 0327/1626] breach report audit api --- src/abstractions/audit.service.ts | 3 ++ src/models/response/breachAccountResponse.ts | 29 ++++++++++++++++++++ src/services/audit.service.ts | 15 ++++++++++ 3 files changed, 47 insertions(+) create mode 100644 src/models/response/breachAccountResponse.ts diff --git a/src/abstractions/audit.service.ts b/src/abstractions/audit.service.ts index 0ea1e1fe89..e324d08c4b 100644 --- a/src/abstractions/audit.service.ts +++ b/src/abstractions/audit.service.ts @@ -1,3 +1,6 @@ +import { BreachAccountResponse } from '../models/response/breachAccountResponse'; + export abstract class AuditService { passwordLeaked: (password: string) => Promise; + breachedAccounts: (email: string) => Promise; } diff --git a/src/models/response/breachAccountResponse.ts b/src/models/response/breachAccountResponse.ts new file mode 100644 index 0000000000..cc5f748232 --- /dev/null +++ b/src/models/response/breachAccountResponse.ts @@ -0,0 +1,29 @@ +export class BreachAccountResponse { + addedDate: Date; + breachDate: Date; + dataClasses: string[]; + description: string; + domain: string; + isActive: boolean; + isVerified: boolean; + logoType: string; + modifiedDate: Date; + name: string; + pwnCount: number; + title: string; + + constructor(response: any) { + this.addedDate = response.AddedDate; + this.breachDate = response.BreachDate; + this.dataClasses = response.DataClasses; + this.description = response.Description; + this.domain = response.Domain; + this.isActive = response.IsActive; + this.isVerified = response.IsVerified; + this.logoType = response.LogoType; + this.modifiedDate = response.ModifiedDate; + this.name = response.Name; + this.pwnCount = response.PwnCount; + this.title = response.Title; + } +} diff --git a/src/services/audit.service.ts b/src/services/audit.service.ts index d18ccbe219..6226386016 100644 --- a/src/services/audit.service.ts +++ b/src/services/audit.service.ts @@ -1,8 +1,12 @@ import { AuditService as AuditServiceAbstraction } from '../abstractions/audit.service'; import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; + import { Utils } from '../misc/utils'; +import { BreachAccountResponse } from '../models/response/breachAccountResponse'; + const PwnedPasswordsApi = 'https://api.pwnedpasswords.com/range/'; +const HibpBreachApi = 'https://haveibeenpwned.com/api/v2/breachedaccount/'; export class AuditService implements AuditServiceAbstraction { constructor(private cryptoFunctionService: CryptoFunctionService) { } @@ -21,4 +25,15 @@ export class AuditService implements AuditServiceAbstraction { return match != null ? parseInt(match.split(':')[1], 10) : 0; } + + async breachedAccounts(email: string): Promise { + const response = await fetch(HibpBreachApi + email); + if (response.status === 404) { + return []; + } else if (response.status !== 200) { + throw new Error(); + } + const responseJson = await response.json(); + return responseJson.map((a) => new BreachAccountResponse(a)); + } } From 3aebe1a09a282fdba1a2b27d42e0c154635b3188 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 28 Jun 2018 13:48:50 -0400 Subject: [PATCH 0328/1626] specify any type --- src/services/audit.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/audit.service.ts b/src/services/audit.service.ts index 6226386016..13226074ca 100644 --- a/src/services/audit.service.ts +++ b/src/services/audit.service.ts @@ -34,6 +34,6 @@ export class AuditService implements AuditServiceAbstraction { throw new Error(); } const responseJson = await response.json(); - return responseJson.map((a) => new BreachAccountResponse(a)); + return responseJson.map((a: any) => new BreachAccountResponse(a)); } } From 93edd272dde1d0d5739c29a57668849fb445eee3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 28 Jun 2018 23:05:25 -0400 Subject: [PATCH 0329/1626] premium api --- src/abstractions/api.service.ts | 1 + src/services/api.service.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index e34da705c2..8a4953f04e 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -62,6 +62,7 @@ export abstract class ApiService { getAccountRevisionDate: () => Promise; postPasswordHint: (request: PasswordHintRequest) => Promise; postRegister: (request: RegisterRequest) => Promise; + postPremium: (data: FormData) => Promise; postFolder: (request: FolderRequest) => Promise; putFolder: (id: string, request: FolderRequest) => Promise; deleteFolder: (id: string) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 525d765a66..6574eb4d4f 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -184,6 +184,10 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/accounts/register', request, false, false); } + postPremium(data: FormData): Promise { + return this.send('POST', '/accounts/premium', data, true, true); + } + // Folder APIs async postFolder(request: FolderRequest): Promise { From e297f39bd53db6939bc888e5aa1c41ff8579b19d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 29 Jun 2018 08:20:28 -0400 Subject: [PATCH 0330/1626] breach check changed to username --- src/abstractions/audit.service.ts | 2 +- src/services/audit.service.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/abstractions/audit.service.ts b/src/abstractions/audit.service.ts index e324d08c4b..b8c40bc29f 100644 --- a/src/abstractions/audit.service.ts +++ b/src/abstractions/audit.service.ts @@ -2,5 +2,5 @@ import { BreachAccountResponse } from '../models/response/breachAccountResponse' export abstract class AuditService { passwordLeaked: (password: string) => Promise; - breachedAccounts: (email: string) => Promise; + breachedAccounts: (username: string) => Promise; } diff --git a/src/services/audit.service.ts b/src/services/audit.service.ts index 13226074ca..2c4e6c1639 100644 --- a/src/services/audit.service.ts +++ b/src/services/audit.service.ts @@ -26,8 +26,8 @@ export class AuditService implements AuditServiceAbstraction { return match != null ? parseInt(match.split(':')[1], 10) : 0; } - async breachedAccounts(email: string): Promise { - const response = await fetch(HibpBreachApi + email); + async breachedAccounts(username: string): Promise { + const response = await fetch(HibpBreachApi + username); if (response.status === 404) { return []; } else if (response.status !== 200) { From ac221d8867c526eb14077398d95399f9016378fa Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 29 Jun 2018 09:27:50 -0400 Subject: [PATCH 0331/1626] blur csv importer --- src/importers/blurCsvImporter.ts | 45 ++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/importers/blurCsvImporter.ts diff --git a/src/importers/blurCsvImporter.ts b/src/importers/blurCsvImporter.ts new file mode 100644 index 0000000000..33620181b5 --- /dev/null +++ b/src/importers/blurCsvImporter.ts @@ -0,0 +1,45 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CipherView } from '../models/view/cipherView'; +import { LoginView } from '../models/view/loginView'; + +import { CipherType } from '../enums/cipherType'; + +export class BlurCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + const cipher = new CipherView(); + cipher.type = CipherType.Login; + if (value.label === 'null') { + value.label = null; + } + cipher.name = this.getValueOrDefault(value.label, + this.getValueOrDefault(this.nameFromUrl(value.domain), '--')); + cipher.login = new LoginView(); + cipher.login.uris = this.makeUriArray(value.domain); + cipher.login.password = this.getValueOrDefault(value.password); + + if (this.isNullOrWhitespace(value.email) && !this.isNullOrWhitespace(value.username)) { + cipher.login.username = value.username; + } else { + cipher.login.username = this.getValueOrDefault(value.email); + cipher.notes = this.getValueOrDefault(value.username); + } + + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} From 15651400245da022e6d47384448496edd43be6bd Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 29 Jun 2018 11:29:24 -0400 Subject: [PATCH 0332/1626] add billing apis --- src/abstractions/api.service.ts | 2 + src/enums/paymentMethodType.ts | 6 ++ src/models/response/billingResponse.ts | 101 +++++++++++++++++++++++++ src/services/api.service.ts | 6 ++ 4 files changed, 115 insertions(+) create mode 100644 src/enums/paymentMethodType.ts create mode 100644 src/models/response/billingResponse.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 8a4953f04e..5b45bd3a33 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -28,6 +28,7 @@ import { UpdateTwoFactorEmailRequest } from '../models/request/updateTwoFactorEm import { UpdateTwoFactorU2fRequest } from '../models/request/updateTwoFactorU2fRequest'; import { UpdateTwoFactorYubioOtpRequest } from '../models/request/updateTwoFactorYubioOtpRequest'; +import { BillingResponse } from '../models/response/billingResponse'; import { CipherResponse } from '../models/response/cipherResponse'; import { DomainsResponse } from '../models/response/domainsResponse'; import { FolderResponse } from '../models/response/folderResponse'; @@ -53,6 +54,7 @@ export abstract class ApiService { postIdentityToken: (request: TokenRequest) => Promise; refreshIdentityToken: () => Promise; getProfile: () => Promise; + getUserBilling: () => Promise; putProfile: (request: UpdateProfileRequest) => Promise; postEmailToken: (request: EmailTokenRequest) => Promise; postEmail: (request: EmailRequest) => Promise; diff --git a/src/enums/paymentMethodType.ts b/src/enums/paymentMethodType.ts new file mode 100644 index 0000000000..fcc50fa45e --- /dev/null +++ b/src/enums/paymentMethodType.ts @@ -0,0 +1,6 @@ +export enum PaymentMethodType { + Card = 0, + BankAccount = 1, + PayPal = 2, + Bitcoin = 3, +} diff --git a/src/models/response/billingResponse.ts b/src/models/response/billingResponse.ts new file mode 100644 index 0000000000..2bd97a9ba6 --- /dev/null +++ b/src/models/response/billingResponse.ts @@ -0,0 +1,101 @@ +import { PaymentMethodType } from '../../enums/paymentMethodType'; + +export class BillingResponse { + storageName: string; + storageGb: number; + maxStorageGb: number; + + constructor(response: any) { + this.storageName = response.StorageName; + this.storageGb = response.StorageGb; + this.maxStorageGb = response.MaxStorageGb; + } +} + +export class BillingSourceResponse { + type: PaymentMethodType; + cardBrand: string; + description: string; + needsVerification: boolean; + + constructor(response: any) { + this.type = response.Type; + this.cardBrand = response.CardBrand; + this.description = response.Description; + this.needsVerification = response.NeedsVerification; + } +} + +export class BillingSubscriptionResponse { + trialStartDate: Date; + trialEndDate: Date; + periodStartDate: Date; + periodEndDate: Date; + cancelledDate: Date; + cancelAtEndDate: boolean; + status: string; + cancelled: boolean; + items: BillingSubscriptionItemResponse[] = []; + + constructor(response: any) { + this.trialEndDate = response.TrialStartDate; + this.trialEndDate = response.TrialEndDate; + this.periodStartDate = response.PeriodStartDate; + this.periodEndDate = response.PeriodEndDate; + this.cancelledDate = response.CancelledDate; + this.cancelAtEndDate = response.CancelAtEndDate; + this.status = response.Status; + this.cancelled = response.Cancelled; + if (response.Items != null) { + this.items = response.Items.map((i) => new BillingSubscriptionItemResponse(i)); + } + } +} + +export class BillingSubscriptionItemResponse { + name: string; + amount: number; + quantity: number; + internal: string; + + constructor(response: any) { + this.name = response.Name; + this.amount = response.Amount; + this.quantity = response.Quantity; + this.internal = response.Internal; + } +} + +export class BillingInvoiceResponse { + date: Date; + amount: number; + + constructor(response: any) { + this.date = response.Date; + this.amount = response.Amount; + } +} + +export class BillingChargeResponse { + createdDate: Date; + amount: number; + paymentSource: BillingSourceResponse; + status: string; + failureMessage: string; + refunded: boolean; + partiallyRefunded: boolean; + refundedAmount: number; + invoiceId: string; + + constructor(response: any) { + this.createdDate = response.CreatedDate; + this.amount = response.Amount; + this.paymentSource = response.PaymentSource != null ? new BillingSourceResponse(response.PaymentSource) : null; + this.status = response.Status; + this.failureMessage = response.FailureMessage; + this.refunded = response.Refunded; + this.partiallyRefunded = response.PartiallyRefunded; + this.refundedAmount = response.RefundedAmount; + this.invoiceId = response.InvoiceId; + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 6574eb4d4f..01a08e7802 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -34,6 +34,7 @@ import { UpdateTwoFactorEmailRequest } from '../models/request/updateTwoFactorEm import { UpdateTwoFactorU2fRequest } from '../models/request/updateTwoFactorU2fRequest'; import { UpdateTwoFactorYubioOtpRequest } from '../models/request/updateTwoFactorYubioOtpRequest'; +import { BillingResponse } from '../models/response/billingResponse'; import { CipherResponse } from '../models/response/cipherResponse'; import { DomainsResponse } from '../models/response/domainsResponse'; import { ErrorResponse } from '../models/response/errorResponse'; @@ -146,6 +147,11 @@ export class ApiService implements ApiServiceAbstraction { return new ProfileResponse(r); } + async getUserBilling(): Promise { + const r = await this.send('GET', '/accounts/billing', null, true, true); + return new BillingResponse(r); + } + async putProfile(request: UpdateProfileRequest): Promise { const r = await this.send('PUT', '/accounts/profile', request, true, true); return new ProfileResponse(r); From 3726d9e6d8a0572e0a2c5d284724000ea5b9cff1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 29 Jun 2018 13:58:01 -0400 Subject: [PATCH 0333/1626] add cancel and reinstate premium --- src/abstractions/api.service.ts | 2 ++ src/models/response/billingResponse.ts | 18 +++++++++++++++++- src/services/api.service.ts | 10 +++++++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 5b45bd3a33..2755d0db2b 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -65,6 +65,8 @@ export abstract class ApiService { postPasswordHint: (request: PasswordHintRequest) => Promise; postRegister: (request: RegisterRequest) => Promise; postPremium: (data: FormData) => Promise; + postReinstatePremium: () => Promise; + postCancelPremium: () => Promise; postFolder: (request: FolderRequest) => Promise; putFolder: (id: string, request: FolderRequest) => Promise; deleteFolder: (id: string) => Promise; diff --git a/src/models/response/billingResponse.ts b/src/models/response/billingResponse.ts index 2bd97a9ba6..12ca0a47f7 100644 --- a/src/models/response/billingResponse.ts +++ b/src/models/response/billingResponse.ts @@ -4,11 +4,27 @@ export class BillingResponse { storageName: string; storageGb: number; maxStorageGb: number; + paymentSource: BillingSourceResponse; + subscription: BillingSubscriptionResponse; + upcomingInvoice: BillingInvoiceResponse; + charges: BillingChargeResponse[] = []; + license: any; + expiration: Date; constructor(response: any) { this.storageName = response.StorageName; this.storageGb = response.StorageGb; this.maxStorageGb = response.MaxStorageGb; + this.paymentSource = response.PaymentSource == null ? null : new BillingSourceResponse(response.PaymentSource); + this.subscription = response.Subscription == null ? + null : new BillingSubscriptionResponse(response.Subscription); + this.upcomingInvoice = response.UpcomingInvoice == null ? + null : new BillingInvoiceResponse(response.UpcomingInvoice); + if (response.Charges != null) { + this.charges = response.Charges.map((c: any) => new BillingChargeResponse(c)); + } + this.license = response.License; + this.expiration = response.Expiration; } } @@ -47,7 +63,7 @@ export class BillingSubscriptionResponse { this.status = response.Status; this.cancelled = response.Cancelled; if (response.Items != null) { - this.items = response.Items.map((i) => new BillingSubscriptionItemResponse(i)); + this.items = response.Items.map((i: any) => new BillingSubscriptionItemResponse(i)); } } } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 01a08e7802..570ae4ddff 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -191,7 +191,15 @@ export class ApiService implements ApiServiceAbstraction { } postPremium(data: FormData): Promise { - return this.send('POST', '/accounts/premium', data, true, true); + return this.send('POST', '/accounts/premium', data, true, false); + } + + postReinstatePremium(): Promise { + return this.send('POST', '/accounts/reinstate-premium', null, true, false); + } + + postCancelPremium(): Promise { + return this.send('POST', '/accounts/cancel-premium', null, true, false); } // Folder APIs From ef897695e9b5bfecbfcbbd4ad3aec62b4ecdca25 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 29 Jun 2018 16:55:04 -0400 Subject: [PATCH 0334/1626] fix typo on billing response --- src/models/response/billingResponse.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/response/billingResponse.ts b/src/models/response/billingResponse.ts index 12ca0a47f7..d3e451696a 100644 --- a/src/models/response/billingResponse.ts +++ b/src/models/response/billingResponse.ts @@ -72,13 +72,13 @@ export class BillingSubscriptionItemResponse { name: string; amount: number; quantity: number; - internal: string; + interval: string; constructor(response: any) { this.name = response.Name; this.amount = response.Amount; this.quantity = response.Quantity; - this.internal = response.Internal; + this.interval = response.Interval; } } From c0e7e588ed59832a6f579ff63d85bfcdfb400d78 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 29 Jun 2018 23:40:10 -0400 Subject: [PATCH 0335/1626] adjust storage apis --- src/abstractions/api.service.ts | 2 ++ src/models/request/storageRequest.ts | 3 +++ src/services/api.service.ts | 5 +++++ 3 files changed, 10 insertions(+) create mode 100644 src/models/request/storageRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 2755d0db2b..83c7a1fd88 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -16,6 +16,7 @@ import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; import { RegisterRequest } from '../models/request/registerRequest'; +import { StorageRequest } from '../models/request/storageRequest'; import { TokenRequest } from '../models/request/tokenRequest'; import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; import { TwoFactorProviderRequest } from '../models/request/twoFactorProviderRequest'; @@ -67,6 +68,7 @@ export abstract class ApiService { postPremium: (data: FormData) => Promise; postReinstatePremium: () => Promise; postCancelPremium: () => Promise; + postAccountStorage: (request: StorageRequest) => Promise; postFolder: (request: FolderRequest) => Promise; putFolder: (id: string, request: FolderRequest) => Promise; deleteFolder: (id: string) => Promise; diff --git a/src/models/request/storageRequest.ts b/src/models/request/storageRequest.ts new file mode 100644 index 0000000000..f4b78559bc --- /dev/null +++ b/src/models/request/storageRequest.ts @@ -0,0 +1,3 @@ +export class StorageRequest { + storageGbAdjustment: number; +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 570ae4ddff..cf0d7b6bc4 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -22,6 +22,7 @@ import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; import { RegisterRequest } from '../models/request/registerRequest'; +import { StorageRequest } from '../models/request/storageRequest'; import { TokenRequest } from '../models/request/tokenRequest'; import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; import { TwoFactorProviderRequest } from '../models/request/twoFactorProviderRequest'; @@ -202,6 +203,10 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/accounts/cancel-premium', null, true, false); } + postAccountStorage(request: StorageRequest): Promise { + return this.send('POST', '/accounts/storage', request, true, false); + } + // Folder APIs async postFolder(request: FolderRequest): Promise { From f5287e29a2a135c131d00c4a56a90b18bc4afaab Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 30 Jun 2018 13:22:21 -0400 Subject: [PATCH 0336/1626] payment update api --- src/abstractions/api.service.ts | 2 ++ src/models/request/paymentRequest.ts | 3 +++ src/services/api.service.ts | 5 +++++ 3 files changed, 10 insertions(+) create mode 100644 src/models/request/paymentRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 83c7a1fd88..6ff853fde4 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -15,6 +15,7 @@ import { ImportOrganizationCiphersRequest } from '../models/request/importOrgani import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; +import { PaymentRequest } from '../models/request/paymentRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { StorageRequest } from '../models/request/storageRequest'; import { TokenRequest } from '../models/request/tokenRequest'; @@ -69,6 +70,7 @@ export abstract class ApiService { postReinstatePremium: () => Promise; postCancelPremium: () => Promise; postAccountStorage: (request: StorageRequest) => Promise; + postAccountPayment: (request: PaymentRequest) => Promise; postFolder: (request: FolderRequest) => Promise; putFolder: (id: string, request: FolderRequest) => Promise; deleteFolder: (id: string) => Promise; diff --git a/src/models/request/paymentRequest.ts b/src/models/request/paymentRequest.ts new file mode 100644 index 0000000000..710fd18ffc --- /dev/null +++ b/src/models/request/paymentRequest.ts @@ -0,0 +1,3 @@ +export class PaymentRequest { + paymentToken: string; +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index cf0d7b6bc4..67b603afba 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -21,6 +21,7 @@ import { ImportOrganizationCiphersRequest } from '../models/request/importOrgani import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; +import { PaymentRequest } from '../models/request/paymentRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { StorageRequest } from '../models/request/storageRequest'; import { TokenRequest } from '../models/request/tokenRequest'; @@ -207,6 +208,10 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/accounts/storage', request, true, false); } + postAccountPayment(request: PaymentRequest): Promise { + return this.send('POST', '/accounts/payment', request, true, false); + } + // Folder APIs async postFolder(request: FolderRequest): Promise { From 20622db73c5c2a56777944bb06f32a21bf2e763f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 30 Jun 2018 13:51:09 -0400 Subject: [PATCH 0337/1626] isSelfHost --- src/abstractions/platformUtils.service.ts | 1 + src/electron/services/electronPlatformUtils.service.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index 98eb7e349a..debb19f6ae 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -25,5 +25,6 @@ export abstract class PlatformUtilsService { showDialog: (text: string, title?: string, confirmText?: string, cancelText?: string, type?: string) => Promise; isDev: () => boolean; + isSelfHost: () => boolean; copyToClipboard: (text: string, options?: any) => void; } diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index 04d8669b2f..dcd0224816 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -170,6 +170,10 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { return isDev(); } + isSelfHost(): boolean { + return false; + } + copyToClipboard(text: string, options?: any): void { const type = options ? options.type : null; clipboard.writeText(text, type); From 5b5d3069a92226b6c5da67d3e6c47a7eb032f6b5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 2 Jul 2018 09:53:43 -0400 Subject: [PATCH 0338/1626] update license apis --- src/abstractions/api.service.ts | 1 + src/services/api.service.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 6ff853fde4..27af163f04 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -71,6 +71,7 @@ export abstract class ApiService { postCancelPremium: () => Promise; postAccountStorage: (request: StorageRequest) => Promise; postAccountPayment: (request: PaymentRequest) => Promise; + postAccountLicense: (data: FormData) => Promise; postFolder: (request: FolderRequest) => Promise; putFolder: (id: string, request: FolderRequest) => Promise; deleteFolder: (id: string) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 67b603afba..56b43c3e8c 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -212,6 +212,10 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/accounts/payment', request, true, false); } + postAccountLicense(data: FormData): Promise { + return this.send('POST', '/accounts/license', data, true, false); + } + // Folder APIs async postFolder(request: FolderRequest): Promise { From 8be95bfe574a7ae2c8173921bbdfe82451436081 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 2 Jul 2018 10:27:27 -0400 Subject: [PATCH 0339/1626] get email verified from token service --- src/abstractions/token.service.ts | 1 + src/services/token.service.ts | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/src/abstractions/token.service.ts b/src/abstractions/token.service.ts index 939b96f90f..dbc446a41d 100644 --- a/src/abstractions/token.service.ts +++ b/src/abstractions/token.service.ts @@ -17,6 +17,7 @@ export abstract class TokenService { tokenNeedsRefresh: (minutes?: number) => boolean; getUserId: () => string; getEmail: () => string; + getEmailVerified: () => boolean; getName: () => string; getPremium: () => boolean; getIssuer: () => string; diff --git a/src/services/token.service.ts b/src/services/token.service.ts index c572621103..1540d1a0ef 100644 --- a/src/services/token.service.ts +++ b/src/services/token.service.ts @@ -148,6 +148,15 @@ export class TokenService implements TokenServiceAbstraction { return decoded.email as string; } + getEmailVerified(): boolean { + const decoded = this.decodeToken(); + if (typeof decoded.email_verified === 'undefined') { + throw new Error('No email verification found'); + } + + return decoded.email_verified as boolean; + } + getName(): string { const decoded = this.decodeToken(); if (typeof decoded.name === 'undefined') { From 7ba04c919eb64f321347a1c38810db3432cb3141 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 2 Jul 2018 15:19:33 -0400 Subject: [PATCH 0340/1626] plan types --- src/enums/planType.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/enums/planType.ts diff --git a/src/enums/planType.ts b/src/enums/planType.ts new file mode 100644 index 0000000000..eadd188415 --- /dev/null +++ b/src/enums/planType.ts @@ -0,0 +1,9 @@ +export enum PlanType { + Free = 0, + FamiliesAnnually = 1, + TeamsMonthly = 2, + TeamsAnnually = 3, + EnterpriseMonthly = 4, + EnterpriseAnnually = 5, + Custom = 6, +} From 0033b92a2ddd5a85ea308b236735a2eeba8e37f5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 2 Jul 2018 15:36:32 -0400 Subject: [PATCH 0341/1626] org create apis --- src/abstractions/api.service.ts | 4 ++ .../request/organizationCreateRequest.ts | 14 ++++++ src/models/response/organizationResponse.ts | 45 +++++++++++++++++++ src/services/api.service.ts | 14 ++++++ 4 files changed, 77 insertions(+) create mode 100644 src/models/request/organizationCreateRequest.ts create mode 100644 src/models/response/organizationResponse.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 27af163f04..21dc924ec1 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -12,6 +12,7 @@ import { FolderRequest } from '../models/request/folderRequest'; import { ImportCiphersRequest } from '../models/request/importCiphersRequest'; import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest'; +import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest'; import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; @@ -37,6 +38,7 @@ import { FolderResponse } from '../models/response/folderResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; import { ListResponse } from '../models/response/listResponse'; +import { OrganizationResponse } from '../models/response/organizationResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SyncResponse } from '../models/response/syncResponse'; import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; @@ -111,4 +113,6 @@ export abstract class ApiService { postTwoFactorRecover: (request: TwoFactorRecoveryRequest) => Promise; postTwoFactorEmailSetup: (request: TwoFactorEmailRequest) => Promise; postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise; + postOrganization: (request: OrganizationCreateRequest) => Promise; + postOrganizationLicense: (data: FormData) => Promise; } diff --git a/src/models/request/organizationCreateRequest.ts b/src/models/request/organizationCreateRequest.ts new file mode 100644 index 0000000000..57a5f125ca --- /dev/null +++ b/src/models/request/organizationCreateRequest.ts @@ -0,0 +1,14 @@ +import { PlanType } from '../../enums/planType'; + +export class OrganizationCreateRequest { + name: string; + businessName: string; + billingEmail: string; + planType: PlanType; + key: string; + paymentToken: string; + additionalSeats: number; + additionalStorageGb: number; + collectionName: string; + country: string; +} diff --git a/src/models/response/organizationResponse.ts b/src/models/response/organizationResponse.ts new file mode 100644 index 0000000000..ea62f2c98a --- /dev/null +++ b/src/models/response/organizationResponse.ts @@ -0,0 +1,45 @@ +import { PlanType } from '../../enums/planType'; + +export class OrganizationResponse { + id: string; + name: string; + businessName: string; + businessAddress1: string; + businessAddress2: string; + businessAddress3: string; + businessCountry: string; + businessTaxNumber: string; + billingEmail: string; + plan: string; + planType: PlanType; + seats: number; + maxCollections: number; + maxStorageGb: number; + useGroups: boolean; + useDirectory: boolean; + useEvents: boolean; + useTotp: boolean; + use2fa: boolean; + + constructor(response: any) { + this.id = response.Id; + this.name = response.Name; + this.businessName = response.BusinessName; + this.businessAddress1 = response.BusinessAddress1; + this.businessAddress2 = response.BusinessAddress2; + this.businessAddress3 = response.BusinessAddress3; + this.businessCountry = response.BusinessCountry; + this.businessTaxNumber = response.BusinessTaxNumber; + this.billingEmail = response.BillingEmail; + this.plan = response.Plan; + this.planType = response.PlanType; + this.seats = response.Seats; + this.maxCollections = response.MaxCollections; + this.maxStorageGb = response.MaxStorageGb; + this.useGroups = response.UseGroups; + this.useDirectory = response.UseDirectory; + this.useEvents = response.UseEvents; + this.useTotp = response.UseTotp; + this.use2fa = response.Use2fa; + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 56b43c3e8c..13c0a2d314 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -18,6 +18,7 @@ import { FolderRequest } from '../models/request/folderRequest'; import { ImportCiphersRequest } from '../models/request/importCiphersRequest'; import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest'; +import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest'; import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; @@ -44,6 +45,7 @@ import { FolderResponse } from '../models/response/folderResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; import { ListResponse } from '../models/response/listResponse'; +import { OrganizationResponse } from '../models/response/organizationResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SyncResponse } from '../models/response/syncResponse'; import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; @@ -401,6 +403,18 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/two-factor/send-email-login', request, false, false); } + // Organization APIs + + async postOrganization(request: OrganizationCreateRequest): Promise { + const r = await this.send('POST', '/organizations', request, true, true); + return new OrganizationResponse(r); + } + + async postOrganizationLicense(data: FormData): Promise { + const r = await this.send('POST', '/organizations/license', data, true, true); + return new OrganizationResponse(r); + } + // Helpers private async send(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body: any, From e22915818cb7c6a756c4ac34124b14e33621c5aa Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 2 Jul 2018 17:09:45 -0400 Subject: [PATCH 0342/1626] added public key and share key to crypto service --- src/abstractions/crypto.service.ts | 2 ++ src/services/crypto.service.ts | 42 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index fc55466ee7..c4807087c1 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -12,6 +12,7 @@ export abstract class CryptoService { getKey: () => Promise; getKeyHash: () => Promise; getEncKey: () => Promise; + getPublicKey: () => Promise; getPrivateKey: () => Promise; getOrgKeys: () => Promise>; getOrgKey: (orgId: string) => Promise; @@ -24,6 +25,7 @@ export abstract class CryptoService { clearKeys: () => Promise; toggleKey: () => Promise; makeKey: (password: string, salt: string) => Promise; + makeShareKey: () => Promise<[CipherString, SymmetricCryptoKey]>; hashPassword: (password: string, key: SymmetricCryptoKey) => Promise; makeEncKey: (key: SymmetricCryptoKey) => Promise; encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise; diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index f01f05b5c0..8c913ad8cc 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -26,6 +26,7 @@ export class CryptoService implements CryptoServiceAbstraction { private encKey: SymmetricCryptoKey; private legacyEtmKey: SymmetricCryptoKey; private keyHash: string; + private publicKey: ArrayBuffer; private privateKey: ArrayBuffer; private orgKeys: Map; @@ -135,6 +136,20 @@ export class CryptoService implements CryptoServiceAbstraction { return this.encKey; } + async getPublicKey(): Promise { + if (this.publicKey != null) { + return this.publicKey; + } + + const privateKey = await this.getPrivateKey(); + if (privateKey == null) { + return null; + } + + this.publicKey = null; // TODO: + return this.publicKey; + } + async getPrivateKey(): Promise { if (this.privateKey != null) { return this.privateKey; @@ -258,6 +273,14 @@ export class CryptoService implements CryptoServiceAbstraction { return new SymmetricCryptoKey(key); } + async makeShareKey(): Promise<[CipherString, SymmetricCryptoKey]> { + const shareKey = await this.cryptoFunctionService.randomBytes(64); + const publicKey = await this.getPublicKey(); + const encKey = await this.getEncKey(); + const encShareKey = await this.rsaEncrypt(shareKey, publicKey, encKey); + return [encShareKey, new SymmetricCryptoKey(shareKey)]; + } + async hashPassword(password: string, key: SymmetricCryptoKey): Promise { if (key == null) { key = await this.getKey(); @@ -497,6 +520,25 @@ export class CryptoService implements CryptoServiceAbstraction { return await this.cryptoFunctionService.aesDecrypt(data, iv, theKey.encKey); } + private async rsaEncrypt(data: ArrayBuffer, publicKey?: ArrayBuffer, key?: SymmetricCryptoKey) { + if (publicKey == null) { + publicKey = await this.getPublicKey(); + } + if (publicKey == null) { + throw new Error('Public key unavailable.'); + } + + let type = EncryptionType.Rsa2048_OaepSha1_B64; + const encBytes = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, 'sha1'); + let mac: string = null; + if (key != null && key.macKey != null) { + type = EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64; + const macBytes = await this.cryptoFunctionService.hmac(encBytes, key.macKey, 'sha256'); + mac = Utils.fromBufferToB64(macBytes); + } + return new CipherString(type, Utils.fromBufferToB64(encBytes), null, mac); + } + private async rsaDecrypt(encValue: string): Promise { const headerPieces = encValue.split('.'); let encType: EncryptionType = null; From 2bc7ae0da24b0cefa732608ce855cc8b61c37893 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 2 Jul 2018 23:53:44 -0400 Subject: [PATCH 0343/1626] extract public from private key --- .../nodeCryptoFunction.service.spec.ts | 9 +++++++++ .../webCryptoFunction.service.spec.ts | 9 +++++++++ src/abstractions/cryptoFunction.service.ts | 3 ++- src/services/nodeCryptoFunction.service.ts | 10 ++++++++++ src/services/webCryptoFunction.service.ts | 19 +++++++++++++++++++ 5 files changed, 49 insertions(+), 1 deletion(-) diff --git a/spec/node/services/nodeCryptoFunction.service.spec.ts b/spec/node/services/nodeCryptoFunction.service.spec.ts index e57527918f..3bd8835127 100644 --- a/spec/node/services/nodeCryptoFunction.service.spec.ts +++ b/spec/node/services/nodeCryptoFunction.service.spec.ts @@ -161,6 +161,15 @@ describe('NodeCrypto Function Service', () => { }); }); + describe('rsaExtractPublicKey', () => { + it('should successfully extract key', async () => { + const nodeCryptoFunctionService = new NodeCryptoFunctionService(); + const privKey = Utils.fromB64ToArray(RsaPrivateKey); + const publicKey = await nodeCryptoFunctionService.rsaExtractPublicKey(privKey.buffer); + expect(Utils.fromBufferToB64(publicKey)).toBe(RsaPublicKey); + }); + }); + describe('randomBytes', () => { it('should make a value of the correct length', async () => { const nodeCryptoFunctionService = new NodeCryptoFunctionService(); diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index ca517eaec2..92c80e090d 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -247,6 +247,15 @@ describe('WebCrypto Function Service', () => { }); }); + describe('rsaExtractPublicKey', () => { + it('should successfully extract key', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const privKey = Utils.fromB64ToArray(RsaPrivateKey); + const publicKey = await cryptoFunctionService.rsaExtractPublicKey(privKey.buffer); + expect(Utils.fromBufferToB64(publicKey)).toBe(RsaPublicKey); + }); + }); + describe('randomBytes', () => { it('should make a value of the correct length', async () => { const cryptoFunctionService = getWebCryptoFunctionService(); diff --git a/src/abstractions/cryptoFunction.service.ts b/src/abstractions/cryptoFunction.service.ts index 553444b667..d45221ea2d 100644 --- a/src/abstractions/cryptoFunction.service.ts +++ b/src/abstractions/cryptoFunction.service.ts @@ -16,6 +16,7 @@ export abstract class CryptoFunctionService { aesDecryptFast: (parameters: DecryptParameters) => Promise; aesDecrypt: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise; rsaEncrypt: (data: ArrayBuffer, publicKey: ArrayBuffer, algorithm: 'sha1' | 'sha256') => Promise; - rsaDecrypt: (data: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256') => Promise; + rsaDecrypt: (data: ArrayBuffer, privateKey: ArrayBuffer, algorithm: 'sha1' | 'sha256') => Promise; + rsaExtractPublicKey: (privateKey: ArrayBuffer) => Promise; randomBytes: (length: number) => Promise; } diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index 08b19dd0dd..ee154c06e6 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -133,6 +133,16 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { return Promise.resolve(this.toArrayBuffer(decipher)); } + async rsaExtractPublicKey(privateKey: ArrayBuffer): Promise { + const privateKeyByteString = Utils.fromBufferToByteString(privateKey); + const privateKeyAsn1 = forge.asn1.fromDer(privateKeyByteString); + const forgePrivateKey = (forge as any).pki.privateKeyFromAsn1(privateKeyAsn1); + const forgePublicKey = (forge.pki as any).setRsaPublicKey(forgePrivateKey.n, forgePrivateKey.e); + const publicKeyAsn1 = (forge.pki as any).publicKeyToAsn1(forgePublicKey); + const publicKeyByteString = forge.asn1.toDer(publicKeyAsn1).data; + return Utils.fromByteStringToArray(publicKeyByteString).buffer; + } + randomBytes(length: number): Promise { return new Promise((resolve, reject) => { crypto.randomBytes(length, (error, bytes) => { diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index f055df3206..f5617236b9 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -204,6 +204,25 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return await this.subtle.decrypt(rsaParams, impKey, data); } + async rsaExtractPublicKey(privateKey: ArrayBuffer): Promise { + const rsaParams = { + name: 'RSA-OAEP', + // Have to specify some algorithm + hash: { name: this.toWebCryptoAlgorithm('sha1') }, + }; + const impPrivateKey = await this.subtle.importKey('pkcs8', privateKey, rsaParams, true, ['decrypt']); + const jwkPrivateKey = await this.subtle.exportKey('jwk', impPrivateKey); + const jwkPublicKeyParams = { + kty: 'RSA', + e: jwkPrivateKey.e, + n: jwkPrivateKey.n, + alg: 'RSA-OAEP', + ext: true, + }; + const impPublicKey = await this.subtle.importKey('jwk', jwkPublicKeyParams, rsaParams, true, ['encrypt']); + return await this.subtle.exportKey('spki', impPublicKey); + } + randomBytes(length: number): Promise { const arr = new Uint8Array(length); this.crypto.getRandomValues(arr); From 269b59210cba1114ff75519c9d0b391406f94bd3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 2 Jul 2018 23:57:22 -0400 Subject: [PATCH 0344/1626] use rsaExtractPublicKey for getting public key --- src/services/crypto.service.ts | 2 +- src/services/nodeCryptoFunction.service.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 8c913ad8cc..356d15ccae 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -146,7 +146,7 @@ export class CryptoService implements CryptoServiceAbstraction { return null; } - this.publicKey = null; // TODO: + this.publicKey = await this.cryptoFunctionService.rsaExtractPublicKey(privateKey); return this.publicKey; } diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index ee154c06e6..7af5de0ffb 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -133,14 +133,15 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { return Promise.resolve(this.toArrayBuffer(decipher)); } - async rsaExtractPublicKey(privateKey: ArrayBuffer): Promise { + rsaExtractPublicKey(privateKey: ArrayBuffer): Promise { const privateKeyByteString = Utils.fromBufferToByteString(privateKey); const privateKeyAsn1 = forge.asn1.fromDer(privateKeyByteString); const forgePrivateKey = (forge as any).pki.privateKeyFromAsn1(privateKeyAsn1); const forgePublicKey = (forge.pki as any).setRsaPublicKey(forgePrivateKey.n, forgePrivateKey.e); const publicKeyAsn1 = (forge.pki as any).publicKeyToAsn1(forgePublicKey); const publicKeyByteString = forge.asn1.toDer(publicKeyAsn1).data; - return Utils.fromByteStringToArray(publicKeyByteString).buffer; + const publicKeyArray = Utils.fromByteStringToArray(publicKeyByteString); + return Promise.resolve(publicKeyArray.buffer); } randomBytes(length: number): Promise { From 3454d93fef76f84c7351990089f7b155b88580f8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 3 Jul 2018 11:41:55 -0400 Subject: [PATCH 0345/1626] generate keypair on registration --- .../nodeCryptoFunction.service.spec.ts | 19 +++++++++++++ .../webCryptoFunction.service.spec.ts | 16 +++++++++++ src/abstractions/crypto.service.ts | 5 ++-- src/abstractions/cryptoFunction.service.ts | 1 + src/angular/components/register.component.ts | 27 +++++++++++-------- src/models/request/registerRequest.ts | 17 ++++++++++-- src/services/crypto.service.ts | 22 ++++++++++----- src/services/lock.service.ts | 2 +- src/services/nodeCryptoFunction.service.ts | 26 ++++++++++++++++++ src/services/webCryptoFunction.service.ts | 14 ++++++++++ 10 files changed, 127 insertions(+), 22 deletions(-) diff --git a/spec/node/services/nodeCryptoFunction.service.spec.ts b/spec/node/services/nodeCryptoFunction.service.spec.ts index 3bd8835127..828b8decec 100644 --- a/spec/node/services/nodeCryptoFunction.service.spec.ts +++ b/spec/node/services/nodeCryptoFunction.service.spec.ts @@ -170,6 +170,15 @@ describe('NodeCrypto Function Service', () => { }); }); + describe('rsaGenerateKeyPair', () => { + testRsaGenerateKeyPair(1024); + testRsaGenerateKeyPair(2048); + + // Generating 4096 bit keys is really slow with Forge lib. + // Maybe move to something else if we ever want to generate keys of this size. + // testRsaGenerateKeyPair(4096); + }); + describe('randomBytes', () => { it('should make a value of the correct length', async () => { const nodeCryptoFunctionService = new NodeCryptoFunctionService(); @@ -302,6 +311,16 @@ function testCompare(fast = false) { }); } +function testRsaGenerateKeyPair(length: 1024 | 2048 | 4096) { + it('should successfully generate a ' + length + ' bit key pair', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const keyPair = await cryptoFunctionService.rsaGenerateKeyPair(length); + expect(keyPair[0] == null || keyPair[1] == null).toBe(false); + const publicKey = await cryptoFunctionService.rsaExtractPublicKey(keyPair[1]); + expect(Utils.fromBufferToB64(keyPair[0])).toBe(Utils.fromBufferToB64(publicKey)); + }, 10000); +} + function makeStaticByteArray(length: number) { const arr = new Uint8Array(length); for (let i = 0; i < length; i++) { diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index 92c80e090d..50605b10fb 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -256,6 +256,12 @@ describe('WebCrypto Function Service', () => { }); }); + describe('rsaGenerateKeyPair', () => { + testRsaGenerateKeyPair(1024); + testRsaGenerateKeyPair(2048); + testRsaGenerateKeyPair(4096); + }); + describe('randomBytes', () => { it('should make a value of the correct length', async () => { const cryptoFunctionService = getWebCryptoFunctionService(); @@ -357,6 +363,16 @@ function testHmacFast(algorithm: 'sha1' | 'sha256' | 'sha512', mac: string) { }); } +function testRsaGenerateKeyPair(length: 1024 | 2048 | 4096) { + it('should successfully generate a ' + length + ' bit key pair', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const keyPair = await cryptoFunctionService.rsaGenerateKeyPair(length); + expect(keyPair[0] == null || keyPair[1] == null).toBe(false); + const publicKey = await cryptoFunctionService.rsaExtractPublicKey(keyPair[1]); + expect(Utils.fromBufferToB64(keyPair[0])).toBe(Utils.fromBufferToB64(publicKey)); + }); +} + function getWebCryptoFunctionService() { const platformUtilsMock = TypeMoq.Mock.ofType(PlatformUtilsServiceMock); platformUtilsMock.setup((x) => x.isEdge()).returns(() => navigator.userAgent.indexOf(' Edge/') !== -1); diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index c4807087c1..9c153efca9 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -20,14 +20,15 @@ export abstract class CryptoService { clearKey: () => Promise; clearKeyHash: () => Promise; clearEncKey: (memoryOnly?: boolean) => Promise; - clearPrivateKey: (memoryOnly?: boolean) => Promise; + clearKeyPair: (memoryOnly?: boolean) => Promise; clearOrgKeys: (memoryOnly?: boolean) => Promise; clearKeys: () => Promise; toggleKey: () => Promise; makeKey: (password: string, salt: string) => Promise; makeShareKey: () => Promise<[CipherString, SymmetricCryptoKey]>; + makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, CipherString]>; hashPassword: (password: string, key: SymmetricCryptoKey) => Promise; - makeEncKey: (key: SymmetricCryptoKey) => Promise; + makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise; encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise; decryptToUtf8: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise; diff --git a/src/abstractions/cryptoFunction.service.ts b/src/abstractions/cryptoFunction.service.ts index d45221ea2d..72b64fac57 100644 --- a/src/abstractions/cryptoFunction.service.ts +++ b/src/abstractions/cryptoFunction.service.ts @@ -18,5 +18,6 @@ export abstract class CryptoFunctionService { rsaEncrypt: (data: ArrayBuffer, publicKey: ArrayBuffer, algorithm: 'sha1' | 'sha256') => Promise; rsaDecrypt: (data: ArrayBuffer, privateKey: ArrayBuffer, algorithm: 'sha1' | 'sha256') => Promise; rsaExtractPublicKey: (privateKey: ArrayBuffer) => Promise; + rsaGenerateKeyPair: (length: 1024 | 2048 | 4096) => Promise<[ArrayBuffer, ArrayBuffer]>; randomBytes: (length: number) => Promise; } diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index 8b89897324..336073e50e 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -3,7 +3,10 @@ import { Router } from '@angular/router'; import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; -import { RegisterRequest } from '../../models/request/registerRequest'; +import { + RegisterKeysRequest, + RegisterRequest, +} from '../../models/request/registerRequest'; import { ApiService } from '../../abstractions/api.service'; import { AuthService } from '../../abstractions/auth.service'; @@ -11,6 +14,7 @@ import { CryptoService } from '../../abstractions/crypto.service'; import { I18nService } from '../../abstractions/i18n.service'; export class RegisterComponent { + name: string = ''; email: string = ''; masterPassword: string = ''; confirmMasterPassword: string = ''; @@ -52,8 +56,18 @@ export class RegisterComponent { return; } + this.name = this.name === '' ? null : this.name; + this.email = this.email.toLowerCase(); + const key = await this.cryptoService.makeKey(this.masterPassword, this.email); + const encKey = await this.cryptoService.makeEncKey(key); + const hashedPassword = await this.cryptoService.hashPassword(this.masterPassword, key); + const keys = await this.cryptoService.makeKeyPair(encKey[0]); + const request = new RegisterRequest(this.email, this.name, hashedPassword, + this.hint, encKey[1].encryptedString); + request.keys = new RegisterKeysRequest(keys[0], keys[1].encryptedString); + try { - this.formPromise = this.register(); + this.formPromise = this.apiService.postRegister(request); await this.formPromise; this.analytics.eventTrack.next({ action: 'Registered' }); this.toasterService.popAsync('success', null, this.i18nService.t('newAccountCreated')); @@ -66,13 +80,4 @@ export class RegisterComponent { this.showPassword = !this.showPassword; document.getElementById(confirmField ? 'masterPasswordRetype' : 'masterPassword').focus(); } - - private async register() { - this.email = this.email.toLowerCase(); - const key = await this.cryptoService.makeKey(this.masterPassword, this.email); - const encKey = await this.cryptoService.makeEncKey(key); - const hashedPassword = await this.cryptoService.hashPassword(this.masterPassword, key); - const request = new RegisterRequest(this.email, hashedPassword, this.hint, encKey.encryptedString); - await this.apiService.postRegister(request); - } } diff --git a/src/models/request/registerRequest.ts b/src/models/request/registerRequest.ts index fc815d5bb7..9f375af0c1 100644 --- a/src/models/request/registerRequest.ts +++ b/src/models/request/registerRequest.ts @@ -4,12 +4,25 @@ export class RegisterRequest { masterPasswordHash: string; masterPasswordHint: string; key: string; + keys: RegisterKeysRequest; + token: string; + organizationUserId: string; - constructor(email: string, masterPasswordHash: string, masterPasswordHint: string, key: string) { - this.name = null; + constructor(email: string, name: string, masterPasswordHash: string, masterPasswordHint: string, key: string) { + this.name = name; this.email = email; this.masterPasswordHash = masterPasswordHash; this.masterPasswordHint = masterPasswordHint ? masterPasswordHint : null; this.key = key; } } + +export class RegisterKeysRequest { + publicKey: string; + encryptedPrivateKey: string; + + constructor(publicKey: string, encryptedPrivateKey: string) { + this.publicKey = publicKey; + this.encryptedPrivateKey = encryptedPrivateKey; + } +} diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 356d15ccae..43aa0e0fd2 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -229,8 +229,9 @@ export class CryptoService implements CryptoServiceAbstraction { return this.storageService.remove(Keys.encKey); } - clearPrivateKey(memoryOnly?: boolean): Promise { + clearKeyPair(memoryOnly?: boolean): Promise { this.privateKey = null; + this.publicKey = null; if (memoryOnly) { return Promise.resolve(); } @@ -251,7 +252,7 @@ export class CryptoService implements CryptoServiceAbstraction { this.clearKeyHash(), this.clearOrgKeys(), this.clearEncKey(), - this.clearPrivateKey(), + this.clearKeyPair(), ]); } @@ -281,6 +282,13 @@ export class CryptoService implements CryptoServiceAbstraction { return [encShareKey, new SymmetricCryptoKey(shareKey)]; } + async makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, CipherString]> { + const keyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048); + const publicB64 = Utils.fromBufferToB64(keyPair[0]); + const privateEnc = await this.encrypt(keyPair[1], key); + return [publicB64, privateEnc]; + } + async hashPassword(password: string, key: SymmetricCryptoKey): Promise { if (key == null) { key = await this.getKey(); @@ -293,20 +301,22 @@ export class CryptoService implements CryptoServiceAbstraction { return Utils.fromBufferToB64(hash); } - async makeEncKey(key: SymmetricCryptoKey): Promise { + async makeEncKey(key: SymmetricCryptoKey): Promise<[SymmetricCryptoKey, CipherString]> { const encKey = await this.cryptoFunctionService.randomBytes(64); + let encKeyEnc: CipherString = null; // TODO: Uncomment when we're ready to enable key stretching - return this.encrypt(encKey, key); + encKeyEnc = await this.encrypt(encKey, key); /* if (key.key.byteLength === 32) { const newKey = await this.stretchKey(key); - return this.encrypt(encKey, newKey); + encKeyEnc = await this.encrypt(encKey, newKey); } else if (key.key.byteLength === 64) { - return this.encrypt(encKey, key); + encKeyEnc = await this.encrypt(encKey, key); } else { throw new Error('Invalid key size.'); } */ + return [new SymmetricCryptoKey(encKey), encKeyEnc]; } async encrypt(plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey): Promise { diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts index bc744ce012..4d00022741 100644 --- a/src/services/lock.service.ts +++ b/src/services/lock.service.ts @@ -67,7 +67,7 @@ export class LockService implements LockServiceAbstraction { await Promise.all([ this.cryptoService.clearKey(), this.cryptoService.clearOrgKeys(true), - this.cryptoService.clearPrivateKey(true), + this.cryptoService.clearKeyPair(true), this.cryptoService.clearEncKey(true), ]); diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index 7af5de0ffb..eedacf5374 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -144,6 +144,32 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { return Promise.resolve(publicKeyArray.buffer); } + async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[ArrayBuffer, ArrayBuffer]> { + return new Promise<[ArrayBuffer, ArrayBuffer]>((resolve, reject) => { + forge.pki.rsa.generateKeyPair({ + bits: length, + workers: -1, + e: 0x10001, // 65537 + }, (error, keyPair) => { + if (error != null) { + reject(error); + return; + } + + const publicKeyAsn1 = (forge.pki as any).publicKeyToAsn1(keyPair.publicKey); + const publicKeyByteString = forge.asn1.toDer(publicKeyAsn1).getBytes(); + const publicKey = Utils.fromByteStringToArray(publicKeyByteString); + + const privateKeyAsn1 = (forge.pki as any).privateKeyToAsn1(keyPair.privateKey); + const privateKeyPkcs8 = (forge.pki as any).wrapRsaPrivateKey(privateKeyAsn1); + const privateKeyByteString = forge.asn1.toDer(privateKeyPkcs8).getBytes(); + const privateKey = Utils.fromByteStringToArray(privateKeyByteString); + + resolve([publicKey.buffer, privateKey.buffer]); + }); + }); + } + randomBytes(length: number): Promise { return new Promise((resolve, reject) => { crypto.randomBytes(length, (error, bytes) => { diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index f5617236b9..0c6c23d4e9 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -223,6 +223,20 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return await this.subtle.exportKey('spki', impPublicKey); } + async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[ArrayBuffer, ArrayBuffer]> { + const rsaParams = { + name: 'RSA-OAEP', + modulusLength: length, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537 + // Have to specify some algorithm + hash: { name: this.toWebCryptoAlgorithm('sha1') }, + }; + const keyPair = await this.subtle.generateKey(rsaParams, true, ['encrypt', 'decrypt']); + const publicKey = await this.subtle.exportKey('spki', keyPair.publicKey); + const privateKey = await this.subtle.exportKey('pkcs8', keyPair.privateKey); + return [publicKey, privateKey]; + } + randomBytes(length: number): Promise { const arr = new Uint8Array(length); this.crypto.getRandomValues(arr); From af43232567fa63911725929d549f2b01927fa243 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 3 Jul 2018 12:06:01 -0400 Subject: [PATCH 0346/1626] make keypair on login if missing --- src/abstractions/api.service.ts | 2 ++ src/angular/components/register.component.ts | 8 +++----- src/models/request/keysRequest.ts | 9 +++++++++ src/models/request/registerRequest.ts | 14 +++----------- src/services/api.service.ts | 5 +++++ src/services/auth.service.ts | 14 ++++++++++++++ 6 files changed, 36 insertions(+), 16 deletions(-) create mode 100644 src/models/request/keysRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 21dc924ec1..6e1a21261c 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -12,6 +12,7 @@ import { FolderRequest } from '../models/request/folderRequest'; import { ImportCiphersRequest } from '../models/request/importCiphersRequest'; import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest'; +import { KeysRequest } from '../models/request/keysRequest'; import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest'; import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; @@ -74,6 +75,7 @@ export abstract class ApiService { postAccountStorage: (request: StorageRequest) => Promise; postAccountPayment: (request: PaymentRequest) => Promise; postAccountLicense: (data: FormData) => Promise; + postAccountKeys: (request: KeysRequest) => Promise; postFolder: (request: FolderRequest) => Promise; putFolder: (id: string, request: FolderRequest) => Promise; deleteFolder: (id: string) => Promise; diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index 336073e50e..a83c89f5f0 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -3,10 +3,8 @@ import { Router } from '@angular/router'; import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; -import { - RegisterKeysRequest, - RegisterRequest, -} from '../../models/request/registerRequest'; +import { KeysRequest } from '../../models/request/keysRequest'; +import { RegisterRequest } from '../../models/request/registerRequest'; import { ApiService } from '../../abstractions/api.service'; import { AuthService } from '../../abstractions/auth.service'; @@ -64,7 +62,7 @@ export class RegisterComponent { const keys = await this.cryptoService.makeKeyPair(encKey[0]); const request = new RegisterRequest(this.email, this.name, hashedPassword, this.hint, encKey[1].encryptedString); - request.keys = new RegisterKeysRequest(keys[0], keys[1].encryptedString); + request.keys = new KeysRequest(keys[0], keys[1].encryptedString); try { this.formPromise = this.apiService.postRegister(request); diff --git a/src/models/request/keysRequest.ts b/src/models/request/keysRequest.ts new file mode 100644 index 0000000000..6ce11e8e00 --- /dev/null +++ b/src/models/request/keysRequest.ts @@ -0,0 +1,9 @@ +export class KeysRequest { + publicKey: string; + encryptedPrivateKey: string; + + constructor(publicKey: string, encryptedPrivateKey: string) { + this.publicKey = publicKey; + this.encryptedPrivateKey = encryptedPrivateKey; + } +} diff --git a/src/models/request/registerRequest.ts b/src/models/request/registerRequest.ts index 9f375af0c1..7d3e25fc36 100644 --- a/src/models/request/registerRequest.ts +++ b/src/models/request/registerRequest.ts @@ -1,10 +1,12 @@ +import { KeysRequest } from './keysRequest'; + export class RegisterRequest { name: string; email: string; masterPasswordHash: string; masterPasswordHint: string; key: string; - keys: RegisterKeysRequest; + keys: KeysRequest; token: string; organizationUserId: string; @@ -16,13 +18,3 @@ export class RegisterRequest { this.key = key; } } - -export class RegisterKeysRequest { - publicKey: string; - encryptedPrivateKey: string; - - constructor(publicKey: string, encryptedPrivateKey: string) { - this.publicKey = publicKey; - this.encryptedPrivateKey = encryptedPrivateKey; - } -} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 13c0a2d314..0380749014 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -18,6 +18,7 @@ import { FolderRequest } from '../models/request/folderRequest'; import { ImportCiphersRequest } from '../models/request/importCiphersRequest'; import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest'; +import { KeysRequest } from '../models/request/keysRequest'; import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest'; import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; @@ -218,6 +219,10 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/accounts/license', data, true, false); } + postAccountKeys(request: KeysRequest): Promise { + return this.send('POST', '/accounts/keys', request, true, false); + } + // Folder APIs async postFolder(request: FolderRequest): Promise { diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 3c06bc6f4f..3e2364acf0 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -4,6 +4,7 @@ import { AuthResult } from '../models/domain/authResult'; import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; import { DeviceRequest } from '../models/request/deviceRequest'; +import { KeysRequest } from '../models/request/keysRequest'; import { TokenRequest } from '../models/request/tokenRequest'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; @@ -239,6 +240,19 @@ export class AuthService { await this.cryptoService.setKey(key); await this.cryptoService.setKeyHash(hashedPassword); await this.cryptoService.setEncKey(tokenResponse.key); + + // User doesn't have a key pair yet (old account), let's generate one for them + if (tokenResponse.privateKey == null) { + try { + const keyPair = await this.cryptoService.makeKeyPair(); + await this.apiService.postAccountKeys(new KeysRequest(keyPair[0], keyPair[1].encryptedString)); + tokenResponse.privateKey = keyPair[1].encryptedString; + } catch (e) { + // tslint:disable-next-line + console.error(e); + } + } + await this.cryptoService.setEncPrivateKey(tokenResponse.privateKey); } From ff8c1dfea9a3a6e42e2191dd3816889ce43045e0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 3 Jul 2018 23:33:15 -0400 Subject: [PATCH 0347/1626] org vault listing from apis --- src/abstractions/api.service.ts | 3 +++ src/abstractions/cipher.service.ts | 1 + src/abstractions/collection.service.ts | 1 + src/angular/components/ciphers.component.ts | 24 +++++++++++-------- src/angular/components/groupings.component.ts | 19 +++++++++++++-- src/models/domain/organization.ts | 11 +++++++++ src/models/response/cipherResponse.ts | 6 ++--- src/services/api.service.ts | 14 +++++++++++ src/services/cipher.service.ts | 6 ++--- src/services/collection.service.ts | 2 +- 10 files changed, 68 insertions(+), 19 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 6e1a21261c..15c6a51b37 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -34,6 +34,7 @@ import { UpdateTwoFactorYubioOtpRequest } from '../models/request/updateTwoFacto import { BillingResponse } from '../models/response/billingResponse'; import { CipherResponse } from '../models/response/cipherResponse'; +import { CollectionResponse } from '../models/response/collectionResponse'; import { DomainsResponse } from '../models/response/domainsResponse'; import { FolderResponse } from '../models/response/folderResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; @@ -79,6 +80,7 @@ export abstract class ApiService { postFolder: (request: FolderRequest) => Promise; putFolder: (id: string, request: FolderRequest) => Promise; deleteFolder: (id: string) => Promise; + getCiphersOrganization: (organizationId: string) => Promise>; postCipher: (request: CipherRequest) => Promise; putCipher: (id: string, request: CipherRequest) => Promise; deleteCipher: (id: string) => Promise; @@ -94,6 +96,7 @@ export abstract class ApiService { deleteCipherAttachment: (id: string, attachmentId: string) => Promise; postShareCipherAttachment: (id: string, attachmentId: string, data: FormData, organizationId: string) => Promise; + getCollections: (organizationId: string) => Promise>; getSync: () => Promise; postImportDirectory: (organizationId: string, request: ImportDirectoryRequest) => Promise; getSettingsDomains: () => Promise; diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index c1072e484d..5fa873017d 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -44,4 +44,5 @@ export abstract class CipherService { deleteAttachmentWithServer: (id: string, attachmentId: string) => Promise; sortCiphersByLastUsed: (a: any, b: any) => number; sortCiphersByLastUsedThenName: (a: any, b: any) => number; + getLocaleSortingFunction: () => (a: CipherView, b: CipherView) => number; } diff --git a/src/abstractions/collection.service.ts b/src/abstractions/collection.service.ts index 6c51478555..7960dfd7bd 100644 --- a/src/abstractions/collection.service.ts +++ b/src/abstractions/collection.service.ts @@ -15,4 +15,5 @@ export abstract class CollectionService { replace: (collections: { [id: string]: CollectionData; }) => Promise; clear: (userId: string) => Promise; delete: (id: string | string[]) => Promise; + getLocaleSortingFunction: () => (a: CollectionView, b: CollectionView) => number; } diff --git a/src/angular/components/ciphers.component.ts b/src/angular/components/ciphers.component.ts index 40f1899346..50f460be43 100644 --- a/src/angular/components/ciphers.component.ts +++ b/src/angular/components/ciphers.component.ts @@ -19,20 +19,15 @@ export class CiphersComponent { ciphers: CipherView[] = []; searchText: string; searchPlaceholder: string = null; - private filter: (cipher: CipherView) => boolean = null; + + protected allCiphers: CipherView[] = []; + protected filter: (cipher: CipherView) => boolean = null; constructor(protected cipherService: CipherService) { } async load(filter: (cipher: CipherView) => boolean = null) { - this.filter = filter; - const ciphers = await this.cipherService.getAllDecrypted(); - - if (this.filter == null) { - this.ciphers = ciphers; - } else { - this.ciphers = ciphers.filter(this.filter); - } - + this.allCiphers = await this.cipherService.getAllDecrypted(); + this.applyFilter(filter); this.loaded = true; } @@ -42,6 +37,15 @@ export class CiphersComponent { await this.load(this.filter); } + async applyFilter(filter: (cipher: CipherView) => boolean = null) { + this.filter = filter; + if (this.filter == null) { + this.ciphers = this.allCiphers; + } else { + this.ciphers = this.allCiphers.filter(this.filter); + } + } + selectCipher(cipher: CipherView) { this.onCipherClicked.emit(cipher); } diff --git a/src/angular/components/groupings.component.ts b/src/angular/components/groupings.component.ts index 0e3a000afe..605ce4bc58 100644 --- a/src/angular/components/groupings.component.ts +++ b/src/angular/components/groupings.component.ts @@ -14,6 +14,10 @@ import { CollectionService } from '../../abstractions/collection.service'; import { FolderService } from '../../abstractions/folder.service'; export class GroupingsComponent { + @Input() showFolders = true; + @Input() showCollections = true; + @Input() showFavorites = true; + @Output() onAllClicked = new EventEmitter(); @Output() onFavoritesClicked = new EventEmitter(); @Output() onCipherTypeClicked = new EventEmitter(); @@ -44,11 +48,22 @@ export class GroupingsComponent { } } - async loadCollections() { - this.collections = await this.collectionService.getAllDecrypted(); + async loadCollections(organizationId?: string) { + if (!this.showCollections) { + return; + } + const collections = await this.collectionService.getAllDecrypted(); + if (organizationId != null) { + this.collections = collections.filter((c) => c.organizationId === organizationId); + } else { + this.collections = collections; + } } async loadFolders() { + if (!this.showFolders) { + return; + } this.folders = await this.folderService.getAllDecrypted(); } diff --git a/src/models/domain/organization.ts b/src/models/domain/organization.ts index 69f93b34c0..ce243e838a 100644 --- a/src/models/domain/organization.ts +++ b/src/models/domain/organization.ts @@ -21,4 +21,15 @@ export class Organization { this.type = obj.type; this.enabled = obj.enabled; } + + get canAccess() { + if (this.type === OrganizationUserType.Owner) { + return true; + } + return this.enabled && this.status === OrganizationUserStatusType.Confirmed; + } + + get isAdmin() { + return this.type === OrganizationUserType.Owner || this.type === OrganizationUserType.Admin; + } } diff --git a/src/models/response/cipherResponse.ts b/src/models/response/cipherResponse.ts index 61c8907310..452a88d1b5 100644 --- a/src/models/response/cipherResponse.ts +++ b/src/models/response/cipherResponse.ts @@ -28,12 +28,12 @@ export class CipherResponse { constructor(response: any) { this.id = response.Id; this.organizationId = response.OrganizationId; - this.folderId = response.FolderId; + this.folderId = response.FolderId || null; this.type = response.Type; this.name = response.Name; this.notes = response.Notes; - this.favorite = response.Favorite; - this.edit = response.Edit; + this.favorite = response.Favorite || false; + this.edit = response.Edit || true; this.organizationUseTotp = response.OrganizationUseTotp; this.revisionDate = new Date(response.RevisionDate); diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 0380749014..d6938f47e7 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -40,6 +40,7 @@ import { UpdateTwoFactorYubioOtpRequest } from '../models/request/updateTwoFacto import { BillingResponse } from '../models/response/billingResponse'; import { CipherResponse } from '../models/response/cipherResponse'; +import { CollectionResponse } from '../models/response/collectionResponse'; import { DomainsResponse } from '../models/response/domainsResponse'; import { ErrorResponse } from '../models/response/errorResponse'; import { FolderResponse } from '../models/response/folderResponse'; @@ -241,6 +242,12 @@ export class ApiService implements ApiServiceAbstraction { // Cipher APIs + async getCiphersOrganization(organizationId: string): Promise> { + const r = await this.send('GET', '/ciphers/organization-details?organizationId=' + organizationId, + null, true, true); + return new ListResponse(r, CipherResponse); + } + async postCipher(request: CipherRequest): Promise { const r = await this.send('POST', '/ciphers', request, true, true); return new CipherResponse(r); @@ -304,6 +311,13 @@ export class ApiService implements ApiServiceAbstraction { attachmentId + '/share?organizationId=' + organizationId, data, true, false); } + // Collections APIs + + async getCollections(organizationId: string): Promise> { + const r = await this.send('GET', '/organizations/' + organizationId + '/collections', null, true, true); + return new ListResponse(r, CollectionResponse); + } + // Sync APIs async getSync(): Promise { diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index e1d6152bf3..6215fbd2fa 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -619,9 +619,7 @@ export class CipherService implements CipherServiceAbstraction { return this.getLocaleSortingFunction()(a, b); } - // Helpers - - private getLocaleSortingFunction(): (a: CipherView, b: CipherView) => number { + getLocaleSortingFunction(): (a: CipherView, b: CipherView) => number { return (a, b) => { let aName = a.name; let bName = b.name; @@ -656,6 +654,8 @@ export class CipherService implements CipherServiceAbstraction { }; } + // Helpers + private async encryptObjProperty(model: V, obj: D, map: any, key: SymmetricCryptoKey): Promise { const promises = []; diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index 02821bcba9..c76fbb6299 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -125,7 +125,7 @@ export class CollectionService implements CollectionServiceAbstraction { this.decryptedCollectionCache = null; } - private getLocaleSortingFunction(): (a: CollectionView, b: CollectionView) => number { + getLocaleSortingFunction(): (a: CollectionView, b: CollectionView) => number { return (a, b) => { if (a.name == null && b.name != null) { return -1; From 236c00475d54a6cbc8c39525642d9fb36b0d53b5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 3 Jul 2018 23:46:04 -0400 Subject: [PATCH 0348/1626] remove 4096 test --- spec/web/services/webCryptoFunction.service.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index 50605b10fb..8ee1fe691e 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -259,7 +259,9 @@ describe('WebCrypto Function Service', () => { describe('rsaGenerateKeyPair', () => { testRsaGenerateKeyPair(1024); testRsaGenerateKeyPair(2048); - testRsaGenerateKeyPair(4096); + + // Generating 4096 bit keys can be slow. Commenting it out to save CI. + // testRsaGenerateKeyPair(4096); }); describe('randomBytes', () => { From 278b4402da94ab36b4def76f520599a23308f7f1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 4 Jul 2018 09:54:14 -0400 Subject: [PATCH 0349/1626] api for leaving organization --- src/abstractions/api.service.ts | 1 + src/angular/components/ciphers.component.ts | 2 +- src/services/api.service.ts | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 15c6a51b37..c8bac36130 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -119,5 +119,6 @@ export abstract class ApiService { postTwoFactorEmailSetup: (request: TwoFactorEmailRequest) => Promise; postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise; postOrganization: (request: OrganizationCreateRequest) => Promise; + postLeaveOrganization: (id: string) => Promise; postOrganizationLicense: (data: FormData) => Promise; } diff --git a/src/angular/components/ciphers.component.ts b/src/angular/components/ciphers.component.ts index 50f460be43..fea70ce280 100644 --- a/src/angular/components/ciphers.component.ts +++ b/src/angular/components/ciphers.component.ts @@ -37,7 +37,7 @@ export class CiphersComponent { await this.load(this.filter); } - async applyFilter(filter: (cipher: CipherView) => boolean = null) { + applyFilter(filter: (cipher: CipherView) => boolean = null) { this.filter = filter; if (this.filter == null) { this.ciphers = this.allCiphers; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index d6938f47e7..a81cf77c34 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -429,6 +429,10 @@ export class ApiService implements ApiServiceAbstraction { return new OrganizationResponse(r); } + postLeaveOrganization(id: string): Promise { + return this.send('POST', '/organizations/' + id + '/leave', null, true, false); + } + async postOrganizationLicense(data: FormData): Promise { const r = await this.send('POST', '/organizations/license', data, true, true); return new OrganizationResponse(r); From 9b008ff382d1dd6682185cf092afbf02a83ce951 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 5 Jul 2018 09:42:13 -0400 Subject: [PATCH 0350/1626] admin cipher apis --- src/abstractions/api.service.ts | 8 +++++ src/angular/components/add-edit.component.ts | 20 +++++++++-- src/services/api.service.ts | 37 ++++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index c8bac36130..b466f04f45 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -80,20 +80,28 @@ export abstract class ApiService { postFolder: (request: FolderRequest) => Promise; putFolder: (id: string, request: FolderRequest) => Promise; deleteFolder: (id: string) => Promise; + getCipher: (id: string) => Promise; + getCipherAdmin: (id: string) => Promise; getCiphersOrganization: (organizationId: string) => Promise>; postCipher: (request: CipherRequest) => Promise; + postCipherAdmin: (request: CipherRequest) => Promise; putCipher: (id: string, request: CipherRequest) => Promise; + putCipherAdmin: (id: string, request: CipherRequest) => Promise; deleteCipher: (id: string) => Promise; + deleteCipherAdmin: (id: string) => Promise; deleteManyCiphers: (request: CipherBulkDeleteRequest) => Promise; putMoveCiphers: (request: CipherBulkMoveRequest) => Promise; putShareCipher: (id: string, request: CipherShareRequest) => Promise; putShareCiphers: (request: CipherBulkShareRequest) => Promise; putCipherCollections: (id: string, request: CipherCollectionsRequest) => Promise; + putCipherCollectionsAdmin: (id: string, request: CipherCollectionsRequest) => Promise; postPurgeCiphers: (request: PasswordVerificationRequest) => Promise; postImportCiphers: (request: ImportCiphersRequest) => Promise; postImportOrganizationCiphers: (request: ImportOrganizationCiphersRequest) => Promise; postCipherAttachment: (id: string, data: FormData) => Promise; + postCipherAttachmentAdmin: (id: string, data: FormData) => Promise; deleteCipherAttachment: (id: string, attachmentId: string) => Promise; + deleteCipherAttachmentAdmin: (id: string, attachmentId: string) => Promise; postShareCipherAttachment: (id: string, attachmentId: string, data: FormData, organizationId: string) => Promise; getCollections: (organizationId: string) => Promise>; diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 3034c377f2..654353c28a 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -19,6 +19,8 @@ import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { StateService } from '../../abstractions/state.service'; +import { Cipher } from '../../models/domain/cipher'; + import { CardView } from '../../models/view/cardView'; import { CipherView } from '../../models/view/cipherView'; import { FieldView } from '../../models/view/fieldView'; @@ -130,7 +132,7 @@ export class AddEditComponent { await this.stateService.remove('addEditCipher'); if (this.cipher == null) { if (this.editMode) { - const cipher = await this.cipherService.get(this.cipherId); + const cipher = await this.loadCipher(); this.cipher = await cipher.decrypt(); } else { this.cipher = new CipherView(); @@ -163,7 +165,7 @@ export class AddEditComponent { const cipher = await this.cipherService.encrypt(this.cipher); try { - this.formPromise = this.cipherService.saveWithServer(cipher); + this.formPromise = this.saveCipher(cipher); await this.formPromise; this.cipher.id = cipher.id; this.analytics.eventTrack.next({ action: this.editMode ? 'Edited Cipher' : 'Added Cipher' }); @@ -233,7 +235,7 @@ export class AddEditComponent { } try { - this.deletePromise = this.cipherService.deleteWithServer(this.cipher.id); + this.deletePromise = this.deleteCipher(); await this.deletePromise; this.analytics.eventTrack.next({ action: 'Deleted Cipher' }); this.toasterService.popAsync('success', null, this.i18nService.t('deletedItem')); @@ -304,4 +306,16 @@ export class AddEditComponent { this.toasterService.popAsync('success', null, this.i18nService.t('passwordSafe')); } } + + protected loadCipher() { + return this.cipherService.get(this.cipherId); + } + + protected saveCipher(cipher: Cipher) { + return this.cipherService.saveWithServer(cipher); + } + + protected deleteCipher() { + return this.cipherService.deleteWithServer(this.cipher.id); + } } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index a81cf77c34..a8aee56913 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -242,6 +242,16 @@ export class ApiService implements ApiServiceAbstraction { // Cipher APIs + async getCipher(id: string): Promise { + const r = await this.send('GET', '/ciphers/' + id, null, true, true); + return new CipherResponse(r); + } + + async getCipherAdmin(id: string): Promise { + const r = await this.send('GET', '/ciphers/' + id + '/admin', null, true, true); + return new CipherResponse(r); + } + async getCiphersOrganization(organizationId: string): Promise> { const r = await this.send('GET', '/ciphers/organization-details?organizationId=' + organizationId, null, true, true); @@ -253,15 +263,29 @@ export class ApiService implements ApiServiceAbstraction { return new CipherResponse(r); } + async postCipherAdmin(request: CipherRequest): Promise { + const r = await this.send('POST', '/ciphers/admin', request, true, true); + return new CipherResponse(r); + } + async putCipher(id: string, request: CipherRequest): Promise { const r = await this.send('PUT', '/ciphers/' + id, request, true, true); return new CipherResponse(r); } + async putCipherAdmin(id: string, request: CipherRequest): Promise { + const r = await this.send('PUT', '/ciphers/' + id + '/admin', request, true, true); + return new CipherResponse(r); + } + deleteCipher(id: string): Promise { return this.send('DELETE', '/ciphers/' + id, null, true, false); } + deleteCipherAdmin(id: string): Promise { + return this.send('DELETE', '/ciphers/' + id + '/admin', null, true, false); + } + deleteManyCiphers(request: CipherBulkDeleteRequest): Promise { return this.send('DELETE', '/ciphers', request, true, false); } @@ -282,6 +306,10 @@ export class ApiService implements ApiServiceAbstraction { return this.send('PUT', '/ciphers/' + id + '/collections', request, true, false); } + putCipherCollectionsAdmin(id: string, request: CipherCollectionsRequest): Promise { + return this.send('PUT', '/ciphers/' + id + '/collections-admin', request, true, false); + } + postPurgeCiphers(request: PasswordVerificationRequest): Promise { return this.send('POST', '/ciphers/purge', request, true, false); } @@ -301,10 +329,19 @@ export class ApiService implements ApiServiceAbstraction { return new CipherResponse(r); } + async postCipherAttachmentAdmin(id: string, data: FormData): Promise { + const r = await this.send('POST', '/ciphers/' + id + '/attachment-admin', data, true, true); + return new CipherResponse(r); + } + deleteCipherAttachment(id: string, attachmentId: string): Promise { return this.send('DELETE', '/ciphers/' + id + '/attachment/' + attachmentId, null, true, false); } + deleteCipherAttachmentAdmin(id: string, attachmentId: string): Promise { + return this.send('DELETE', '/ciphers/' + id + '/attachment/' + attachmentId + '/admin', null, true, false); + } + postShareCipherAttachment(id: string, attachmentId: string, data: FormData, organizationId: string): Promise { return this.send('POST', '/ciphers/' + id + '/attachment/' + From ef5eebba66d606d53cf4bc730426ea393276e801 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 5 Jul 2018 10:10:15 -0400 Subject: [PATCH 0351/1626] allow overridable encrypt function --- src/angular/components/add-edit.component.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 654353c28a..0e9f2cdbbc 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -157,13 +157,13 @@ export class AddEditComponent { return false; } - if (!this.editMode && this.cipher.type === CipherType.Login && this.cipher.login.uris.length === 1 && + if (!this.editMode && this.cipher.type === CipherType.Login && + this.cipher.login.uris != null && this.cipher.login.uris.length === 1 && (this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === '')) { this.cipher.login.uris = null; } - const cipher = await this.cipherService.encrypt(this.cipher); - + const cipher = await this.encryptCipher(); try { this.formPromise = this.saveCipher(cipher); await this.formPromise; @@ -311,6 +311,10 @@ export class AddEditComponent { return this.cipherService.get(this.cipherId); } + protected encryptCipher() { + return this.cipherService.encrypt(this.cipher); + } + protected saveCipher(cipher: Cipher) { return this.cipherService.saveWithServer(cipher); } From 47ab71e73098d1b83a1bdcbae3cd3e2b1d9ccacc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 5 Jul 2018 10:48:19 -0400 Subject: [PATCH 0352/1626] admin functions for cipher attachments --- src/abstractions/cipher.service.ts | 5 +++-- .../components/attachments.component.ts | 19 +++++++++++++++---- src/services/cipher.service.ts | 19 +++++++++++++------ 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index 5fa873017d..39176d1204 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -30,8 +30,9 @@ export abstract class CipherService { shareManyWithServer: (ciphers: CipherView[], organizationId: string, collectionIds: string[]) => Promise; shareAttachmentWithServer: (attachmentView: AttachmentView, cipherId: string, organizationId: string) => Promise; - saveAttachmentWithServer: (cipher: Cipher, unencryptedFile: any) => Promise; - saveAttachmentRawWithServer: (cipher: Cipher, filename: string, data: ArrayBuffer) => Promise; + saveAttachmentWithServer: (cipher: Cipher, unencryptedFile: any, admin?: boolean) => Promise; + saveAttachmentRawWithServer: (cipher: Cipher, filename: string, data: ArrayBuffer, + admin?: boolean) => Promise; saveCollectionsWithServer: (cipher: Cipher) => Promise; upsert: (cipher: CipherData | CipherData[]) => Promise; replace: (ciphers: { [id: string]: CipherData; }) => Promise; diff --git a/src/angular/components/attachments.component.ts b/src/angular/components/attachments.component.ts index fff40c4b0b..6429c1cdd9 100644 --- a/src/angular/components/attachments.component.ts +++ b/src/angular/components/attachments.component.ts @@ -37,7 +37,7 @@ export class AttachmentsComponent implements OnInit { protected platformUtilsService: PlatformUtilsService, protected win: Window) { } async ngOnInit() { - this.cipherDomain = await this.cipherService.get(this.cipherId); + this.cipherDomain = await this.loadCipher(); this.cipher = await this.cipherDomain.decrypt(); const key = await this.cryptoService.getEncKey(); @@ -84,7 +84,7 @@ export class AttachmentsComponent implements OnInit { } try { - this.formPromise = this.cipherService.saveAttachmentWithServer(this.cipherDomain, files[0]); + this.formPromise = this.saveCipherAttachment(files[0]); this.cipherDomain = await this.formPromise; this.cipher = await this.cipherDomain.decrypt(); this.analytics.eventTrack.next({ action: 'Added Attachment' }); @@ -112,8 +112,7 @@ export class AttachmentsComponent implements OnInit { } try { - this.deletePromises[attachment.id] = this.cipherService.deleteAttachmentWithServer( - this.cipher.id, attachment.id); + this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id); await this.deletePromises[attachment.id]; this.analytics.eventTrack.next({ action: 'Deleted Attachment' }); this.toasterService.popAsync('success', null, this.i18nService.t('deletedAttachment')); @@ -158,4 +157,16 @@ export class AttachmentsComponent implements OnInit { a.downloading = false; } + + protected loadCipher() { + return this.cipherService.get(this.cipherId); + } + + protected saveCipherAttachment(file: File) { + return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file); + } + + protected deleteCipherAttachment(attachmentId: string) { + return this.cipherService.deleteAttachmentWithServer(this.cipher.id, attachmentId); + } } diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 6215fbd2fa..54cc1517b8 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -418,14 +418,14 @@ export class CipherService implements CipherServiceAbstraction { } } - saveAttachmentWithServer(cipher: Cipher, unencryptedFile: any): Promise { + saveAttachmentWithServer(cipher: Cipher, unencryptedFile: any, admin = false): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsArrayBuffer(unencryptedFile); reader.onload = async (evt: any) => { try { const cData = await this.saveAttachmentRawWithServer(cipher, - unencryptedFile.name, evt.target.result); + unencryptedFile.name, evt.target.result, admin); resolve(cData); } catch (e) { reject(e); @@ -437,7 +437,8 @@ export class CipherService implements CipherServiceAbstraction { }); } - async saveAttachmentRawWithServer(cipher: Cipher, filename: string, data: ArrayBuffer): Promise { + async saveAttachmentRawWithServer(cipher: Cipher, filename: string, + data: ArrayBuffer, admin = false): Promise { const key = await this.cryptoService.getOrgKey(cipher.organizationId); const encFileName = await this.cryptoService.encrypt(filename, key); const encData = await this.cryptoService.encryptToBytes(data, key); @@ -459,20 +460,26 @@ export class CipherService implements CipherServiceAbstraction { let response: CipherResponse; try { - response = await this.apiService.postCipherAttachment(cipher.id, fd); + if (admin) { + response = await this.apiService.postCipherAttachmentAdmin(cipher.id, fd); + } else { + response = await this.apiService.postCipherAttachment(cipher.id, fd); + } } catch (e) { throw new Error((e as ErrorResponse).getSingleMessage()); } const userId = await this.userService.getUserId(); const cData = new CipherData(response, userId, cipher.collectionIds); - this.upsert(cData); + if (!admin) { + this.upsert(cData); + } return new Cipher(cData); } async saveCollectionsWithServer(cipher: Cipher): Promise { const request = new CipherCollectionsRequest(cipher.collectionIds); - const response = await this.apiService.putCipherCollections(cipher.id, request); + await this.apiService.putCipherCollections(cipher.id, request); const userId = await this.userService.getUserId(); const data = cipher.toCipherData(userId); await this.upsert(data); From 87e273252be42dab90d9a33857fe7755f378338b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 5 Jul 2018 14:39:58 -0400 Subject: [PATCH 0353/1626] exporting organization data --- src/abstractions/export.service.ts | 5 +- src/angular/components/export.component.ts | 22 ++- src/services/export.service.ts | 185 +++++++++++++++------ 3 files changed, 152 insertions(+), 60 deletions(-) diff --git a/src/abstractions/export.service.ts b/src/abstractions/export.service.ts index e03ce20644..7f9f27234b 100644 --- a/src/abstractions/export.service.ts +++ b/src/abstractions/export.service.ts @@ -1,4 +1,5 @@ export abstract class ExportService { - getCsv: () => Promise; - getFileName: () => string; + getExport: (format?: 'csv' | 'json') => Promise; + getOrganizationExport: (organizationId: string, format?: 'csv' | 'json') => Promise; + getFileName: (prefix?: string) => string; } diff --git a/src/angular/components/export.component.ts b/src/angular/components/export.component.ts index 964d8f61b7..c509354f57 100644 --- a/src/angular/components/export.component.ts +++ b/src/angular/components/export.component.ts @@ -15,6 +15,7 @@ import { UserService } from '../../abstractions/user.service'; export class ExportComponent { @Output() onSaved = new EventEmitter(); + formPromise: Promise; masterPassword: string; showPassword = false; @@ -36,10 +37,13 @@ export class ExportComponent { const storedKeyHash = await this.cryptoService.getKeyHash(); if (storedKeyHash != null && keyHash != null && storedKeyHash === keyHash) { - const csv = await this.exportService.getCsv(); - this.analytics.eventTrack.next({ action: 'Exported Data' }); - this.downloadFile(csv); - this.saved(); + try { + this.formPromise = this.getExportData(); + const data = await this.formPromise; + this.analytics.eventTrack.next({ action: 'Exported Data' }); + this.downloadFile(data); + this.saved(); + } catch { } } else { this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), this.i18nService.t('invalidMasterPassword')); @@ -56,8 +60,16 @@ export class ExportComponent { this.onSaved.emit(); } + protected getExportData() { + return this.exportService.getExport('csv'); + } + + protected getFileName(prefix?: string) { + return this.exportService.getFileName(prefix); + } + private downloadFile(csv: string): void { - const fileName = this.exportService.getFileName(); + const fileName = this.getFileName(); this.platformUtilsService.saveFile(this.win, csv, { type: 'text/plain' }, fileName); } } diff --git a/src/services/export.service.ts b/src/services/export.service.ts index e62b089c97..add422a052 100644 --- a/src/services/export.service.ts +++ b/src/services/export.service.ts @@ -2,17 +2,26 @@ import * as papa from 'papaparse'; import { CipherType } from '../enums/cipherType'; +import { ApiService } from '../abstractions/api.service'; import { CipherService } from '../abstractions/cipher.service'; import { ExportService as ExportServiceAbstraction } from '../abstractions/export.service'; import { FolderService } from '../abstractions/folder.service'; import { CipherView } from '../models/view/cipherView'; +import { CollectionView } from '../models/view/collectionView'; import { FolderView } from '../models/view/folderView'; -export class ExportService implements ExportServiceAbstraction { - constructor(private folderService: FolderService, private cipherService: CipherService) { } +import { Cipher } from '../models/domain/cipher'; +import { Collection } from '../models/domain/collection'; - async getCsv(): Promise { +import { CipherData } from '../models/data/cipherData'; +import { CollectionData } from '../models/data/collectionData'; + +export class ExportService implements ExportServiceAbstraction { + constructor(private folderService: FolderService, private cipherService: CipherService, + private apiService: ApiService) { } + + async getExport(format: 'csv' | 'json' = 'csv'): Promise { let decFolders: FolderView[] = []; let decCiphers: CipherView[] = []; const promises = []; @@ -43,67 +52,90 @@ export class ExportService implements ExportServiceAbstraction { return; } - const cipher: any = { - folder: c.folderId && foldersMap.has(c.folderId) ? foldersMap.get(c.folderId).name : null, - favorite: c.favorite ? 1 : null, - type: null, - name: c.name, - notes: c.notes, - fields: null, - // Login props - login_uri: null, - login_username: null, - login_password: null, - login_totp: null, - }; - - if (c.fields) { - c.fields.forEach((f: any) => { - if (!cipher.fields) { - cipher.fields = ''; - } else { - cipher.fields += '\n'; - } - - cipher.fields += ((f.name || '') + ': ' + f.value); - }); - } - - switch (c.type) { - case CipherType.Login: - cipher.type = 'login'; - cipher.login_username = c.login.username; - cipher.login_password = c.login.password; - cipher.login_totp = c.login.totp; - - if (c.login.uris) { - cipher.login_uri = []; - c.login.uris.forEach((u) => { - cipher.login_uri.push(u.uri); - }); - } - break; - case CipherType.SecureNote: - cipher.type = 'note'; - break; - default: - return; - } - + const cipher: any = {}; + cipher.folder = c.folderId != null && foldersMap.has(c.folderId) ? foldersMap.get(c.folderId).name : null; + cipher.favorite = c.favorite ? 1 : null; + this.buildCommonCipher(cipher, c); exportCiphers.push(cipher); }); - return papa.unparse(exportCiphers); + if (format === 'csv') { + return papa.unparse(exportCiphers); + } else { + return JSON.stringify(exportCiphers, null, ' '); + } } - getFileName(): string { + async getOrganizationExport(organizationId: string, format: 'csv' | 'json' = 'csv'): Promise { + const decCollections: CollectionView[] = []; + const decCiphers: CipherView[] = []; + const promises = []; + + promises.push(this.apiService.getCollections(organizationId).then((collections) => { + const collectionPromises = []; + if (collections != null && collections.data != null && collections.data.length > 0) { + collections.data.forEach((c) => { + const collection = new Collection(new CollectionData(c)); + collectionPromises.push(collection.decrypt().then((decCol) => { + decCollections.push(decCol); + })); + }); + } + return Promise.all(collectionPromises); + })); + + promises.push(this.apiService.getCiphersOrganization(organizationId).then((ciphers) => { + const cipherPromises = []; + if (ciphers != null && ciphers.data != null && ciphers.data.length > 0) { + ciphers.data.forEach((c) => { + const cipher = new Cipher(new CipherData(c)); + cipherPromises.push(cipher.decrypt().then((decCipher) => { + decCiphers.push(decCipher); + })); + }); + } + return Promise.all(cipherPromises); + })); + + await Promise.all(promises); + + const collectionsMap = new Map(); + decCollections.forEach((c) => { + collectionsMap.set(c.id, c); + }); + + const exportCiphers: any[] = []; + decCiphers.forEach((c) => { + // only export logins and secure notes + if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) { + return; + } + + const cipher: any = {}; + cipher.collections = []; + if (c.collectionIds != null) { + cipher.collections = c.collectionIds.filter((id) => collectionsMap.has(id)) + .map((id) => collectionsMap.get(id).name); + } + this.buildCommonCipher(cipher, c); + exportCiphers.push(cipher); + }); + + if (format === 'csv') { + return papa.unparse(exportCiphers); + } else { + return JSON.stringify(exportCiphers, null, ' '); + } + } + + getFileName(prefix: string = null): string { const now = new Date(); const dateString = now.getFullYear() + '' + this.padNumber(now.getMonth() + 1, 2) + '' + this.padNumber(now.getDate(), 2) + this.padNumber(now.getHours(), 2) + '' + this.padNumber(now.getMinutes(), 2) + this.padNumber(now.getSeconds(), 2); - return 'bitwarden_export_' + dateString + '.csv'; + return 'bitwarden' + (prefix ? ('_' + prefix) : '') + '_export_' + dateString + '.csv'; } private padNumber(num: number, width: number, padCharacter: string = '0'): string { @@ -111,4 +143,51 @@ export class ExportService implements ExportServiceAbstraction { return numString.length >= width ? numString : new Array(width - numString.length + 1).join(padCharacter) + numString; } + + private buildCommonCipher(cipher: any, c: CipherView) { + cipher.type = null; + cipher.name = c.name; + cipher.notes = c.notes; + cipher.fields = null; + // Login props + cipher.login_uri = null; + cipher.login_username = null; + cipher.login_password = null; + cipher.login_totp = null; + + if (c.fields) { + c.fields.forEach((f: any) => { + if (!cipher.fields) { + cipher.fields = ''; + } else { + cipher.fields += '\n'; + } + + cipher.fields += ((f.name || '') + ': ' + f.value); + }); + } + + switch (c.type) { + case CipherType.Login: + cipher.type = 'login'; + cipher.login_username = c.login.username; + cipher.login_password = c.login.password; + cipher.login_totp = c.login.totp; + + if (c.login.uris) { + cipher.login_uri = []; + c.login.uris.forEach((u) => { + cipher.login_uri.push(u.uri); + }); + } + break; + case CipherType.SecureNote: + cipher.type = 'note'; + break; + default: + return; + } + + return cipher; + } } From 91081d92327da22ab3be88a60f8b71be26933370 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 5 Jul 2018 14:49:48 -0400 Subject: [PATCH 0354/1626] fix implicit any --- src/services/export.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/export.service.ts b/src/services/export.service.ts index add422a052..6a49d729f7 100644 --- a/src/services/export.service.ts +++ b/src/services/export.service.ts @@ -72,7 +72,7 @@ export class ExportService implements ExportServiceAbstraction { const promises = []; promises.push(this.apiService.getCollections(organizationId).then((collections) => { - const collectionPromises = []; + const collectionPromises: any = []; if (collections != null && collections.data != null && collections.data.length > 0) { collections.data.forEach((c) => { const collection = new Collection(new CollectionData(c)); @@ -85,7 +85,7 @@ export class ExportService implements ExportServiceAbstraction { })); promises.push(this.apiService.getCiphersOrganization(organizationId).then((ciphers) => { - const cipherPromises = []; + const cipherPromises: any = []; if (ciphers != null && ciphers.data != null && ciphers.data.length > 0) { ciphers.data.forEach((c) => { const cipher = new Cipher(new CipherData(c)); From 56cf068acd8e9a4da839f0bdea99ab9447932b2a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 5 Jul 2018 17:29:35 -0400 Subject: [PATCH 0355/1626] importer organization support --- src/importers/baseImporter.ts | 14 ++++++ src/importers/bitwardenCsvImporter.ts | 67 ++++++++++++++++++--------- src/importers/importer.ts | 2 +- src/importers/keepassxCsvImporter.ts | 28 +++++------ src/importers/lastpassCsvImporter.ts | 16 ++++--- 5 files changed, 85 insertions(+), 42 deletions(-) diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index 620627facb..d995d71221 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -1,5 +1,8 @@ import * as papa from 'papaparse'; +import { ImportResult } from '../models/domain/importResult'; + +import { CollectionView } from '../models/view/collectionView'; import { LoginUriView } from '../models/view/loginUriView'; import { Utils } from '../misc/utils'; @@ -194,4 +197,15 @@ export abstract class BaseImporter { return null; } + + protected moveFoldersToCollections(result: ImportResult) { + result.folderRelationships.forEach((value, key) => result.collectionRelationships.set(key, value)); + result.collections = result.folders.map((f) => { + const collection = new CollectionView(); + collection.name = f.name; + return collection; + }); + result.folderRelationships = new Map(); + result.folders = []; + } } diff --git a/src/importers/bitwardenCsvImporter.ts b/src/importers/bitwardenCsvImporter.ts index 4643e17021..acfdc3d1ed 100644 --- a/src/importers/bitwardenCsvImporter.ts +++ b/src/importers/bitwardenCsvImporter.ts @@ -4,6 +4,7 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; import { CipherView } from '../models/view/cipherView'; +import { CollectionView } from '../models/view/collectionView'; import { FieldView } from '../models/view/fieldView'; import { FolderView } from '../models/view/folderView'; import { LoginView } from '../models/view/loginView'; @@ -14,7 +15,7 @@ import { FieldType } from '../enums/fieldType'; import { SecureNoteType } from '../enums/secureNoteType'; export class BitwardenCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string, organization = false): ImportResult { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { @@ -23,24 +24,57 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer { } results.forEach((value) => { - let folderIndex = result.folders.length; - const cipherIndex = result.ciphers.length; - const hasFolder = !this.isNullOrWhitespace(value.folder); - let addFolder = hasFolder; + if (organization && !this.isNullOrWhitespace(value.collections)) { + const collections = (value.collections as string).split(','); + collections.forEach((col) => { + let addCollection = true; + let collectionIndex = result.collections.length; - if (hasFolder) { - for (let i = 0; i < result.folders.length; i++) { - if (result.folders[i].name === value.folder) { - addFolder = false; - folderIndex = i; - break; + for (let i = 0; i < result.collections.length; i++) { + if (result.collections[i].name === col) { + addCollection = false; + collectionIndex = i; + break; + } } + + if (addCollection) { + const collection = new CollectionView(); + collection.name = col; + result.collections.push(collection); + } + + result.collectionRelationships.set(result.ciphers.length, collectionIndex); + }); + } else if (!organization) { + let folderIndex = result.folders.length; + const hasFolder = !organization && !this.isNullOrWhitespace(value.folder); + let addFolder = hasFolder; + + if (hasFolder) { + for (let i = 0; i < result.folders.length; i++) { + if (result.folders[i].name === value.folder) { + addFolder = false; + folderIndex = i; + break; + } + } + } + + if (addFolder) { + const f = new FolderView(); + f.name = value.folder; + result.folders.push(f); + } + + if (hasFolder) { + result.folderRelationships.set(result.ciphers.length, folderIndex); } } const cipher = new CipherView(); + cipher.favorite = !organization && this.getValueOrDefault(value.favorite, '0') !== '0' ? true : false; cipher.type = CipherType.Login; - cipher.favorite = this.getValueOrDefault(value.favorite, '0') !== '0' ? true : false; cipher.notes = this.getValueOrDefault(value.notes); cipher.name = this.getValueOrDefault(value.name, '--'); @@ -93,15 +127,6 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer { } result.ciphers.push(cipher); - - if (addFolder) { - const f = new FolderView(); - f.name = value.folder; - result.folders.push(f); - } - if (hasFolder) { - result.folderRelationships.set(cipherIndex, folderIndex); - } }); result.success = true; diff --git a/src/importers/importer.ts b/src/importers/importer.ts index e6df305dde..78f838882f 100644 --- a/src/importers/importer.ts +++ b/src/importers/importer.ts @@ -1,5 +1,5 @@ import { ImportResult } from '../models/domain/importResult'; export interface Importer { - parse(data: string): ImportResult; + parse(data: string, organization?: boolean): ImportResult; } diff --git a/src/importers/keepassxCsvImporter.ts b/src/importers/keepassxCsvImporter.ts index c8ab05ec9f..610351fd09 100644 --- a/src/importers/keepassxCsvImporter.ts +++ b/src/importers/keepassxCsvImporter.ts @@ -19,12 +19,15 @@ export class KeePassXCsvImporter extends BaseImporter implements Importer { } results.forEach((value) => { + if (this.isNullOrWhitespace(value.Title)) { + return; + } + value.Group = !this.isNullOrWhitespace(value.Group) && value.Group.startsWith('Root/') ? value.Group.replace('Root/', '') : value.Group; const groupName = !this.isNullOrWhitespace(value.Group) ? value.Group.split('/').join(' > ') : null; let folderIndex = result.folders.length; - const cipherIndex = result.ciphers.length; const hasFolder = groupName != null; let addFolder = hasFolder; @@ -38,6 +41,15 @@ export class KeePassXCsvImporter extends BaseImporter implements Importer { } } + if (addFolder) { + const f = new FolderView(); + f.name = groupName; + result.folders.push(f); + } + if (hasFolder) { + result.folderRelationships.set(result.ciphers.length, folderIndex); + } + const cipher = new CipherView(); cipher.type = CipherType.Login; cipher.favorite = false; @@ -47,19 +59,7 @@ export class KeePassXCsvImporter extends BaseImporter implements Importer { cipher.login.username = this.getValueOrDefault(value.Username); cipher.login.password = this.getValueOrDefault(value.Password); cipher.login.uris = this.makeUriArray(value.URL); - - if (!this.isNullOrWhitespace(value.Title)) { - result.ciphers.push(cipher); - } - - if (addFolder) { - const f = new FolderView(); - f.name = groupName; - result.folders.push(f); - } - if (hasFolder) { - result.folderRelationships.set(cipherIndex, folderIndex); - } + result.ciphers.push(cipher); }); result.success = true; diff --git a/src/importers/lastpassCsvImporter.ts b/src/importers/lastpassCsvImporter.ts index 1177686e5f..0eba428ebc 100644 --- a/src/importers/lastpassCsvImporter.ts +++ b/src/importers/lastpassCsvImporter.ts @@ -14,7 +14,7 @@ import { CipherType } from '../enums/cipherType'; import { SecureNoteType } from '../enums/secureNoteType'; export class LastPassCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string, organization = false): ImportResult { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { @@ -23,8 +23,8 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { } results.forEach((value, index) => { - let folderIndex = result.folders.length; const cipherIndex = result.ciphers.length; + let folderIndex = result.folders.length; const hasFolder = this.getValueOrDefault(value.grouping, '(none)') !== '(none)'; let addFolder = hasFolder; @@ -38,7 +38,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { } } - const cipher = this.buildBaseCipher(value); + const cipher = this.buildBaseCipher(value, organization); if (cipher.name === '--' && results.length > 2 && index >= (results.length - 2)) { // LastPass file traditionally has two empty lines at the end return; @@ -60,7 +60,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { cipher.notes = this.getValueOrDefault(value.notes); if (!this.isNullOrWhitespace(value.ccnum)) { // there is a card on this identity too - const cardCipher = this.buildBaseCipher(value); + const cardCipher = this.buildBaseCipher(value, organization); cardCipher.identity = null; cardCipher.type = CipherType.Card; cardCipher.card = this.parseCard(value); @@ -80,11 +80,15 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { } }); + if (organization) { + this.moveFoldersToCollections(result); + } + result.success = true; return result; } - private buildBaseCipher(value: any) { + private buildBaseCipher(value: any, organization: boolean) { const cipher = new CipherView(); if (value.hasOwnProperty('profilename') && value.hasOwnProperty('profilelanguage')) { // form fill @@ -100,7 +104,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { } } else { // site or secure note - cipher.favorite = this.getValueOrDefault(value.fav, '0') === '1'; // TODO: if org, always false + cipher.favorite = !organization && this.getValueOrDefault(value.fav, '0') === '1'; cipher.name = this.getValueOrDefault(value.name, '--'); cipher.type = value.url === 'http://sn' ? CipherType.SecureNote : CipherType.Login; } From ed93fa9ea3c486d78ba2591b3057245ead147a15 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 5 Jul 2018 19:00:00 -0400 Subject: [PATCH 0356/1626] move to organization property instead of param --- src/importers/aviraCsvImporter.ts | 4 ++++ src/importers/baseImporter.ts | 2 ++ src/importers/bitwardenCsvImporter.ts | 10 +++++----- src/importers/blurCsvImporter.ts | 4 ++++ src/importers/importer.ts | 4 +++- src/importers/keepassxCsvImporter.ts | 4 ++++ src/importers/lastpassCsvImporter.ts | 12 ++++++------ 7 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/importers/aviraCsvImporter.ts b/src/importers/aviraCsvImporter.ts index bb10ad3927..15f9ddf491 100644 --- a/src/importers/aviraCsvImporter.ts +++ b/src/importers/aviraCsvImporter.ts @@ -10,6 +10,10 @@ import { CipherType } from '../enums/cipherType'; export class AviraCsvImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { + if (this.organization) { + throw new Error('Organization import not supported.'); + } + const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index d995d71221..009a7ccdc2 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -8,6 +8,8 @@ import { LoginUriView } from '../models/view/loginUriView'; import { Utils } from '../misc/utils'; export abstract class BaseImporter { + organization = false; + protected passwordFieldNames = [ 'password', 'pass word', 'passphrase', 'pass phrase', 'pass', 'code', 'code word', 'codeword', diff --git a/src/importers/bitwardenCsvImporter.ts b/src/importers/bitwardenCsvImporter.ts index acfdc3d1ed..29d83e70cb 100644 --- a/src/importers/bitwardenCsvImporter.ts +++ b/src/importers/bitwardenCsvImporter.ts @@ -15,7 +15,7 @@ import { FieldType } from '../enums/fieldType'; import { SecureNoteType } from '../enums/secureNoteType'; export class BitwardenCsvImporter extends BaseImporter implements Importer { - parse(data: string, organization = false): ImportResult { + parse(data: string): ImportResult { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { @@ -24,7 +24,7 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer { } results.forEach((value) => { - if (organization && !this.isNullOrWhitespace(value.collections)) { + if (this.organization && !this.isNullOrWhitespace(value.collections)) { const collections = (value.collections as string).split(','); collections.forEach((col) => { let addCollection = true; @@ -46,9 +46,9 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer { result.collectionRelationships.set(result.ciphers.length, collectionIndex); }); - } else if (!organization) { + } else if (!this.organization) { let folderIndex = result.folders.length; - const hasFolder = !organization && !this.isNullOrWhitespace(value.folder); + const hasFolder = !this.organization && !this.isNullOrWhitespace(value.folder); let addFolder = hasFolder; if (hasFolder) { @@ -73,7 +73,7 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer { } const cipher = new CipherView(); - cipher.favorite = !organization && this.getValueOrDefault(value.favorite, '0') !== '0' ? true : false; + cipher.favorite = !this.organization && this.getValueOrDefault(value.favorite, '0') !== '0' ? true : false; cipher.type = CipherType.Login; cipher.notes = this.getValueOrDefault(value.notes); cipher.name = this.getValueOrDefault(value.name, '--'); diff --git a/src/importers/blurCsvImporter.ts b/src/importers/blurCsvImporter.ts index 33620181b5..efe91de808 100644 --- a/src/importers/blurCsvImporter.ts +++ b/src/importers/blurCsvImporter.ts @@ -10,6 +10,10 @@ import { CipherType } from '../enums/cipherType'; export class BlurCsvImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { + if (this.organization) { + throw new Error('Organization import not supported.'); + } + const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { diff --git a/src/importers/importer.ts b/src/importers/importer.ts index 78f838882f..fdd5c0c45d 100644 --- a/src/importers/importer.ts +++ b/src/importers/importer.ts @@ -1,5 +1,7 @@ import { ImportResult } from '../models/domain/importResult'; export interface Importer { - parse(data: string, organization?: boolean): ImportResult; + organization: boolean; + + parse(data: string): ImportResult; } diff --git a/src/importers/keepassxCsvImporter.ts b/src/importers/keepassxCsvImporter.ts index 610351fd09..59325efe9d 100644 --- a/src/importers/keepassxCsvImporter.ts +++ b/src/importers/keepassxCsvImporter.ts @@ -11,6 +11,10 @@ import { CipherType } from '../enums/cipherType'; export class KeePassXCsvImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { + if (this.organization) { + throw new Error('Organization import not supported.'); + } + const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { diff --git a/src/importers/lastpassCsvImporter.ts b/src/importers/lastpassCsvImporter.ts index 0eba428ebc..fd9d9f1b94 100644 --- a/src/importers/lastpassCsvImporter.ts +++ b/src/importers/lastpassCsvImporter.ts @@ -14,7 +14,7 @@ import { CipherType } from '../enums/cipherType'; import { SecureNoteType } from '../enums/secureNoteType'; export class LastPassCsvImporter extends BaseImporter implements Importer { - parse(data: string, organization = false): ImportResult { + parse(data: string): ImportResult { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { @@ -38,7 +38,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { } } - const cipher = this.buildBaseCipher(value, organization); + const cipher = this.buildBaseCipher(value); if (cipher.name === '--' && results.length > 2 && index >= (results.length - 2)) { // LastPass file traditionally has two empty lines at the end return; @@ -60,7 +60,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { cipher.notes = this.getValueOrDefault(value.notes); if (!this.isNullOrWhitespace(value.ccnum)) { // there is a card on this identity too - const cardCipher = this.buildBaseCipher(value, organization); + const cardCipher = this.buildBaseCipher(value); cardCipher.identity = null; cardCipher.type = CipherType.Card; cardCipher.card = this.parseCard(value); @@ -80,7 +80,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { } }); - if (organization) { + if (this.organization) { this.moveFoldersToCollections(result); } @@ -88,7 +88,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { return result; } - private buildBaseCipher(value: any, organization: boolean) { + private buildBaseCipher(value: any) { const cipher = new CipherView(); if (value.hasOwnProperty('profilename') && value.hasOwnProperty('profilelanguage')) { // form fill @@ -104,7 +104,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { } } else { // site or secure note - cipher.favorite = !organization && this.getValueOrDefault(value.fav, '0') === '1'; + cipher.favorite = !this.organization && this.getValueOrDefault(value.fav, '0') === '1'; cipher.name = this.getValueOrDefault(value.name, '--'); cipher.type = value.url === 'http://sn' ? CipherType.SecureNote : CipherType.Login; } From a600c4a5398f8893d4cfea298fcfb66f90c3d975 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 5 Jul 2018 23:38:27 -0400 Subject: [PATCH 0357/1626] org import and collection encrypt function --- src/abstractions/api.service.ts | 2 +- src/abstractions/collection.service.ts | 1 + .../request/importOrganizationCiphersRequest.ts | 6 +++--- src/services/api.service.ts | 4 ++-- src/services/collection.service.ts | 16 ++++++++++++++++ 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index b466f04f45..b9884bb7dd 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -97,7 +97,7 @@ export abstract class ApiService { putCipherCollectionsAdmin: (id: string, request: CipherCollectionsRequest) => Promise; postPurgeCiphers: (request: PasswordVerificationRequest) => Promise; postImportCiphers: (request: ImportCiphersRequest) => Promise; - postImportOrganizationCiphers: (request: ImportOrganizationCiphersRequest) => Promise; + postImportOrganizationCiphers: (organizationId: string, request: ImportOrganizationCiphersRequest) => Promise; postCipherAttachment: (id: string, data: FormData) => Promise; postCipherAttachmentAdmin: (id: string, data: FormData) => Promise; deleteCipherAttachment: (id: string, attachmentId: string) => Promise; diff --git a/src/abstractions/collection.service.ts b/src/abstractions/collection.service.ts index 7960dfd7bd..041db130bc 100644 --- a/src/abstractions/collection.service.ts +++ b/src/abstractions/collection.service.ts @@ -8,6 +8,7 @@ export abstract class CollectionService { decryptedCollectionCache: CollectionView[]; clearCache: () => void; + encrypt: (model: CollectionView) => Promise; get: (id: string) => Promise; getAll: () => Promise; getAllDecrypted: () => Promise; diff --git a/src/models/request/importOrganizationCiphersRequest.ts b/src/models/request/importOrganizationCiphersRequest.ts index 3a63693235..c39e66e797 100644 --- a/src/models/request/importOrganizationCiphersRequest.ts +++ b/src/models/request/importOrganizationCiphersRequest.ts @@ -3,7 +3,7 @@ import { CollectionRequest } from './collectionRequest'; import { KvpRequest } from './kvpRequest'; export class ImportOrganizationCiphersRequest { - ciphers: CipherRequest[]; - collections: CollectionRequest[]; - collectionRelationships: Array>; + ciphers: CipherRequest[] = []; + collections: CollectionRequest[] = []; + collectionRelationships: Array> = []; } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index a8aee56913..7eb3331f45 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -318,8 +318,8 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/ciphers/import', request, true, false); } - postImportOrganizationCiphers(request: ImportOrganizationCiphersRequest): Promise { - return this.send('POST', '/ciphers/import-organization', request, true, false); + postImportOrganizationCiphers(organizationId: string, request: ImportOrganizationCiphersRequest): Promise { + return this.send('POST', '/ciphers/import-organization?organizationId=' + organizationId, request, true, false); } // Attachments APIs diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index c76fbb6299..8cf1b8ac17 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -25,6 +25,22 @@ export class CollectionService implements CollectionServiceAbstraction { this.decryptedCollectionCache = null; } + async encrypt(model: CollectionView): Promise { + if (model.organizationId == null) { + throw new Error('Collection has no organization id.'); + } + const key = await this.cryptoService.getOrgKey(model.organizationId); + if (key == null) { + throw new Error('No key for this collection\'s organization.'); + } + const collection = new Collection(); + collection.id = model.id; + collection.organizationId = model.organizationId; + collection.readOnly = model.readOnly; + collection.name = await this.cryptoService.encrypt(model.name, key); + return collection; + } + async get(id: string): Promise { const userId = await this.userService.getUserId(); const collections = await this.storageService.get<{ [id: string]: CollectionData; }>( From 4aebc4ab3d080e24db6e63ddc194ca75b50719f1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 6 Jul 2018 12:40:08 -0400 Subject: [PATCH 0358/1626] search pipe --- src/angular/pipes/search.pipe.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/angular/pipes/search.pipe.ts diff --git a/src/angular/pipes/search.pipe.ts b/src/angular/pipes/search.pipe.ts new file mode 100644 index 0000000000..c71ab3a8b1 --- /dev/null +++ b/src/angular/pipes/search.pipe.ts @@ -0,0 +1,30 @@ +import { + Pipe, + PipeTransform, +} from '@angular/core'; + +@Pipe({ + name: 'search', +}) +export class SearchPipe implements PipeTransform { + transform(items: any[], searchText: string, prop1?: string, prop2?: string): any[] { + if (items == null || items.length === 0) { + return []; + } + + if (searchText == null || searchText.length < 2) { + return items; + } + + searchText = searchText.toLowerCase(); + return items.filter((i) => { + if (prop1 != null && i[prop1] != null && i[prop1].toString().toLowerCase().indexOf(searchText) > -1) { + return true; + } + if (prop2 != null && i[prop2] != null && i[prop2].toString().toLowerCase().indexOf(searchText) > -1) { + return true; + } + return false; + }); + } +} From 4ae4b667ffa317537fdf1be5d4b594781778186a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 6 Jul 2018 12:40:36 -0400 Subject: [PATCH 0359/1626] decrypt many collections service function --- src/abstractions/collection.service.ts | 1 + src/services/collection.service.ts | 23 ++++++++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/abstractions/collection.service.ts b/src/abstractions/collection.service.ts index 041db130bc..64bf22def6 100644 --- a/src/abstractions/collection.service.ts +++ b/src/abstractions/collection.service.ts @@ -9,6 +9,7 @@ export abstract class CollectionService { clearCache: () => void; encrypt: (model: CollectionView) => Promise; + decryptMany: (collections: Collection[]) => Promise; get: (id: string) => Promise; getAll: () => Promise; getAllDecrypted: () => Promise; diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index 8cf1b8ac17..ce26f72dcc 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -41,6 +41,19 @@ export class CollectionService implements CollectionServiceAbstraction { return collection; } + async decryptMany(collections: Collection[]): Promise { + if (collections == null) { + return []; + } + const decCollections: CollectionView[] = []; + const promises: Array> = []; + collections.forEach((collection) => { + promises.push(collection.decrypt().then((c) => decCollections.push(c))); + }); + await Promise.all(promises); + return decCollections.sort(this.getLocaleSortingFunction()); + } + async get(id: string): Promise { const userId = await this.userService.getUserId(); const collections = await this.storageService.get<{ [id: string]: CollectionData; }>( @@ -75,16 +88,8 @@ export class CollectionService implements CollectionServiceAbstraction { throw new Error('No key.'); } - const decCollections: CollectionView[] = []; - const promises: Array> = []; const collections = await this.getAll(); - collections.forEach((collection) => { - promises.push(collection.decrypt().then((c) => decCollections.push(c))); - }); - - await Promise.all(promises); - decCollections.sort(this.getLocaleSortingFunction()); - this.decryptedCollectionCache = decCollections; + this.decryptedCollectionCache = await this.decryptMany(collections); return this.decryptedCollectionCache; } From 2a526940fd4eefd2758ea654b0c7994e0ee12c4e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 6 Jul 2018 12:40:43 -0400 Subject: [PATCH 0360/1626] apis for groups and collections --- src/models/data/collectionData.ts | 4 +- src/models/request/collectionRequest.ts | 3 + src/models/request/groupRequest.ts | 8 +++ .../request/selectionReadOnlyRequest.ts | 9 +++ src/models/response/collectionResponse.ts | 22 +++++- src/models/response/collectionUserResponse.ts | 22 ++++++ src/models/response/groupResponse.ts | 28 ++++++++ src/models/response/groupUserResponse.ts | 20 ++++++ .../response/selectionReadOnlyResponse.ts | 9 +++ src/models/response/syncResponse.ts | 6 +- src/services/api.service.ts | 72 ++++++++++++++++++- src/services/sync.service.ts | 4 +- 12 files changed, 198 insertions(+), 9 deletions(-) create mode 100644 src/models/request/groupRequest.ts create mode 100644 src/models/request/selectionReadOnlyRequest.ts create mode 100644 src/models/response/collectionUserResponse.ts create mode 100644 src/models/response/groupResponse.ts create mode 100644 src/models/response/groupUserResponse.ts create mode 100644 src/models/response/selectionReadOnlyResponse.ts diff --git a/src/models/data/collectionData.ts b/src/models/data/collectionData.ts index ff226d315e..5d49ae2389 100644 --- a/src/models/data/collectionData.ts +++ b/src/models/data/collectionData.ts @@ -1,4 +1,4 @@ -import { CollectionResponse } from '../response/collectionResponse'; +import { CollectionDetailsResponse } from '../response/collectionResponse'; export class CollectionData { id: string; @@ -6,7 +6,7 @@ export class CollectionData { name: string; readOnly: boolean; - constructor(response: CollectionResponse) { + constructor(response: CollectionDetailsResponse) { this.id = response.id; this.organizationId = response.organizationId; this.name = response.name; diff --git a/src/models/request/collectionRequest.ts b/src/models/request/collectionRequest.ts index 38e62a449f..e5f427624b 100644 --- a/src/models/request/collectionRequest.ts +++ b/src/models/request/collectionRequest.ts @@ -1,7 +1,10 @@ import { Collection } from '../domain/collection'; +import { SelectionReadOnlyRequest } from './selectionReadOnlyRequest'; + export class CollectionRequest { name: string; + groups: SelectionReadOnlyRequest[] = []; constructor(collection: Collection) { this.name = collection.name ? collection.name.encryptedString : null; diff --git a/src/models/request/groupRequest.ts b/src/models/request/groupRequest.ts new file mode 100644 index 0000000000..d3e18cf1d7 --- /dev/null +++ b/src/models/request/groupRequest.ts @@ -0,0 +1,8 @@ +import { SelectionReadOnlyRequest } from './selectionReadOnlyRequest'; + +export class GroupRequest { + name: string; + accessAll: boolean; + externalId: string; + collections: SelectionReadOnlyRequest[] = []; +} diff --git a/src/models/request/selectionReadOnlyRequest.ts b/src/models/request/selectionReadOnlyRequest.ts new file mode 100644 index 0000000000..e947b0a364 --- /dev/null +++ b/src/models/request/selectionReadOnlyRequest.ts @@ -0,0 +1,9 @@ +export class SelectionReadOnlyRequest { + id: string; + readOnly: boolean; + + constructor(id: string, readOnly: boolean) { + this.id = id; + this.readOnly = readOnly; + } +} diff --git a/src/models/response/collectionResponse.ts b/src/models/response/collectionResponse.ts index 525bf60e34..d04304b785 100644 --- a/src/models/response/collectionResponse.ts +++ b/src/models/response/collectionResponse.ts @@ -1,13 +1,33 @@ +import { SelectionReadOnlyResponse } from './selectionReadOnlyResponse'; + export class CollectionResponse { id: string; organizationId: string; name: string; - readOnly: boolean; constructor(response: any) { this.id = response.Id; this.organizationId = response.OrganizationId; this.name = response.Name; + } +} + +export class CollectionDetailsResponse extends CollectionResponse { + readOnly: boolean; + + constructor(response: any) { + super(response); this.readOnly = response.ReadOnly || false; } } + +export class CollectionGroupDetailsResponse extends CollectionResponse { + groups: SelectionReadOnlyResponse[] = []; + + constructor(response: any) { + super(response); + if (response.Groups != null) { + this.groups = response.Collections.map((g: any) => new SelectionReadOnlyResponse(g)); + } + } +} diff --git a/src/models/response/collectionUserResponse.ts b/src/models/response/collectionUserResponse.ts new file mode 100644 index 0000000000..ea36e473e6 --- /dev/null +++ b/src/models/response/collectionUserResponse.ts @@ -0,0 +1,22 @@ +import { OrganizationUserStatusType } from '../../enums/organizationUserStatusType'; +import { OrganizationUserType } from '../../enums/organizationUserType'; + +export class CollectionUserResponse { + organizationUserId: string; + accessAll: boolean; + name: string; + email: string; + type: OrganizationUserType; + status: OrganizationUserStatusType; + readOnly: boolean; + + constructor(response: any) { + this.organizationUserId = response.OrganizationUserId; + this.accessAll = response.AccessAll; + this.name = response.Name; + this.email = response.Email; + this.type = response.Type; + this.status = response.Status; + this.readOnly = response.ReadOnly; + } +} diff --git a/src/models/response/groupResponse.ts b/src/models/response/groupResponse.ts new file mode 100644 index 0000000000..960e077a2a --- /dev/null +++ b/src/models/response/groupResponse.ts @@ -0,0 +1,28 @@ +import { SelectionReadOnlyResponse } from './selectionReadOnlyResponse'; + +export class GroupResponse { + id: string; + organizationId: string; + name: string; + accessAll: boolean; + externalId: string; + + constructor(response: any) { + this.id = response.Id; + this.organizationId = response.OrganizationId; + this.name = response.Name; + this.accessAll = response.AccessAll; + this.externalId = response.ExternalId; + } +} + +export class GroupDetailsResponse extends GroupResponse { + collections: SelectionReadOnlyResponse[] = []; + + constructor(response: any) { + super(response); + if (response.Collections != null) { + this.collections = response.Collections.map((c: any) => new SelectionReadOnlyResponse(c)); + } + } +} diff --git a/src/models/response/groupUserResponse.ts b/src/models/response/groupUserResponse.ts new file mode 100644 index 0000000000..0d8d25c30d --- /dev/null +++ b/src/models/response/groupUserResponse.ts @@ -0,0 +1,20 @@ +import { OrganizationUserStatusType } from '../../enums/organizationUserStatusType'; +import { OrganizationUserType } from '../../enums/organizationUserType'; + +export class GroupUserResponse { + organizationUserId: string; + accessAll: boolean; + name: string; + email: string; + type: OrganizationUserType; + status: OrganizationUserStatusType; + + constructor(response: any) { + this.organizationUserId = response.OrganizationUserId; + this.accessAll = response.AccessAll; + this.name = response.Name; + this.email = response.Email; + this.type = response.Type; + this.status = response.Status; + } +} diff --git a/src/models/response/selectionReadOnlyResponse.ts b/src/models/response/selectionReadOnlyResponse.ts new file mode 100644 index 0000000000..c5f2c2dffd --- /dev/null +++ b/src/models/response/selectionReadOnlyResponse.ts @@ -0,0 +1,9 @@ +export class SelectionReadOnlyResponse { + id: string; + readOnly: boolean; + + constructor(response: any) { + this.id = response.Id; + this.readOnly = response.ReadOnly; + } +} diff --git a/src/models/response/syncResponse.ts b/src/models/response/syncResponse.ts index 58c0280739..3d0b741099 100644 --- a/src/models/response/syncResponse.ts +++ b/src/models/response/syncResponse.ts @@ -1,5 +1,5 @@ import { CipherResponse } from './cipherResponse'; -import { CollectionResponse } from './collectionResponse'; +import { CollectionDetailsResponse } from './collectionResponse'; import { DomainsResponse } from './domainsResponse'; import { FolderResponse } from './folderResponse'; import { ProfileResponse } from './profileResponse'; @@ -7,7 +7,7 @@ import { ProfileResponse } from './profileResponse'; export class SyncResponse { profile?: ProfileResponse; folders: FolderResponse[] = []; - collections: CollectionResponse[] = []; + collections: CollectionDetailsResponse[] = []; ciphers: CipherResponse[] = []; domains?: DomainsResponse; @@ -24,7 +24,7 @@ export class SyncResponse { if (response.Collections) { response.Collections.forEach((collection: any) => { - this.collections.push(new CollectionResponse(collection)); + this.collections.push(new CollectionDetailsResponse(collection)); }); } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 7eb3331f45..e8a1cdedbd 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -12,9 +12,11 @@ import { CipherBulkShareRequest } from '../models/request/cipherBulkShareRequest import { CipherCollectionsRequest } from '../models/request/cipherCollectionsRequest'; import { CipherRequest } from '../models/request/cipherRequest'; import { CipherShareRequest } from '../models/request/cipherShareRequest'; +import { CollectionRequest } from '../models/request/collectionRequest'; import { EmailRequest } from '../models/request/emailRequest'; import { EmailTokenRequest } from '../models/request/emailTokenRequest'; import { FolderRequest } from '../models/request/folderRequest'; +import { GroupRequest } from '../models/request/groupRequest'; import { ImportCiphersRequest } from '../models/request/importCiphersRequest'; import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest'; @@ -40,10 +42,19 @@ import { UpdateTwoFactorYubioOtpRequest } from '../models/request/updateTwoFacto import { BillingResponse } from '../models/response/billingResponse'; import { CipherResponse } from '../models/response/cipherResponse'; -import { CollectionResponse } from '../models/response/collectionResponse'; +import { + CollectionGroupDetailsResponse, + CollectionResponse, +} from '../models/response/collectionResponse'; +import { CollectionUserResponse } from '../models/response/collectionUserResponse'; import { DomainsResponse } from '../models/response/domainsResponse'; import { ErrorResponse } from '../models/response/errorResponse'; import { FolderResponse } from '../models/response/folderResponse'; +import { + GroupDetailsResponse, + GroupResponse, +} from '../models/response/groupResponse'; +import { GroupUserResponse } from '../models/response/groupUserResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; import { ListResponse } from '../models/response/listResponse'; @@ -350,11 +361,70 @@ export class ApiService implements ApiServiceAbstraction { // Collections APIs + async getCollectionDetails(organizationId: string, id: string): Promise { + const r = await this.send('GET', '/organizations/' + organizationId + '/collections/' + id + '/details', + null, true, true); + return new CollectionGroupDetailsResponse(r); + } + async getCollections(organizationId: string): Promise> { const r = await this.send('GET', '/organizations/' + organizationId + '/collections', null, true, true); return new ListResponse(r, CollectionResponse); } + async getCollectionUsers(organizationId: string, id: string): Promise> { + const r = await this.send('GET', '/organizations/' + organizationId + '/collections/' + id + '/users', + null, true, true); + return new ListResponse(r, CollectionUserResponse); + } + + async postCollection(request: CollectionRequest): Promise { + const r = await this.send('POST', '/collections', request, true, true); + return new CollectionResponse(r); + } + + async putCollection(id: string, request: CollectionRequest): Promise { + const r = await this.send('PUT', '/collections/' + id, request, true, true); + return new CollectionResponse(r); + } + + deleteCollection(id: string): Promise { + return this.send('DELETE', '/collections/' + id, null, true, false); + } + + // Groups APIs + + async getGroupDetails(organizationId: string, id: string): Promise { + const r = await this.send('GET', '/organizations/' + organizationId + '/groups/' + id + '/details', + null, true, true); + return new GroupDetailsResponse(r); + } + + async getGroups(organizationId: string): Promise> { + const r = await this.send('GET', '/organizations/' + organizationId + '/groups', null, true, true); + return new ListResponse(r, GroupResponse); + } + + async getGroupUsers(organizationId: string, id: string): Promise> { + const r = await this.send('GET', '/organizations/' + organizationId + '/groups/' + id + '/users', + null, true, true); + return new ListResponse(r, GroupUserResponse); + } + + async postGroup(request: GroupRequest): Promise { + const r = await this.send('POST', '/groups', request, true, true); + return new GroupResponse(r); + } + + async putGroup(id: string, request: GroupRequest): Promise { + const r = await this.send('PUT', '/groups/' + id, request, true, true); + return new GroupResponse(r); + } + + deleteGroup(id: string): Promise { + return this.send('DELETE', '/groups/' + id, null, true, false); + } + // Sync APIs async getSync(): Promise { diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index 2e025772c5..07dd66e003 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -15,7 +15,7 @@ import { FolderData } from '../models/data/folderData'; import { OrganizationData } from '../models/data/organizationData'; import { CipherResponse } from '../models/response/cipherResponse'; -import { CollectionResponse } from '../models/response/collectionResponse'; +import { CollectionDetailsResponse } from '../models/response/collectionResponse'; import { DomainsResponse } from '../models/response/domainsResponse'; import { FolderResponse } from '../models/response/folderResponse'; import { ProfileResponse } from '../models/response/profileResponse'; @@ -162,7 +162,7 @@ export class SyncService implements SyncServiceAbstraction { return await this.folderService.replace(folders); } - private async syncCollections(response: CollectionResponse[]) { + private async syncCollections(response: CollectionDetailsResponse[]) { const collections: { [id: string]: CollectionData; } = {}; response.forEach((c) => { collections[c.id] = new CollectionData(c); From 7f8ed5a9289f999b359c79680b4fe79a92db3919 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 6 Jul 2018 12:49:22 -0400 Subject: [PATCH 0361/1626] api interface updates --- src/abstractions/api.service.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index b9884bb7dd..d5c490be33 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -6,9 +6,11 @@ import { CipherBulkShareRequest } from '../models/request/cipherBulkShareRequest import { CipherCollectionsRequest } from '../models/request/cipherCollectionsRequest'; import { CipherRequest } from '../models/request/cipherRequest'; import { CipherShareRequest } from '../models/request/cipherShareRequest'; +import { CollectionRequest } from '../models/request/collectionRequest'; import { EmailRequest } from '../models/request/emailRequest'; import { EmailTokenRequest } from '../models/request/emailTokenRequest'; import { FolderRequest } from '../models/request/folderRequest'; +import { GroupRequest } from '../models/request/groupRequest'; import { ImportCiphersRequest } from '../models/request/importCiphersRequest'; import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest'; @@ -34,9 +36,18 @@ import { UpdateTwoFactorYubioOtpRequest } from '../models/request/updateTwoFacto import { BillingResponse } from '../models/response/billingResponse'; import { CipherResponse } from '../models/response/cipherResponse'; -import { CollectionResponse } from '../models/response/collectionResponse'; +import { + CollectionGroupDetailsResponse, + CollectionResponse, +} from '../models/response/collectionResponse'; +import { CollectionUserResponse } from '../models/response/collectionUserResponse'; import { DomainsResponse } from '../models/response/domainsResponse'; import { FolderResponse } from '../models/response/folderResponse'; +import { + GroupDetailsResponse, + GroupResponse, +} from '../models/response/groupResponse'; +import { GroupUserResponse } from '../models/response/groupUserResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; import { ListResponse } from '../models/response/listResponse'; @@ -104,7 +115,18 @@ export abstract class ApiService { deleteCipherAttachmentAdmin: (id: string, attachmentId: string) => Promise; postShareCipherAttachment: (id: string, attachmentId: string, data: FormData, organizationId: string) => Promise; + getCollectionDetails: (organizationId: string, id: string) => Promise; getCollections: (organizationId: string) => Promise>; + getCollectionUsers: (organizationId: string, id: string) => Promise>; + postCollection: (request: CollectionRequest) => Promise; + putCollection: (id: string, request: CollectionRequest) => Promise; + deleteCollection: (id: string) => Promise; + getGroupDetails: (organizationId: string, id: string) => Promise; + getGroups: (organizationId: string) => Promise>; + getGroupUsers: (organizationId: string, id: string) => Promise>; + postGroup: (request: GroupRequest) => Promise; + putGroup: (id: string, request: GroupRequest) => Promise; + deleteGroup: (id: string) => Promise; getSync: () => Promise; postImportDirectory: (organizationId: string, request: ImportDirectoryRequest) => Promise; getSettingsDomains: () => Promise; From b9a15d7ced6f5d44fe81ed2e441b5506159dcf85 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 6 Jul 2018 12:55:48 -0400 Subject: [PATCH 0362/1626] cast fix --- src/services/export.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/export.service.ts b/src/services/export.service.ts index 6a49d729f7..68f5a7e1d8 100644 --- a/src/services/export.service.ts +++ b/src/services/export.service.ts @@ -16,6 +16,7 @@ import { Collection } from '../models/domain/collection'; import { CipherData } from '../models/data/cipherData'; import { CollectionData } from '../models/data/collectionData'; +import { CollectionDetailsResponse } from '../models/response/collectionResponse'; export class ExportService implements ExportServiceAbstraction { constructor(private folderService: FolderService, private cipherService: CipherService, @@ -75,7 +76,7 @@ export class ExportService implements ExportServiceAbstraction { const collectionPromises: any = []; if (collections != null && collections.data != null && collections.data.length > 0) { collections.data.forEach((c) => { - const collection = new Collection(new CollectionData(c)); + const collection = new Collection(new CollectionData(c as CollectionDetailsResponse)); collectionPromises.push(collection.decrypt().then((decCol) => { decCollections.push(decCol); })); From e25ad93082c7a9f4db46b84e67d1403996337bcc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 6 Jul 2018 14:11:47 -0400 Subject: [PATCH 0363/1626] trim search term --- src/angular/pipes/search-ciphers.pipe.ts | 2 +- src/angular/pipes/search.pipe.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/angular/pipes/search-ciphers.pipe.ts b/src/angular/pipes/search-ciphers.pipe.ts index 0ffb12b52a..848f8519e7 100644 --- a/src/angular/pipes/search-ciphers.pipe.ts +++ b/src/angular/pipes/search-ciphers.pipe.ts @@ -26,7 +26,7 @@ export class SearchCiphersPipe implements PipeTransform { return ciphers; } - searchText = searchText.toLowerCase(); + searchText = searchText.trim().toLowerCase(); return ciphers.filter((c) => { if (c.name != null && c.name.toLowerCase().indexOf(searchText) > -1) { return true; diff --git a/src/angular/pipes/search.pipe.ts b/src/angular/pipes/search.pipe.ts index c71ab3a8b1..5de3d5c50b 100644 --- a/src/angular/pipes/search.pipe.ts +++ b/src/angular/pipes/search.pipe.ts @@ -16,7 +16,7 @@ export class SearchPipe implements PipeTransform { return items; } - searchText = searchText.toLowerCase(); + searchText = searchText.trim().toLowerCase(); return items.filter((i) => { if (prop1 != null && i[prop1] != null && i[prop1].toString().toLowerCase().indexOf(searchText) > -1) { return true; From 7b23b90054f18c2093386d62cc39abb9be67b075 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 6 Jul 2018 15:00:55 -0400 Subject: [PATCH 0364/1626] org user apis, sort function utils --- src/abstractions/api.service.ts | 13 ++++++ src/abstractions/collection.service.ts | 1 - src/misc/utils.ts | 19 +++++++++ .../response/organizationUserResponse.ts | 41 +++++++++++++++++++ src/services/api.service.ts | 8 ++++ src/services/collection.service.ts | 21 ++-------- src/services/folder.service.ts | 21 ++-------- 7 files changed, 87 insertions(+), 37 deletions(-) create mode 100644 src/models/response/organizationUserResponse.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index d5c490be33..54891450a1 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -52,6 +52,7 @@ import { IdentityTokenResponse } from '../models/response/identityTokenResponse' import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; import { ListResponse } from '../models/response/listResponse'; import { OrganizationResponse } from '../models/response/organizationResponse'; +import { OrganizationUserUserDetailsResponse } from '../models/response/organizationUserResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SyncResponse } from '../models/response/syncResponse'; import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; @@ -70,6 +71,7 @@ export abstract class ApiService { setUrls: (urls: EnvironmentUrls) => void; postIdentityToken: (request: TokenRequest) => Promise; refreshIdentityToken: () => Promise; + getProfile: () => Promise; getUserBilling: () => Promise; putProfile: (request: UpdateProfileRequest) => Promise; @@ -88,9 +90,11 @@ export abstract class ApiService { postAccountPayment: (request: PaymentRequest) => Promise; postAccountLicense: (data: FormData) => Promise; postAccountKeys: (request: KeysRequest) => Promise; + postFolder: (request: FolderRequest) => Promise; putFolder: (id: string, request: FolderRequest) => Promise; deleteFolder: (id: string) => Promise; + getCipher: (id: string) => Promise; getCipherAdmin: (id: string) => Promise; getCiphersOrganization: (organizationId: string) => Promise>; @@ -109,28 +113,36 @@ export abstract class ApiService { postPurgeCiphers: (request: PasswordVerificationRequest) => Promise; postImportCiphers: (request: ImportCiphersRequest) => Promise; postImportOrganizationCiphers: (organizationId: string, request: ImportOrganizationCiphersRequest) => Promise; + postCipherAttachment: (id: string, data: FormData) => Promise; postCipherAttachmentAdmin: (id: string, data: FormData) => Promise; deleteCipherAttachment: (id: string, attachmentId: string) => Promise; deleteCipherAttachmentAdmin: (id: string, attachmentId: string) => Promise; postShareCipherAttachment: (id: string, attachmentId: string, data: FormData, organizationId: string) => Promise; + getCollectionDetails: (organizationId: string, id: string) => Promise; getCollections: (organizationId: string) => Promise>; getCollectionUsers: (organizationId: string, id: string) => Promise>; postCollection: (request: CollectionRequest) => Promise; putCollection: (id: string, request: CollectionRequest) => Promise; deleteCollection: (id: string) => Promise; + getGroupDetails: (organizationId: string, id: string) => Promise; getGroups: (organizationId: string) => Promise>; getGroupUsers: (organizationId: string, id: string) => Promise>; postGroup: (request: GroupRequest) => Promise; putGroup: (id: string, request: GroupRequest) => Promise; deleteGroup: (id: string) => Promise; + + getOrganizationUsers: (organizationId: string) => Promise>; + getSync: () => Promise; postImportDirectory: (organizationId: string, request: ImportDirectoryRequest) => Promise; + getSettingsDomains: () => Promise; putSettingsDomains: (request: UpdateDomainsRequest) => Promise; + getTwoFactorProviders: () => Promise>; getTwoFactorAuthenticator: (request: PasswordVerificationRequest) => Promise; getTwoFactorEmail: (request: PasswordVerificationRequest) => Promise; @@ -148,6 +160,7 @@ export abstract class ApiService { postTwoFactorRecover: (request: TwoFactorRecoveryRequest) => Promise; postTwoFactorEmailSetup: (request: TwoFactorEmailRequest) => Promise; postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise; + postOrganization: (request: OrganizationCreateRequest) => Promise; postLeaveOrganization: (id: string) => Promise; postOrganizationLicense: (data: FormData) => Promise; diff --git a/src/abstractions/collection.service.ts b/src/abstractions/collection.service.ts index 64bf22def6..a6decc2c09 100644 --- a/src/abstractions/collection.service.ts +++ b/src/abstractions/collection.service.ts @@ -17,5 +17,4 @@ export abstract class CollectionService { replace: (collections: { [id: string]: CollectionData; }) => Promise; clear: (userId: string) => Promise; delete: (id: string | string[]) => Promise; - getLocaleSortingFunction: () => (a: CollectionView, b: CollectionView) => number; } diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 637330ea34..b507f2aba1 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -1,3 +1,5 @@ +import { I18nService } from '../abstractions/i18n.service'; + // tslint:disable-next-line const nodeURL = typeof window === 'undefined' ? require('url').URL : null; @@ -149,6 +151,23 @@ export class Utils { return url != null ? url.host : null; } + static getSortFunction(i18nService: I18nService, prop: string) { + return (a: any, b: any) => { + if (a[prop] == null && b[prop] != null) { + return -1; + } + if (a[prop] != null && b[prop] == null) { + return 1; + } + if (a[prop] == null && b[prop] == null) { + return 0; + } + + return i18nService.collator ? i18nService.collator.compare(a[prop], b[prop]) : + a[prop].localeCompare(b[prop]); + }; + } + private static getUrl(uriString: string): URL { if (uriString == null) { return null; diff --git a/src/models/response/organizationUserResponse.ts b/src/models/response/organizationUserResponse.ts new file mode 100644 index 0000000000..12006b665d --- /dev/null +++ b/src/models/response/organizationUserResponse.ts @@ -0,0 +1,41 @@ +import { OrganizationUserStatusType } from '../../enums/organizationUserStatusType'; +import { OrganizationUserType } from '../../enums/organizationUserType'; +import { SelectionReadOnlyResponse } from './selectionReadOnlyResponse'; + +export class OrganizationUserResponse { + id: string; + userId: string; + type: OrganizationUserType; + status: OrganizationUserStatusType; + accessAll: boolean; + + constructor(response: any) { + this.id = response.Id; + this.userId = response.UserId; + this.type = response.Type; + this.status = response.Status; + this.accessAll = response.AccessAll; + } +} + +export class OrganizationUserUserDetailsResponse extends OrganizationUserResponse { + name: string; + email: string; + + constructor(response: any) { + super(response); + this.name = response.Name; + this.email = response.Email; + } +} + +export class OrganizationUserDetailsResponse extends OrganizationUserResponse { + collections: SelectionReadOnlyResponse; + + constructor(response: any) { + super(response); + if (response.Collections != null) { + this.collections = response.Collections.map((c: any) => new SelectionReadOnlyResponse(c)); + } + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index e8a1cdedbd..f738e2942e 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -59,6 +59,7 @@ import { IdentityTokenResponse } from '../models/response/identityTokenResponse' import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; import { ListResponse } from '../models/response/listResponse'; import { OrganizationResponse } from '../models/response/organizationResponse'; +import { OrganizationUserUserDetailsResponse } from '../models/response/organizationUserResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SyncResponse } from '../models/response/syncResponse'; import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; @@ -425,6 +426,13 @@ export class ApiService implements ApiServiceAbstraction { return this.send('DELETE', '/groups/' + id, null, true, false); } + // Organization User APIs + + async getOrganizationUsers(organizationId: string): Promise> { + const r = await this.send('GET', '/organizations/' + organizationId + '/users', null, true, true); + return new ListResponse(r, OrganizationUserUserDetailsResponse); + } + // Sync APIs async getSync(): Promise { diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index ce26f72dcc..152d91713b 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -10,6 +10,8 @@ import { I18nService } from '../abstractions/i18n.service'; import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; +import { Utils } from '../misc/utils'; + const Keys = { collectionsPrefix: 'collections_', }; @@ -51,7 +53,7 @@ export class CollectionService implements CollectionServiceAbstraction { promises.push(collection.decrypt().then((c) => decCollections.push(c))); }); await Promise.all(promises); - return decCollections.sort(this.getLocaleSortingFunction()); + return decCollections.sort(Utils.getSortFunction(this.i18nService, 'name')); } async get(id: string): Promise { @@ -145,21 +147,4 @@ export class CollectionService implements CollectionServiceAbstraction { await this.storageService.save(Keys.collectionsPrefix + userId, collections); this.decryptedCollectionCache = null; } - - getLocaleSortingFunction(): (a: CollectionView, b: CollectionView) => number { - return (a, b) => { - if (a.name == null && b.name != null) { - return -1; - } - if (a.name != null && b.name == null) { - return 1; - } - if (a.name == null && b.name == null) { - return 0; - } - - return this.i18nService.collator ? this.i18nService.collator.compare(a.name, b.name) : - a.name.localeCompare(b.name); - }; - } } diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index 51ec8e4bb8..955cc2387a 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -17,6 +17,8 @@ import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; import { CipherData } from '../models/data/cipherData'; +import { Utils } from '../misc/utils'; + const Keys = { foldersPrefix: 'folders_', ciphersPrefix: 'ciphers_', @@ -82,7 +84,7 @@ export class FolderService implements FolderServiceAbstraction { }); await Promise.all(promises); - decFolders.sort(this.getLocaleSortingFunction()); + decFolders.sort(Utils.getSortFunction(this.i18nService, 'name')); const noneFolder = new FolderView(); noneFolder.name = this.i18nService.t('noneFolder'); @@ -180,21 +182,4 @@ export class FolderService implements FolderServiceAbstraction { await this.apiService.deleteFolder(id); await this.delete(id); } - - private getLocaleSortingFunction(): (a: FolderView, b: FolderView) => number { - return (a, b) => { - if (a.name == null && b.name != null) { - return -1; - } - if (a.name != null && b.name == null) { - return 1; - } - if (a.name == null && b.name == null) { - return 0; - } - - return this.i18nService.collator ? this.i18nService.collator.compare(a.name, b.name) : - a.name.localeCompare(b.name); - }; - } } From c44e633f42b9972386fb4f3aaff43a0d8506781e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 6 Jul 2018 15:03:00 -0400 Subject: [PATCH 0365/1626] allow prop 3 search --- src/angular/pipes/search.pipe.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/angular/pipes/search.pipe.ts b/src/angular/pipes/search.pipe.ts index 5de3d5c50b..adc6da0daa 100644 --- a/src/angular/pipes/search.pipe.ts +++ b/src/angular/pipes/search.pipe.ts @@ -7,7 +7,7 @@ import { name: 'search', }) export class SearchPipe implements PipeTransform { - transform(items: any[], searchText: string, prop1?: string, prop2?: string): any[] { + transform(items: any[], searchText: string, prop1?: string, prop2?: string, prop3?: string): any[] { if (items == null || items.length === 0) { return []; } @@ -24,6 +24,9 @@ export class SearchPipe implements PipeTransform { if (prop2 != null && i[prop2] != null && i[prop2].toString().toLowerCase().indexOf(searchText) > -1) { return true; } + if (prop3 != null && i[prop3] != null && i[prop3].toString().toLowerCase().indexOf(searchText) > -1) { + return true; + } return false; }); } From 1b7ace04951997ec07d7ffe412dc052df12b7a69 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 6 Jul 2018 23:06:38 -0400 Subject: [PATCH 0366/1626] event apis --- src/abstractions/api.service.ts | 8 +++++ src/enums/eventType.ts | 34 +++++++++++++++++++++ src/models/response/eventResponse.ts | 28 +++++++++++++++++ src/services/api.service.ts | 45 ++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 src/enums/eventType.ts create mode 100644 src/models/response/eventResponse.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 54891450a1..56c668b37c 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -42,6 +42,7 @@ import { } from '../models/response/collectionResponse'; import { CollectionUserResponse } from '../models/response/collectionUserResponse'; import { DomainsResponse } from '../models/response/domainsResponse'; +import { EventResponse } from '../models/response/eventResponse'; import { FolderResponse } from '../models/response/folderResponse'; import { GroupDetailsResponse, @@ -164,4 +165,11 @@ export abstract class ApiService { postOrganization: (request: OrganizationCreateRequest) => Promise; postLeaveOrganization: (id: string) => Promise; postOrganizationLicense: (data: FormData) => Promise; + + getEvents: (start: string, end: string, token: string) => Promise>; + getEventsCipher: (id: string, start: string, end: string, token: string) => Promise>; + getEventsOrganization: (id: string, start: string, end: string, + token: string) => Promise>; + getEventsOrganizationUser: (organizationId: string, id: string, + start: string, end: string, token: string) => Promise> } diff --git a/src/enums/eventType.ts b/src/enums/eventType.ts new file mode 100644 index 0000000000..acaae31d4d --- /dev/null +++ b/src/enums/eventType.ts @@ -0,0 +1,34 @@ +export enum EventType { + User_LoggedIn = 1000, + User_ChangedPassword = 1001, + User_Enabled2fa = 1002, + User_Disabled2fa = 1003, + User_Recovered2fa = 1004, + User_FailedLogIn = 1005, + User_FailedLogIn2fa = 1006, + + Cipher_Created = 1100, + Cipher_Updated = 1101, + Cipher_Deleted = 1102, + Cipher_AttachmentCreated = 1103, + Cipher_AttachmentDeleted = 1104, + Cipher_Shared = 1105, + Cipher_UpdatedCollections = 1106, + + Collection_Created = 1300, + Collection_Updated = 1301, + Collection_Deleted = 1302, + + Group_Created = 1400, + Group_Updated = 1401, + Group_Deleted = 1402, + + OrganizationUser_Invited = 1500, + OrganizationUser_Confirmed = 1501, + OrganizationUser_Updated = 1502, + OrganizationUser_Removed = 1503, + OrganizationUser_UpdatedGroups = 1504, + + Organization_Updated = 1600, + +} diff --git a/src/models/response/eventResponse.ts b/src/models/response/eventResponse.ts new file mode 100644 index 0000000000..1a5ca70ed5 --- /dev/null +++ b/src/models/response/eventResponse.ts @@ -0,0 +1,28 @@ +import { DeviceType } from '../../enums/deviceType'; +import { EventType } from '../../enums/eventType'; + +export class EventResponse { + type: EventType; + userId: string; + organizationId: string; + cipherId: string; + groupId: string; + organizationUserId: string; + actingUserId: string; + date: Date; + deviceType: DeviceType; + ipAddress: string; + + constructor(response: any) { + this.type = response.Type; + this.userId = response.UserId; + this.organizationId = response.OrganizationId; + this.cipherId = response.CipherId; + this.groupId = response.GroupId; + this.organizationUserId = response.OrganizationUserId; + this.actingUserId = response.ActingUserId; + this.date = response.Date; + this.deviceType = response.DeviceType; + this.ipAddress = response.IpAddress; + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index f738e2942e..23520bf4ae 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -49,6 +49,7 @@ import { import { CollectionUserResponse } from '../models/response/collectionUserResponse'; import { DomainsResponse } from '../models/response/domainsResponse'; import { ErrorResponse } from '../models/response/errorResponse'; +import { EventResponse } from '../models/response/eventResponse'; import { FolderResponse } from '../models/response/folderResponse'; import { GroupDetailsResponse, @@ -553,6 +554,35 @@ export class ApiService implements ApiServiceAbstraction { return new OrganizationResponse(r); } + // Event APIs + + async getEvents(start: string, end: string, token: string): Promise> { + const r = await this.send('GET', this.addEventParameters('/events', start, end, token), null, true, true); + return new ListResponse(r, EventResponse); + } + + async getEventsCipher(id: string, start: string, end: string, + token: string): Promise> { + const r = await this.send('GET', this.addEventParameters('/ciphers/' + id + '/events', start, end, token), + null, true, true); + return new ListResponse(r, EventResponse); + } + + async getEventsOrganization(id: string, start: string, end: string, + token: string): Promise> { + const r = await this.send('GET', this.addEventParameters('/organizations/' + id + '/events', start, end, token), + null, true, true); + return new ListResponse(r, EventResponse); + } + + async getEventsOrganizationUser(organizationId: string, id: string, + start: string, end: string, token: string): Promise> { + const r = await this.send('GET', + this.addEventParameters('/organizations/' + organizationId + '/users/' + id + '/events', start, end, token), + null, true, true); + return new ListResponse(r, EventResponse); + } + // Helpers private async send(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body: any, @@ -671,4 +701,19 @@ export class ApiService implements ApiServiceAbstraction { } return undefined; } + + private addEventParameters(base: string, start: string, end: string, token: string) { + if (start != null) { + base += ('?start=' + start); + } + if (end != null) { + base += (base.indexOf('?') > -1 ? '&' : '?'); + base += ('end=' + end); + } + if (token != null) { + base += (base.indexOf('?') > -1 ? '&' : '?'); + base += ('continuationToken=' + token); + } + return base; + } } From a7e7dcc1fe4f945ac8ec33545f9ed259a64afabb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 7 Jul 2018 23:02:53 -0400 Subject: [PATCH 0367/1626] safeincloud xml importer, org import support --- src/abstractions/api.service.ts | 2 +- src/importers/aviraCsvImporter.ts | 4 - src/importers/baseImporter.ts | 15 +++ src/importers/blurCsvImporter.ts | 4 - src/importers/keepassxCsvImporter.ts | 8 +- src/importers/safeInCloudXmlImporter.ts | 121 ++++++++++++++++++++++++ 6 files changed, 141 insertions(+), 13 deletions(-) create mode 100644 src/importers/safeInCloudXmlImporter.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 56c668b37c..4b61c79c4d 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -171,5 +171,5 @@ export abstract class ApiService { getEventsOrganization: (id: string, start: string, end: string, token: string) => Promise>; getEventsOrganizationUser: (organizationId: string, id: string, - start: string, end: string, token: string) => Promise> + start: string, end: string, token: string) => Promise>; } diff --git a/src/importers/aviraCsvImporter.ts b/src/importers/aviraCsvImporter.ts index 15f9ddf491..bb10ad3927 100644 --- a/src/importers/aviraCsvImporter.ts +++ b/src/importers/aviraCsvImporter.ts @@ -10,10 +10,6 @@ import { CipherType } from '../enums/cipherType'; export class AviraCsvImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { - if (this.organization) { - throw new Error('Organization import not supported.'); - } - const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index 009a7ccdc2..19b43104de 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -53,6 +53,12 @@ export abstract class BaseImporter { 'ort', 'adresse', ]; + protected parseXml(data: string): Document { + const parser = new DOMParser(); + const doc = parser.parseFromString(data, 'application/xml'); + return doc != null && doc.querySelector('parsererror') == null ? doc : null; + } + protected parseCsv(data: string, header: boolean): any[] { const result = papa.parse(data, { header: header, @@ -210,4 +216,13 @@ export abstract class BaseImporter { result.folderRelationships = new Map(); result.folders = []; } + + protected querySelectorDirectChild(parentEl: Element, query: string) { + const els = this.querySelectorAllDirectChild(parentEl, query); + return els.length === 0 ? null : els[0]; + } + + protected querySelectorAllDirectChild(parentEl: Element, query: string) { + return Array.from(parentEl.querySelectorAll(query)).filter((el) => el.parentNode === parentEl); + } } diff --git a/src/importers/blurCsvImporter.ts b/src/importers/blurCsvImporter.ts index efe91de808..33620181b5 100644 --- a/src/importers/blurCsvImporter.ts +++ b/src/importers/blurCsvImporter.ts @@ -10,10 +10,6 @@ import { CipherType } from '../enums/cipherType'; export class BlurCsvImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { - if (this.organization) { - throw new Error('Organization import not supported.'); - } - const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { diff --git a/src/importers/keepassxCsvImporter.ts b/src/importers/keepassxCsvImporter.ts index 59325efe9d..cec7fa4e68 100644 --- a/src/importers/keepassxCsvImporter.ts +++ b/src/importers/keepassxCsvImporter.ts @@ -11,10 +11,6 @@ import { CipherType } from '../enums/cipherType'; export class KeePassXCsvImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { - if (this.organization) { - throw new Error('Organization import not supported.'); - } - const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { @@ -66,6 +62,10 @@ export class KeePassXCsvImporter extends BaseImporter implements Importer { result.ciphers.push(cipher); }); + if (this.organization) { + this.moveFoldersToCollections(result); + } + result.success = true; return result; } diff --git a/src/importers/safeInCloudXmlImporter.ts b/src/importers/safeInCloudXmlImporter.ts new file mode 100644 index 0000000000..43c8285754 --- /dev/null +++ b/src/importers/safeInCloudXmlImporter.ts @@ -0,0 +1,121 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CipherView } from '../models/view/cipherView'; +import { FieldView } from '../models/view/fieldView'; +import { FolderView } from '../models/view/folderView'; +import { LoginView } from '../models/view/loginView'; +import { SecureNoteView } from '../models/view/secureNoteView'; + +import { CipherType } from '../enums/cipherType'; +import { FieldType } from '../enums/fieldType'; +import { SecureNoteType } from '../enums/secureNoteType'; + +export class SafeInCloudXmlImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const doc = this.parseXml(data); + if (doc == null) { + result.success = false; + return result; + } + + const db = doc.querySelector('database'); + if (db == null) { + result.errorMessage = 'Missing `database` node.'; + result.success = false; + return result; + } + + const foldersMap = new Map(); + + Array.from(doc.querySelectorAll('database > label')).forEach((labelEl) => { + const name = labelEl.getAttribute('name'); + const id = labelEl.getAttribute('id'); + if (!this.isNullOrWhitespace(name) && !this.isNullOrWhitespace(id)) { + foldersMap.set(id, result.folders.length); + const folder = new FolderView(); + folder.name = name; + result.folders.push(folder); + } + }); + + Array.from(doc.querySelectorAll('database > card')).forEach((cardEl) => { + if (cardEl.getAttribute('template') === 'true') { + return; + } + + const labelIdEl = this.querySelectorDirectChild(cardEl, 'label_id'); + if (labelIdEl != null) { + const labelId = labelIdEl.textContent; + if (!this.isNullOrWhitespace(labelId) && foldersMap.has(labelId)) { + result.folderRelationships.set(result.ciphers.length, foldersMap.get(labelId)); + } + } + + const cipher = new CipherView(); + cipher.favorite = false; + cipher.notes = ''; + cipher.name = this.getValueOrDefault(cardEl.getAttribute('title'), '--'); + cipher.fields = null; + + const cardType = cardEl.getAttribute('type'); + if (cardType === 'note') { + cipher.type = CipherType.SecureNote; + cipher.secureNote = new SecureNoteView(); + cipher.secureNote.type = SecureNoteType.Generic; + } else { + cipher.type = CipherType.Login; + cipher.login = new LoginView(); + Array.from(this.querySelectorAllDirectChild(cardEl, 'field')).forEach((fieldEl) => { + const text = fieldEl.textContent; + if (this.isNullOrWhitespace(text)) { + return; + } + const name = fieldEl.getAttribute('name'); + const fieldType = this.getValueOrDefault(fieldEl.getAttribute('type'), '').toLowerCase(); + if (fieldType === 'login') { + cipher.login.username = text; + } else if (fieldType === 'password') { + cipher.login.password = text; + } else if (fieldType === 'notes') { + cipher.notes += (text + '\n'); + } else if (fieldType === 'weblogin' || fieldType === 'website') { + cipher.login.uris = this.makeUriArray(text); + } else if (text.length > 200) { + cipher.notes += (name + ': ' + text + '\n'); + } else { + if (cipher.fields == null) { + cipher.fields = []; + } + const field = new FieldView(); + field.name = name; + field.value = text; + field.type = FieldType.Text; + cipher.fields.push(field); + } + }); + } + + Array.from(this.querySelectorAllDirectChild(cardEl, 'notes')).forEach((notesEl) => { + cipher.notes += (notesEl.textContent + '\n'); + }); + + cipher.notes = cipher.notes.trim(); + if (cipher.notes === '') { + cipher.notes = null; + } + + result.ciphers.push(cipher); + }); + + if (this.organization) { + this.moveFoldersToCollections(result); + } + + result.success = true; + return result; + } +} From 8ac3450d9eee10c54bc35dd931a70bf82f97d0a5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 7 Jul 2018 23:48:58 -0400 Subject: [PATCH 0368/1626] fetch with proper no-cache --- src/abstractions/api.service.ts | 2 ++ src/services/api.service.ts | 14 +++++++++++--- src/services/audit.service.ts | 7 ++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 4b61c79c4d..ce9e0a0c47 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -172,4 +172,6 @@ export abstract class ApiService { token: string) => Promise>; getEventsOrganizationUser: (organizationId: string, id: string, start: string, end: string, token: string) => Promise>; + + fetch: (request: Request) => Promise; } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 23520bf4ae..162a410dae 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -120,7 +120,7 @@ export class ApiService implements ApiServiceAbstraction { // Auth APIs async postIdentityToken(request: TokenRequest): Promise { - const response = await fetch(new Request(this.identityBaseUrl + '/connect/token', { + const response = await this.fetch(new Request(this.identityBaseUrl + '/connect/token', { body: this.qsStringify(request.toIdentityToken(this.platformUtilsService.identityClientId)), credentials: this.getCredentials(), cache: 'no-cache', @@ -585,6 +585,14 @@ export class ApiService implements ApiServiceAbstraction { // Helpers + fetch(request: Request): Promise { + if (request.method === 'GET') { + request.headers.set('Cache-Control', 'no-cache'); + request.headers.set('Pragma', 'no-cache'); + } + return fetch(request); + } + private async send(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body: any, authed: boolean, hasResponse: boolean): Promise { const headers = new Headers({ @@ -619,7 +627,7 @@ export class ApiService implements ApiServiceAbstraction { } requestInit.headers = headers; - const response = await fetch(new Request(this.apiBaseUrl + path, requestInit)); + const response = await this.fetch(new Request(this.apiBaseUrl + path, requestInit)); if (hasResponse && response.status === 200) { const responseJson = await response.json(); @@ -662,7 +670,7 @@ export class ApiService implements ApiServiceAbstraction { } const decodedToken = this.tokenService.decodeToken(); - const response = await fetch(new Request(this.identityBaseUrl + '/connect/token', { + const response = await this.fetch(new Request(this.identityBaseUrl + '/connect/token', { body: this.qsStringify({ grant_type: 'refresh_token', client_id: decodedToken.client_id, diff --git a/src/services/audit.service.ts b/src/services/audit.service.ts index 2c4e6c1639..9b7b605765 100644 --- a/src/services/audit.service.ts +++ b/src/services/audit.service.ts @@ -1,3 +1,4 @@ +import { ApiService } from '../abstractions/api.service'; import { AuditService as AuditServiceAbstraction } from '../abstractions/audit.service'; import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; @@ -9,7 +10,7 @@ const PwnedPasswordsApi = 'https://api.pwnedpasswords.com/range/'; const HibpBreachApi = 'https://haveibeenpwned.com/api/v2/breachedaccount/'; export class AuditService implements AuditServiceAbstraction { - constructor(private cryptoFunctionService: CryptoFunctionService) { } + constructor(private cryptoFunctionService: CryptoFunctionService, private apiService: ApiService) { } async passwordLeaked(password: string): Promise { const hashBytes = await this.cryptoFunctionService.hash(password, 'sha1'); @@ -17,7 +18,7 @@ export class AuditService implements AuditServiceAbstraction { const hashStart = hash.substr(0, 5); const hashEnding = hash.substr(5); - const response = await fetch(PwnedPasswordsApi + hashStart); + const response = await this.apiService.fetch(new Request(PwnedPasswordsApi + hashStart)); const leakedHashes = await response.text(); const match = leakedHashes.split(/\r?\n/).find((v) => { return v.split(':')[0] === hashEnding; @@ -27,7 +28,7 @@ export class AuditService implements AuditServiceAbstraction { } async breachedAccounts(username: string): Promise { - const response = await fetch(HibpBreachApi + username); + const response = await this.apiService.fetch(new Request(HibpBreachApi + username)); if (response.status === 404) { return []; } else if (response.status !== 200) { From 621a6d1524a4053725a54b9688364ea97958eefa Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 9 Jul 2018 09:04:47 -0400 Subject: [PATCH 0369/1626] adjust enum names for added browser types --- .../services/electronPlatformUtils.service.ts | 6 ++-- src/enums/deviceType.ts | 31 +++++++++++++------ src/misc/analytics.ts | 19 ++++++------ src/services/api.service.ts | 9 ++++-- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index dcd0224816..14b1c270d6 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -31,14 +31,14 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { if (!this.deviceCache) { switch (process.platform) { case 'win32': - this.deviceCache = DeviceType.Windows; + this.deviceCache = DeviceType.WindowsDesktop; break; case 'darwin': - this.deviceCache = DeviceType.MacOs; + this.deviceCache = DeviceType.MacOsDesktop; break; case 'linux': default: - this.deviceCache = DeviceType.Linux; + this.deviceCache = DeviceType.LinuxDesktop; break; } } diff --git a/src/enums/deviceType.ts b/src/enums/deviceType.ts index e3e2fecb92..6f8ddd21ac 100644 --- a/src/enums/deviceType.ts +++ b/src/enums/deviceType.ts @@ -1,12 +1,23 @@ export enum DeviceType { - Chrome = 2, - Firefox = 3, - Opera = 4, - Edge = 5, - Windows = 6, - MacOs = 7, - Linux = 8, - Vivaldi = 19, - Safari = 20, - Web = 21, + Android = 0, + iOS = 1, + ChromeExtension = 2, + FirefoxExtension = 3, + OperaExtension = 4, + EdgeExtension = 5, + WindowsDesktop = 6, + MacOsDesktop = 7, + LinuxDesktop = 8, + ChromeBrowser = 9, + FirefoxBrowser = 10, + OperaBrowser = 11, + EdgeBrowser = 12, + IEBrowser = 13, + UnknownBrowser = 14, + AndroidAmazon = 15, + UWP = 16, + SafariBrowser = 17, + VivaldiBrowser = 18, + VivaldiExtension = 19, + SafariExtension = 20, } diff --git a/src/misc/analytics.ts b/src/misc/analytics.ts index 7f9f565932..37d665f72d 100644 --- a/src/misc/analytics.ts +++ b/src/misc/analytics.ts @@ -9,16 +9,15 @@ import { DeviceType } from '../enums/deviceType'; const GaObj = 'ga'; export const AnalyticsIds = { - [DeviceType.Chrome]: 'UA-81915606-6', - [DeviceType.Firefox]: 'UA-81915606-7', - [DeviceType.Opera]: 'UA-81915606-8', - [DeviceType.Edge]: 'UA-81915606-9', - [DeviceType.Vivaldi]: 'UA-81915606-15', - [DeviceType.Safari]: 'UA-81915606-16', - [DeviceType.Windows]: 'UA-81915606-17', - [DeviceType.Linux]: 'UA-81915606-19', - [DeviceType.MacOs]: 'UA-81915606-18', - [DeviceType.Web]: 'UA-81915606-3', + [DeviceType.ChromeExtension]: 'UA-81915606-6', + [DeviceType.FirefoxExtension]: 'UA-81915606-7', + [DeviceType.OperaExtension]: 'UA-81915606-8', + [DeviceType.EdgeExtension]: 'UA-81915606-9', + [DeviceType.VivaldiExtension]: 'UA-81915606-15', + [DeviceType.SafariExtension]: 'UA-81915606-16', + [DeviceType.WindowsDesktop]: 'UA-81915606-17', + [DeviceType.LinuxDesktop]: 'UA-81915606-19', + [DeviceType.MacOsDesktop]: 'UA-81915606-18', }; export class Analytics { diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 162a410dae..7467cd0d07 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -85,9 +85,12 @@ export class ApiService implements ApiServiceAbstraction { private logoutCallback: (expired: boolean) => Promise) { const device = platformUtilsService.getDevice(); this.deviceType = device.toString(); - this.isWebClient = device === DeviceType.Web; - this.isDesktopClient = device === DeviceType.Windows || device === DeviceType.MacOs || - device === DeviceType.Linux; + this.isWebClient = device === DeviceType.IEBrowser || device === DeviceType.ChromeBrowser || + device === DeviceType.EdgeBrowser || device === DeviceType.FirefoxBrowser || + device === DeviceType.OperaBrowser || device === DeviceType.SafariBrowser || + device === DeviceType.UnknownBrowser || device === DeviceType.VivaldiBrowser; + this.isDesktopClient = device === DeviceType.WindowsDesktop || device === DeviceType.MacOsDesktop || + device === DeviceType.LinuxDesktop; } setUrls(urls: EnvironmentUrls): void { From 9a73e733512e69221d6c74065bc8ec614c7ba0fd Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 9 Jul 2018 09:30:15 -0400 Subject: [PATCH 0370/1626] update device string for electron --- src/electron/services/electronPlatformUtils.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index 14b1c270d6..fdd0bd86a1 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -47,7 +47,8 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { } getDeviceString(): string { - return DeviceType[this.getDevice()].toLowerCase(); + const device = DeviceType[this.getDevice()].toLowerCase(); + return device.replace('desktop', ''); } isFirefox(): boolean { From 0a46513e38297c49aebbbfb71e73e20aaa752780 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 9 Jul 2018 11:49:16 -0400 Subject: [PATCH 0371/1626] add missing collectionid --- src/models/response/eventResponse.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/models/response/eventResponse.ts b/src/models/response/eventResponse.ts index 1a5ca70ed5..db44bc1a7a 100644 --- a/src/models/response/eventResponse.ts +++ b/src/models/response/eventResponse.ts @@ -6,6 +6,7 @@ export class EventResponse { userId: string; organizationId: string; cipherId: string; + collectionId: string; groupId: string; organizationUserId: string; actingUserId: string; @@ -18,6 +19,7 @@ export class EventResponse { this.userId = response.UserId; this.organizationId = response.OrganizationId; this.cipherId = response.CipherId; + this.collectionId = response.CollectionId; this.groupId = response.GroupId; this.organizationUserId = response.OrganizationUserId; this.actingUserId = response.ActingUserId; From 89e71d7c16d95fd7cc9634d46e0db1d77e6fcae6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 9 Jul 2018 16:27:03 -0400 Subject: [PATCH 0372/1626] fix org api paths --- src/abstractions/api.service.ts | 12 ++++++------ src/services/api.service.ts | 25 +++++++++++++------------ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index ce9e0a0c47..8238556992 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -125,16 +125,16 @@ export abstract class ApiService { getCollectionDetails: (organizationId: string, id: string) => Promise; getCollections: (organizationId: string) => Promise>; getCollectionUsers: (organizationId: string, id: string) => Promise>; - postCollection: (request: CollectionRequest) => Promise; - putCollection: (id: string, request: CollectionRequest) => Promise; - deleteCollection: (id: string) => Promise; + postCollection: (organizationId: string, request: CollectionRequest) => Promise; + putCollection: (organizationId: string, id: string, request: CollectionRequest) => Promise; + deleteCollection: (organizationId: string, id: string) => Promise; getGroupDetails: (organizationId: string, id: string) => Promise; getGroups: (organizationId: string) => Promise>; getGroupUsers: (organizationId: string, id: string) => Promise>; - postGroup: (request: GroupRequest) => Promise; - putGroup: (id: string, request: GroupRequest) => Promise; - deleteGroup: (id: string) => Promise; + postGroup: (organizationId: string, request: GroupRequest) => Promise; + putGroup: (organizationId: string, id: string, request: GroupRequest) => Promise; + deleteGroup: (organizationId: string, id: string) => Promise; getOrganizationUsers: (organizationId: string) => Promise>; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 7467cd0d07..1452ea17be 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -383,18 +383,19 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, CollectionUserResponse); } - async postCollection(request: CollectionRequest): Promise { - const r = await this.send('POST', '/collections', request, true, true); + async postCollection(organizationId: string, request: CollectionRequest): Promise { + const r = await this.send('POST', '/organizations/' + organizationId + '/collections', request, true, true); return new CollectionResponse(r); } - async putCollection(id: string, request: CollectionRequest): Promise { - const r = await this.send('PUT', '/collections/' + id, request, true, true); + async putCollection(organizationId: string, id: string, request: CollectionRequest): Promise { + const r = await this.send('PUT', '/organizations/' + organizationId + '/collections/' + id, + request, true, true); return new CollectionResponse(r); } - deleteCollection(id: string): Promise { - return this.send('DELETE', '/collections/' + id, null, true, false); + deleteCollection(organizationId: string, id: string): Promise { + return this.send('DELETE', '/organizations/' + organizationId + '/collections/' + id, null, true, false); } // Groups APIs @@ -416,18 +417,18 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, GroupUserResponse); } - async postGroup(request: GroupRequest): Promise { - const r = await this.send('POST', '/groups', request, true, true); + async postGroup(organizationId: string, request: GroupRequest): Promise { + const r = await this.send('POST', '/organizations/' + organizationId + '/groups', request, true, true); return new GroupResponse(r); } - async putGroup(id: string, request: GroupRequest): Promise { - const r = await this.send('PUT', '/groups/' + id, request, true, true); + async putGroup(organizationId: string, id: string, request: GroupRequest): Promise { + const r = await this.send('PUT', '/organizations/' + organizationId + '/groups/' + id, request, true, true); return new GroupResponse(r); } - deleteGroup(id: string): Promise { - return this.send('DELETE', '/groups/' + id, null, true, false); + deleteGroup(organizationId: string, id: string): Promise { + return this.send('DELETE', '/organizations/' + organizationId + '/groups/' + id, null, true, false); } // Organization User APIs From 049e129f3647ee807bdc68a3d0a38854b4ba0256 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 9 Jul 2018 23:47:12 -0400 Subject: [PATCH 0373/1626] added delete entity user apis --- src/abstractions/api.service.ts | 2 ++ src/services/api.service.ts | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 8238556992..6600b8ccbb 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -128,6 +128,7 @@ export abstract class ApiService { postCollection: (organizationId: string, request: CollectionRequest) => Promise; putCollection: (organizationId: string, id: string, request: CollectionRequest) => Promise; deleteCollection: (organizationId: string, id: string) => Promise; + deleteCollectionUser: (organizationId: string, id: string, organizationUserId: string) => Promise; getGroupDetails: (organizationId: string, id: string) => Promise; getGroups: (organizationId: string) => Promise>; @@ -135,6 +136,7 @@ export abstract class ApiService { postGroup: (organizationId: string, request: GroupRequest) => Promise; putGroup: (organizationId: string, id: string, request: GroupRequest) => Promise; deleteGroup: (organizationId: string, id: string) => Promise; + deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise; getOrganizationUsers: (organizationId: string) => Promise>; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 1452ea17be..54a34f2d0d 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -398,6 +398,12 @@ export class ApiService implements ApiServiceAbstraction { return this.send('DELETE', '/organizations/' + organizationId + '/collections/' + id, null, true, false); } + deleteCollectionUser(organizationId: string, id: string, organizationUserId: string): Promise { + return this.send('DELETE', + '/organizations/' + organizationId + '/collections/' + id + '/user/' + organizationUserId, + null, true, false); + } + // Groups APIs async getGroupDetails(organizationId: string, id: string): Promise { @@ -431,6 +437,11 @@ export class ApiService implements ApiServiceAbstraction { return this.send('DELETE', '/organizations/' + organizationId + '/groups/' + id, null, true, false); } + deleteGroupUser(organizationId: string, id: string, organizationUserId: string): Promise { + return this.send('DELETE', + '/organizations/' + organizationId + '/groups/' + id + '/user/' + organizationUserId, null, true, false); + } + // Organization User APIs async getOrganizationUsers(organizationId: string): Promise> { From 36ab2ec78b0b235008312ab70c16882d28fd76b7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 10 Jul 2018 09:19:15 -0400 Subject: [PATCH 0374/1626] add missing permissions to organization objs --- src/models/data/organizationData.ts | 18 ++++++++++++++++++ src/models/domain/organization.ts | 18 ++++++++++++++++++ .../response/profileOrganizationResponse.ts | 4 ++++ 3 files changed, 40 insertions(+) diff --git a/src/models/data/organizationData.ts b/src/models/data/organizationData.ts index 1af5223391..0f5bc0da95 100644 --- a/src/models/data/organizationData.ts +++ b/src/models/data/organizationData.ts @@ -9,6 +9,15 @@ export class OrganizationData { status: OrganizationUserStatusType; type: OrganizationUserType; enabled: boolean; + useGroups: boolean; + useDirectory: boolean; + useEvents: boolean; + useTotp: boolean; + use2fa: boolean; + selfHost: boolean; + usersGetPremium: boolean; + seats: number; + maxCollections: number; constructor(response: ProfileOrganizationResponse) { this.id = response.id; @@ -16,5 +25,14 @@ export class OrganizationData { this.status = response.status; this.type = response.type; this.enabled = response.enabled; + this.useGroups = response.useGroups; + this.useDirectory = response.useDirectory; + this.useEvents = response.useEvents; + this.useTotp = response.useTotp; + this.use2fa = response.use2fa; + this.selfHost = response.selfHost; + this.usersGetPremium = response.usersGetPremium; + this.seats = response.seats; + this.maxCollections = response.maxCollections; } } diff --git a/src/models/domain/organization.ts b/src/models/domain/organization.ts index ce243e838a..bd38d15883 100644 --- a/src/models/domain/organization.ts +++ b/src/models/domain/organization.ts @@ -9,6 +9,15 @@ export class Organization { status: OrganizationUserStatusType; type: OrganizationUserType; enabled: boolean; + useGroups: boolean; + useDirectory: boolean; + useEvents: boolean; + useTotp: boolean; + use2fa: boolean; + selfHost: boolean; + usersGetPremium: boolean; + seats: number; + maxCollections: number; constructor(obj?: OrganizationData) { if (obj == null) { @@ -20,6 +29,15 @@ export class Organization { this.status = obj.status; this.type = obj.type; this.enabled = obj.enabled; + this.useGroups = obj.useGroups; + this.useDirectory = obj.useDirectory; + this.useEvents = obj.useEvents; + this.useTotp = obj.useTotp; + this.use2fa = obj.use2fa; + this.selfHost = obj.selfHost; + this.usersGetPremium = obj.usersGetPremium; + this.seats = obj.seats; + this.maxCollections = obj.maxCollections; } get canAccess() { diff --git a/src/models/response/profileOrganizationResponse.ts b/src/models/response/profileOrganizationResponse.ts index 7ea65f87e9..72bbc48387 100644 --- a/src/models/response/profileOrganizationResponse.ts +++ b/src/models/response/profileOrganizationResponse.ts @@ -9,6 +9,8 @@ export class ProfileOrganizationResponse { useEvents: boolean; useTotp: boolean; use2fa: boolean; + selfHost: boolean; + usersGetPremium: boolean; seats: number; maxCollections: number; maxStorageGb?: number; @@ -25,6 +27,8 @@ export class ProfileOrganizationResponse { this.useEvents = response.UseEvents; this.useTotp = response.UseTotp; this.use2fa = response.Use2fa; + this.selfHost = response.SelfHost; + this.usersGetPremium = response.UsersGetPremium; this.seats = response.Seats; this.maxCollections = response.MaxCollections; this.maxStorageGb = response.MaxStorageGb; From bded5eb625e3eb8f7577ad3da54d5c3b7e543eb0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 10 Jul 2018 10:06:47 -0400 Subject: [PATCH 0375/1626] bug fixes in api models --- src/models/request/collectionRequest.ts | 5 ++++- src/models/response/collectionResponse.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/models/request/collectionRequest.ts b/src/models/request/collectionRequest.ts index e5f427624b..716b0acab5 100644 --- a/src/models/request/collectionRequest.ts +++ b/src/models/request/collectionRequest.ts @@ -6,7 +6,10 @@ export class CollectionRequest { name: string; groups: SelectionReadOnlyRequest[] = []; - constructor(collection: Collection) { + constructor(collection?: Collection) { + if (collection == null) { + return; + } this.name = collection.name ? collection.name.encryptedString : null; } } diff --git a/src/models/response/collectionResponse.ts b/src/models/response/collectionResponse.ts index d04304b785..14e839cf35 100644 --- a/src/models/response/collectionResponse.ts +++ b/src/models/response/collectionResponse.ts @@ -27,7 +27,7 @@ export class CollectionGroupDetailsResponse extends CollectionResponse { constructor(response: any) { super(response); if (response.Groups != null) { - this.groups = response.Collections.map((g: any) => new SelectionReadOnlyResponse(g)); + this.groups = response.Groups.map((g: any) => new SelectionReadOnlyResponse(g)); } } } From c76cbf1274ec4c3cf7b6c12329259f4887b005d4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 10 Jul 2018 13:03:24 -0400 Subject: [PATCH 0376/1626] org user apis --- src/abstractions/api.service.ts | 22 +++++++- .../request/organizationUserAcceptRequest.ts | 3 ++ .../request/organizationUserConfirmRequest.ts | 3 ++ .../request/organizationUserInviteRequest.ts | 10 ++++ .../organizationUserUpdateGroupsRequest.ts | 3 ++ .../request/organizationUserUpdateRequest.ts | 9 ++++ src/services/api.service.ts | 53 ++++++++++++++++++- 7 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 src/models/request/organizationUserAcceptRequest.ts create mode 100644 src/models/request/organizationUserConfirmRequest.ts create mode 100644 src/models/request/organizationUserInviteRequest.ts create mode 100644 src/models/request/organizationUserUpdateGroupsRequest.ts create mode 100644 src/models/request/organizationUserUpdateRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 6600b8ccbb..07cc8118d0 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -16,6 +16,11 @@ import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest'; import { KeysRequest } from '../models/request/keysRequest'; import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest'; +import { OrganizationUserAcceptRequest } from '../models/request/organizationUserAcceptRequest'; +import { OrganizationUserConfirmRequest } from '../models/request/organizationUserConfirmRequest'; +import { OrganizationUserInviteRequest } from '../models/request/organizationUserInviteRequest'; +import { OrganizationUserUpdateGroupsRequest } from '../models/request/organizationUserUpdateGroupsRequest'; +import { OrganizationUserUpdateRequest } from '../models/request/organizationUserUpdateRequest'; import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; @@ -53,7 +58,10 @@ import { IdentityTokenResponse } from '../models/response/identityTokenResponse' import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; import { ListResponse } from '../models/response/listResponse'; import { OrganizationResponse } from '../models/response/organizationResponse'; -import { OrganizationUserUserDetailsResponse } from '../models/response/organizationUserResponse'; +import { + OrganizationUserDetailsResponse, + OrganizationUserUserDetailsResponse, +} from '../models/response/organizationUserResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SyncResponse } from '../models/response/syncResponse'; import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; @@ -138,7 +146,19 @@ export abstract class ApiService { deleteGroup: (organizationId: string, id: string) => Promise; deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise; + getOrganizationUser: (organizationId: string, id: string) => Promise; + getOrganizationUserGroups: (organizationId: string, id: string) => Promise; getOrganizationUsers: (organizationId: string) => Promise>; + postOrganizationUserInvite: (organizationId: string, request: OrganizationUserInviteRequest) => Promise; + postOrganizationUserReinvite: (organizationId: string, id: string) => Promise; + postOrganizationUserAccept: (organizationId: string, id: string, + request: OrganizationUserAcceptRequest) => Promise; + postOrganizationUserConfirm: (organizationId: string, id: string, + request: OrganizationUserConfirmRequest) => Promise; + putOrganizationUser: (organizationId: string, id: string, request: OrganizationUserUpdateRequest) => Promise; + putOrganizationUserGroups: (organizationId: string, id: string, + request: OrganizationUserUpdateGroupsRequest) => Promise; + deleteOrganizationUser: (organizationId: string, id: string) => Promise; getSync: () => Promise; postImportDirectory: (organizationId: string, request: ImportDirectoryRequest) => Promise; diff --git a/src/models/request/organizationUserAcceptRequest.ts b/src/models/request/organizationUserAcceptRequest.ts new file mode 100644 index 0000000000..acd36a679e --- /dev/null +++ b/src/models/request/organizationUserAcceptRequest.ts @@ -0,0 +1,3 @@ +export class OrganizationUserAcceptRequest { + token: string; +} diff --git a/src/models/request/organizationUserConfirmRequest.ts b/src/models/request/organizationUserConfirmRequest.ts new file mode 100644 index 0000000000..c4d233052a --- /dev/null +++ b/src/models/request/organizationUserConfirmRequest.ts @@ -0,0 +1,3 @@ +export class OrganizationUserConfirmRequest { + key: string; +} diff --git a/src/models/request/organizationUserInviteRequest.ts b/src/models/request/organizationUserInviteRequest.ts new file mode 100644 index 0000000000..79371e99ed --- /dev/null +++ b/src/models/request/organizationUserInviteRequest.ts @@ -0,0 +1,10 @@ +import { OrganizationUserType } from '../../enums/organizationUserType'; + +import { SelectionReadOnlyRequest } from './selectionReadOnlyRequest'; + +export class OrganizationUserInviteRequest { + emails: string[] = []; + type: OrganizationUserType; + accessAll: boolean; + collections: SelectionReadOnlyRequest[] = []; +} diff --git a/src/models/request/organizationUserUpdateGroupsRequest.ts b/src/models/request/organizationUserUpdateGroupsRequest.ts new file mode 100644 index 0000000000..0d7805ccdf --- /dev/null +++ b/src/models/request/organizationUserUpdateGroupsRequest.ts @@ -0,0 +1,3 @@ +export class OrganizationUserUpdateGroupsRequest { + groupIds: string[] = []; +} diff --git a/src/models/request/organizationUserUpdateRequest.ts b/src/models/request/organizationUserUpdateRequest.ts new file mode 100644 index 0000000000..7b8c9cd237 --- /dev/null +++ b/src/models/request/organizationUserUpdateRequest.ts @@ -0,0 +1,9 @@ +import { OrganizationUserType } from '../../enums/organizationUserType'; + +import { SelectionReadOnlyRequest } from './selectionReadOnlyRequest'; + +export class OrganizationUserUpdateRequest { + type: OrganizationUserType; + accessAll: boolean; + collections: SelectionReadOnlyRequest[] = []; +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 54a34f2d0d..4a72e469ad 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -22,6 +22,11 @@ import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest'; import { KeysRequest } from '../models/request/keysRequest'; import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest'; +import { OrganizationUserAcceptRequest } from '../models/request/organizationUserAcceptRequest'; +import { OrganizationUserConfirmRequest } from '../models/request/organizationUserConfirmRequest'; +import { OrganizationUserInviteRequest } from '../models/request/organizationUserInviteRequest'; +import { OrganizationUserUpdateGroupsRequest } from '../models/request/organizationUserUpdateGroupsRequest'; +import { OrganizationUserUpdateRequest } from '../models/request/organizationUserUpdateRequest'; import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; @@ -60,7 +65,10 @@ import { IdentityTokenResponse } from '../models/response/identityTokenResponse' import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; import { ListResponse } from '../models/response/listResponse'; import { OrganizationResponse } from '../models/response/organizationResponse'; -import { OrganizationUserUserDetailsResponse } from '../models/response/organizationUserResponse'; +import { + OrganizationUserDetailsResponse, + OrganizationUserUserDetailsResponse, +} from '../models/response/organizationUserResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SyncResponse } from '../models/response/syncResponse'; import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; @@ -444,11 +452,54 @@ export class ApiService implements ApiServiceAbstraction { // Organization User APIs + async getOrganizationUser(organizationId: string, id: string): Promise { + const r = await this.send('GET', '/organizations/' + organizationId + '/users/' + id, null, true, true); + return new OrganizationUserDetailsResponse(r); + } + + async getOrganizationUserGroups(organizationId: string, id: string): Promise { + const r = await this.send('GET', '/organizations/' + organizationId + '/users/' + id + '/groups', + null, true, true); + return r; + } + async getOrganizationUsers(organizationId: string): Promise> { const r = await this.send('GET', '/organizations/' + organizationId + '/users', null, true, true); return new ListResponse(r, OrganizationUserUserDetailsResponse); } + postOrganizationUserInvite(organizationId: string, request: OrganizationUserInviteRequest): Promise { + return this.send('POST', '/organizations/' + organizationId + '/users/invite', request, true, false); + } + + postOrganizationUserReinvite(organizationId: string, id: string): Promise { + return this.send('POST', '/organizations/' + organizationId + '/users/' + id + '/reinvite', null, true, false); + } + + postOrganizationUserAccept(organizationId: string, id: string, + request: OrganizationUserAcceptRequest): Promise { + return this.send('POST', '/organizations/' + organizationId + '/users/' + id + '/accept', request, true, false); + } + + postOrganizationUserConfirm(organizationId: string, id: string, + request: OrganizationUserConfirmRequest): Promise { + return this.send('POST', '/organizations/' + organizationId + '/users/' + id + '/confirm', + request, true, false); + } + + putOrganizationUser(organizationId: string, id: string, request: OrganizationUserUpdateRequest): Promise { + return this.send('PUT', '/organizations/' + organizationId + '/users/' + id, request, true, false); + } + + putOrganizationUserGroups(organizationId: string, id: string, + request: OrganizationUserUpdateGroupsRequest): Promise { + return this.send('PUT', '/organizations/' + organizationId + '/users/' + id + '/groups', request, true, false); + } + + deleteOrganizationUser(organizationId: string, id: string): Promise { + return this.send('DELETE', '/organizations/' + organizationId + '/users/' + id, null, true, false); + } + // Sync APIs async getSync(): Promise { From a6a0673af8eb11ebe1afc7324751c6ab0a6e338b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 10 Jul 2018 14:43:59 -0400 Subject: [PATCH 0377/1626] fix model bug --- src/models/response/organizationUserResponse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/response/organizationUserResponse.ts b/src/models/response/organizationUserResponse.ts index 12006b665d..7f377b5e95 100644 --- a/src/models/response/organizationUserResponse.ts +++ b/src/models/response/organizationUserResponse.ts @@ -30,7 +30,7 @@ export class OrganizationUserUserDetailsResponse extends OrganizationUserRespons } export class OrganizationUserDetailsResponse extends OrganizationUserResponse { - collections: SelectionReadOnlyResponse; + collections: SelectionReadOnlyResponse[] = []; constructor(response: any) { super(response); From 4004449aa833a2a445fcdb2a9c80f4a014de0b89 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 10 Jul 2018 16:38:43 -0400 Subject: [PATCH 0378/1626] padlock importer. move relationships to arrays --- src/importers/baseImporter.ts | 8 +- src/importers/bitwardenCsvImporter.ts | 6 +- src/importers/keepassxCsvImporter.ts | 2 +- src/importers/lastpassCsvImporter.ts | 2 +- src/importers/padlockCsvImporter.ts | 135 ++++++++++++++++++++++++ src/importers/safeInCloudXmlImporter.ts | 2 +- src/models/domain/importResult.ts | 4 +- 7 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 src/importers/padlockCsvImporter.ts diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index 19b43104de..8bdadfe7cf 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -10,6 +10,8 @@ import { Utils } from '../misc/utils'; export abstract class BaseImporter { organization = false; + protected newLineRegex = /(?:\r\n|\r|\n)/; + protected passwordFieldNames = [ 'password', 'pass word', 'passphrase', 'pass phrase', 'pass', 'code', 'code word', 'codeword', @@ -145,7 +147,7 @@ export abstract class BaseImporter { } protected splitNewLine(str: string): string[] { - return str.split(/(?:\r\n|\r|\n)/); + return str.split(this.newLineRegex); } // ref https://stackoverflow.com/a/5911300 @@ -207,13 +209,13 @@ export abstract class BaseImporter { } protected moveFoldersToCollections(result: ImportResult) { - result.folderRelationships.forEach((value, key) => result.collectionRelationships.set(key, value)); + result.folderRelationships.forEach((r) => result.collectionRelationships.push(r)); result.collections = result.folders.map((f) => { const collection = new CollectionView(); collection.name = f.name; return collection; }); - result.folderRelationships = new Map(); + result.folderRelationships = []; result.folders = []; } diff --git a/src/importers/bitwardenCsvImporter.ts b/src/importers/bitwardenCsvImporter.ts index 29d83e70cb..76ea7aa08b 100644 --- a/src/importers/bitwardenCsvImporter.ts +++ b/src/importers/bitwardenCsvImporter.ts @@ -44,11 +44,11 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer { result.collections.push(collection); } - result.collectionRelationships.set(result.ciphers.length, collectionIndex); + result.collectionRelationships.push([result.ciphers.length, collectionIndex]); }); } else if (!this.organization) { let folderIndex = result.folders.length; - const hasFolder = !this.organization && !this.isNullOrWhitespace(value.folder); + const hasFolder = !this.isNullOrWhitespace(value.folder); let addFolder = hasFolder; if (hasFolder) { @@ -68,7 +68,7 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer { } if (hasFolder) { - result.folderRelationships.set(result.ciphers.length, folderIndex); + result.folderRelationships.push([result.ciphers.length, folderIndex]); } } diff --git a/src/importers/keepassxCsvImporter.ts b/src/importers/keepassxCsvImporter.ts index cec7fa4e68..141e6e1771 100644 --- a/src/importers/keepassxCsvImporter.ts +++ b/src/importers/keepassxCsvImporter.ts @@ -47,7 +47,7 @@ export class KeePassXCsvImporter extends BaseImporter implements Importer { result.folders.push(f); } if (hasFolder) { - result.folderRelationships.set(result.ciphers.length, folderIndex); + result.folderRelationships.push([result.ciphers.length, folderIndex]); } const cipher = new CipherView(); diff --git a/src/importers/lastpassCsvImporter.ts b/src/importers/lastpassCsvImporter.ts index fd9d9f1b94..cdaa0629aa 100644 --- a/src/importers/lastpassCsvImporter.ts +++ b/src/importers/lastpassCsvImporter.ts @@ -76,7 +76,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { result.folders.push(f); } if (hasFolder) { - result.folderRelationships.set(cipherIndex, folderIndex); + result.folderRelationships.push([cipherIndex, folderIndex]); } }); diff --git a/src/importers/padlockCsvImporter.ts b/src/importers/padlockCsvImporter.ts new file mode 100644 index 0000000000..dea2fdc99e --- /dev/null +++ b/src/importers/padlockCsvImporter.ts @@ -0,0 +1,135 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CipherView } from '../models/view/cipherView'; +import { CollectionView } from '../models/view/collectionView'; +import { FieldView } from '../models/view/fieldView'; +import { FolderView } from '../models/view/folderView'; +import { LoginView } from '../models/view/loginView'; + +import { CipherType } from '../enums/cipherType'; +import { FieldType } from '../enums/fieldType'; + +export class PadlockCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, false); + if (results == null) { + result.success = false; + return result; + } + + let headers: string[] = null; + results.forEach((value) => { + if (headers == null) { + headers = value.map((v: string) => v); + return; + } + + if (value.length < 2 || value.length !== headers.length) { + return; + } + + if (!this.isNullOrWhitespace(value[1])) { + if (this.organization) { + const tags = (value[1] as string).split(','); + tags.forEach((tag) => { + tag = tag.trim(); + let addCollection = true; + let collectionIndex = result.collections.length; + + for (let i = 0; i < result.collections.length; i++) { + if (result.collections[i].name === tag) { + addCollection = false; + collectionIndex = i; + break; + } + } + + if (addCollection) { + const collection = new CollectionView(); + collection.name = tag; + result.collections.push(collection); + } + + result.collectionRelationships.push([result.ciphers.length, collectionIndex]); + }); + } else { + const tags = (value[1] as string).split(','); + let folderIndex = result.folders.length; + const hasFolder = tags.length > 0 && !this.isNullOrWhitespace(tags[0].trim()); + let addFolder = hasFolder; + const tag = tags[0].trim(); + + if (hasFolder) { + for (let i = 0; i < result.folders.length; i++) { + if (result.folders[i].name === tag) { + addFolder = false; + folderIndex = i; + break; + } + } + } + + if (addFolder) { + const f = new FolderView(); + f.name = tag; + result.folders.push(f); + } + if (hasFolder) { + result.folderRelationships.push([result.ciphers.length, folderIndex]); + } + } + } + + const cipher = new CipherView(); + cipher.type = CipherType.Login; + cipher.favorite = false; + cipher.notes = ''; + cipher.fields = []; + cipher.name = this.getValueOrDefault(value[0], '--'); + cipher.login = new LoginView(); + + for (let i = 2; i < value.length; i++) { + const header = headers[i].trim().toLowerCase(); + if (this.isNullOrWhitespace(value[i]) || this.isNullOrWhitespace(header)) { + continue; + } + + if (this.usernameFieldNames.indexOf(header) > -1) { + cipher.login.username = value[i]; + } else if (this.passwordFieldNames.indexOf(header) > -1) { + cipher.login.password = value[i]; + } else if (this.uriFieldNames.indexOf(header) > -1) { + cipher.login.uris = this.makeUriArray(value[i]); + } else { + if (value[i].length > 200 || value[i].search(this.newLineRegex) > -1) { + cipher.notes += (headers[i] + ': ' + value[i] + '\n'); + } else { + const field = new FieldView(); + field.type = FieldType.Text; + field.name = headers[i]; + field.value = value[i]; + cipher.fields.push(field); + } + } + } + + if (this.isNullOrWhitespace(cipher.notes)) { + cipher.notes = null; + } else { + cipher.notes = cipher.notes.trim(); + } + if (cipher.fields.length === 0) { + cipher.fields = null; + } + + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} diff --git a/src/importers/safeInCloudXmlImporter.ts b/src/importers/safeInCloudXmlImporter.ts index 43c8285754..1edaf8139f 100644 --- a/src/importers/safeInCloudXmlImporter.ts +++ b/src/importers/safeInCloudXmlImporter.ts @@ -51,7 +51,7 @@ export class SafeInCloudXmlImporter extends BaseImporter implements Importer { if (labelIdEl != null) { const labelId = labelIdEl.textContent; if (!this.isNullOrWhitespace(labelId) && foldersMap.has(labelId)) { - result.folderRelationships.set(result.ciphers.length, foldersMap.get(labelId)); + result.folderRelationships.push([result.ciphers.length, foldersMap.get(labelId)]); } } diff --git a/src/models/domain/importResult.ts b/src/models/domain/importResult.ts index 152d08462e..ab53fb1412 100644 --- a/src/models/domain/importResult.ts +++ b/src/models/domain/importResult.ts @@ -7,7 +7,7 @@ export class ImportResult { errorMessage: string; ciphers: CipherView[] = []; folders: FolderView[] = []; - folderRelationships: Map = new Map(); + folderRelationships: Array<[number, number]> = []; collections: CollectionView[] = []; - collectionRelationships: Map = new Map(); + collectionRelationships: Array<[number, number]> = []; } From c9fc74c5cbefaa782c58df0710ac9ef4dc9b2194 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 10 Jul 2018 17:51:47 -0400 Subject: [PATCH 0379/1626] more importer helpers --- src/importers/aviraCsvImporter.ts | 10 +---- src/importers/baseImporter.ts | 60 +++++++++++++++++++++++++ src/importers/blurCsvImporter.ts | 10 +---- src/importers/keepassxCsvImporter.ts | 10 +---- src/importers/padlockCsvImporter.ts | 33 ++------------ src/importers/safeInCloudXmlImporter.ts | 28 ++---------- 6 files changed, 72 insertions(+), 79 deletions(-) diff --git a/src/importers/aviraCsvImporter.ts b/src/importers/aviraCsvImporter.ts index bb10ad3927..41774ba75c 100644 --- a/src/importers/aviraCsvImporter.ts +++ b/src/importers/aviraCsvImporter.ts @@ -3,11 +3,6 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; -import { CipherView } from '../models/view/cipherView'; -import { LoginView } from '../models/view/loginView'; - -import { CipherType } from '../enums/cipherType'; - export class AviraCsvImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { const result = new ImportResult(); @@ -18,11 +13,9 @@ export class AviraCsvImporter extends BaseImporter implements Importer { } results.forEach((value) => { - const cipher = new CipherView(); - cipher.type = CipherType.Login; + const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(value.name, this.getValueOrDefault(this.nameFromUrl(value.website), '--')); - cipher.login = new LoginView(); cipher.login.uris = this.makeUriArray(value.website); cipher.login.password = this.getValueOrDefault(value.password); @@ -33,6 +26,7 @@ export class AviraCsvImporter extends BaseImporter implements Importer { cipher.notes = this.getValueOrDefault(value.secondary_username); } + this.cleanupCipher(cipher); result.ciphers.push(cipher); }); diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index 8bdadfe7cf..cdef5ef338 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -2,10 +2,16 @@ import * as papa from 'papaparse'; import { ImportResult } from '../models/domain/importResult'; +import { CipherView } from '../models/view/cipherView'; import { CollectionView } from '../models/view/collectionView'; import { LoginUriView } from '../models/view/loginUriView'; import { Utils } from '../misc/utils'; +import { FieldView } from '../models/view/fieldView'; +import { LoginView } from '../models/view/loginView'; + +import { CipherType } from '../enums/cipherType'; +import { FieldType } from '../enums/fieldType'; export abstract class BaseImporter { organization = false; @@ -227,4 +233,58 @@ export abstract class BaseImporter { protected querySelectorAllDirectChild(parentEl: Element, query: string) { return Array.from(parentEl.querySelectorAll(query)).filter((el) => el.parentNode === parentEl); } + + protected initLoginCipher() { + const cipher = new CipherView(); + cipher.favorite = false; + cipher.notes = ''; + cipher.fields = []; + cipher.login = new LoginView(); + cipher.type = CipherType.Login; + return cipher; + } + + protected cleanupCipher(cipher: CipherView) { + if (cipher == null) { + return; + } + if (cipher.type !== CipherType.Login) { + cipher.login = null; + } + if (this.isNullOrWhitespace(cipher.name)) { + cipher.name = '--'; + } + if (this.isNullOrWhitespace(cipher.notes)) { + cipher.notes = null; + } else { + cipher.notes = cipher.notes.trim(); + } + if (cipher.fields != null && cipher.fields.length === 0) { + cipher.fields = null; + } + } + + protected processKvp(cipher: CipherView, key: string, value: string) { + if (this.isNullOrWhitespace(value)) { + return; + } + if (this.isNullOrWhitespace(key)) { + key = ''; + } + if (value.length > 200 || value.search(this.newLineRegex) > -1) { + if (cipher.notes == null) { + cipher.notes = ''; + } + cipher.notes += (key + ': ' + value + '\n'); + } else { + if (cipher.fields == null) { + cipher.fields = []; + } + const field = new FieldView(); + field.type = FieldType.Text; + field.name = key; + field.value = value; + cipher.fields.push(field); + } + } } diff --git a/src/importers/blurCsvImporter.ts b/src/importers/blurCsvImporter.ts index 33620181b5..9fd484a51f 100644 --- a/src/importers/blurCsvImporter.ts +++ b/src/importers/blurCsvImporter.ts @@ -3,11 +3,6 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; -import { CipherView } from '../models/view/cipherView'; -import { LoginView } from '../models/view/loginView'; - -import { CipherType } from '../enums/cipherType'; - export class BlurCsvImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { const result = new ImportResult(); @@ -18,14 +13,12 @@ export class BlurCsvImporter extends BaseImporter implements Importer { } results.forEach((value) => { - const cipher = new CipherView(); - cipher.type = CipherType.Login; if (value.label === 'null') { value.label = null; } + const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(value.label, this.getValueOrDefault(this.nameFromUrl(value.domain), '--')); - cipher.login = new LoginView(); cipher.login.uris = this.makeUriArray(value.domain); cipher.login.password = this.getValueOrDefault(value.password); @@ -36,6 +29,7 @@ export class BlurCsvImporter extends BaseImporter implements Importer { cipher.notes = this.getValueOrDefault(value.username); } + this.cleanupCipher(cipher); result.ciphers.push(cipher); }); diff --git a/src/importers/keepassxCsvImporter.ts b/src/importers/keepassxCsvImporter.ts index 141e6e1771..c679a45cec 100644 --- a/src/importers/keepassxCsvImporter.ts +++ b/src/importers/keepassxCsvImporter.ts @@ -3,11 +3,7 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; -import { CipherView } from '../models/view/cipherView'; import { FolderView } from '../models/view/folderView'; -import { LoginView } from '../models/view/loginView'; - -import { CipherType } from '../enums/cipherType'; export class KeePassXCsvImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { @@ -50,15 +46,13 @@ export class KeePassXCsvImporter extends BaseImporter implements Importer { result.folderRelationships.push([result.ciphers.length, folderIndex]); } - const cipher = new CipherView(); - cipher.type = CipherType.Login; - cipher.favorite = false; + const cipher = this.initLoginCipher(); cipher.notes = this.getValueOrDefault(value.Notes); cipher.name = this.getValueOrDefault(value.Title, '--'); - cipher.login = new LoginView(); cipher.login.username = this.getValueOrDefault(value.Username); cipher.login.password = this.getValueOrDefault(value.Password); cipher.login.uris = this.makeUriArray(value.URL); + this.cleanupCipher(cipher); result.ciphers.push(cipher); }); diff --git a/src/importers/padlockCsvImporter.ts b/src/importers/padlockCsvImporter.ts index dea2fdc99e..7e0cd4f153 100644 --- a/src/importers/padlockCsvImporter.ts +++ b/src/importers/padlockCsvImporter.ts @@ -3,14 +3,8 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; -import { CipherView } from '../models/view/cipherView'; import { CollectionView } from '../models/view/collectionView'; -import { FieldView } from '../models/view/fieldView'; import { FolderView } from '../models/view/folderView'; -import { LoginView } from '../models/view/loginView'; - -import { CipherType } from '../enums/cipherType'; -import { FieldType } from '../enums/fieldType'; export class PadlockCsvImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { @@ -84,13 +78,8 @@ export class PadlockCsvImporter extends BaseImporter implements Importer { } } - const cipher = new CipherView(); - cipher.type = CipherType.Login; - cipher.favorite = false; - cipher.notes = ''; - cipher.fields = []; + const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(value[0], '--'); - cipher.login = new LoginView(); for (let i = 2; i < value.length; i++) { const header = headers[i].trim().toLowerCase(); @@ -105,27 +94,11 @@ export class PadlockCsvImporter extends BaseImporter implements Importer { } else if (this.uriFieldNames.indexOf(header) > -1) { cipher.login.uris = this.makeUriArray(value[i]); } else { - if (value[i].length > 200 || value[i].search(this.newLineRegex) > -1) { - cipher.notes += (headers[i] + ': ' + value[i] + '\n'); - } else { - const field = new FieldView(); - field.type = FieldType.Text; - field.name = headers[i]; - field.value = value[i]; - cipher.fields.push(field); - } + this.processKvp(cipher, headers[i], value[i]); } } - if (this.isNullOrWhitespace(cipher.notes)) { - cipher.notes = null; - } else { - cipher.notes = cipher.notes.trim(); - } - if (cipher.fields.length === 0) { - cipher.fields = null; - } - + this.cleanupCipher(cipher); result.ciphers.push(cipher); }); diff --git a/src/importers/safeInCloudXmlImporter.ts b/src/importers/safeInCloudXmlImporter.ts index 1edaf8139f..4029a2c979 100644 --- a/src/importers/safeInCloudXmlImporter.ts +++ b/src/importers/safeInCloudXmlImporter.ts @@ -3,14 +3,10 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; -import { CipherView } from '../models/view/cipherView'; -import { FieldView } from '../models/view/fieldView'; import { FolderView } from '../models/view/folderView'; -import { LoginView } from '../models/view/loginView'; import { SecureNoteView } from '../models/view/secureNoteView'; import { CipherType } from '../enums/cipherType'; -import { FieldType } from '../enums/fieldType'; import { SecureNoteType } from '../enums/secureNoteType'; export class SafeInCloudXmlImporter extends BaseImporter implements Importer { @@ -55,11 +51,8 @@ export class SafeInCloudXmlImporter extends BaseImporter implements Importer { } } - const cipher = new CipherView(); - cipher.favorite = false; - cipher.notes = ''; + const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(cardEl.getAttribute('title'), '--'); - cipher.fields = null; const cardType = cardEl.getAttribute('type'); if (cardType === 'note') { @@ -67,8 +60,6 @@ export class SafeInCloudXmlImporter extends BaseImporter implements Importer { cipher.secureNote = new SecureNoteView(); cipher.secureNote.type = SecureNoteType.Generic; } else { - cipher.type = CipherType.Login; - cipher.login = new LoginView(); Array.from(this.querySelectorAllDirectChild(cardEl, 'field')).forEach((fieldEl) => { const text = fieldEl.textContent; if (this.isNullOrWhitespace(text)) { @@ -84,17 +75,8 @@ export class SafeInCloudXmlImporter extends BaseImporter implements Importer { cipher.notes += (text + '\n'); } else if (fieldType === 'weblogin' || fieldType === 'website') { cipher.login.uris = this.makeUriArray(text); - } else if (text.length > 200) { - cipher.notes += (name + ': ' + text + '\n'); } else { - if (cipher.fields == null) { - cipher.fields = []; - } - const field = new FieldView(); - field.name = name; - field.value = text; - field.type = FieldType.Text; - cipher.fields.push(field); + this.processKvp(cipher, name, text); } }); } @@ -103,11 +85,7 @@ export class SafeInCloudXmlImporter extends BaseImporter implements Importer { cipher.notes += (notesEl.textContent + '\n'); }); - cipher.notes = cipher.notes.trim(); - if (cipher.notes === '') { - cipher.notes = null; - } - + this.cleanupCipher(cipher); result.ciphers.push(cipher); }); From 74ddd9b8e30cb871573cfe571550fc26fbdc5024 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 10 Jul 2018 17:51:55 -0400 Subject: [PATCH 0380/1626] keepass2 xml importer --- src/importers/keepass2XmlImporter.ts | 86 ++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/importers/keepass2XmlImporter.ts diff --git a/src/importers/keepass2XmlImporter.ts b/src/importers/keepass2XmlImporter.ts new file mode 100644 index 0000000000..46d566713e --- /dev/null +++ b/src/importers/keepass2XmlImporter.ts @@ -0,0 +1,86 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { FolderView } from '../models/view/folderView'; + +export class KeePass2XmlImporter extends BaseImporter implements Importer { + result = new ImportResult(); + + parse(data: string): ImportResult { + const doc = this.parseXml(data); + if (doc == null) { + this.result.success = false; + return this.result; + } + + const rootGroup = doc.querySelector('KeePassFile > Root > Group'); + if (rootGroup == null) { + this.result.errorMessage = 'Missing `KeePassFile > Root > Group` node.'; + this.result.success = false; + return this.result; + } + + this.traverse(rootGroup, true, ''); + + this.result.success = true; + return this.result; + } + + traverse(node: Element, isRootNode: boolean, groupPrefixName: string) { + const folderIndex = this.result.folders.length; + let groupName = groupPrefixName; + + if (!isRootNode) { + if (groupName !== '') { + groupName += ' > '; + } + const nameEl = this.querySelectorDirectChild(node, 'Name'); + groupName += nameEl == null ? '-' : nameEl.textContent; + const folder = new FolderView(); + folder.name = groupName; + this.result.folders.push(folder); + } + + Array.from(this.querySelectorAllDirectChild(node, 'Entry')).forEach((entry) => { + const cipherIndex = this.result.ciphers.length; + + const cipher = this.initLoginCipher(); + Array.from(this.querySelectorAllDirectChild(entry, 'String')).forEach((entryString) => { + const valueEl = this.querySelectorDirectChild(entryString, 'Value'); + const value = valueEl != null ? valueEl.textContent : null; + if (this.isNullOrWhitespace(value)) { + return; + } + const keyEl = this.querySelectorDirectChild(entryString, 'Key'); + const key = keyEl != null ? keyEl.textContent : null; + + if (key === 'URL') { + cipher.login.uris = this.makeUriArray(value); + } else if (key === 'UserName') { + cipher.login.username = value; + } else if (key === 'Password') { + cipher.login.password = value; + } else if (key === 'Title') { + cipher.name = value; + } else if (key === 'Notes') { + cipher.notes += (value + '\n'); + } else { + this.processKvp(cipher, key, value); + } + }); + + this.cleanupCipher(cipher); + this.result.ciphers.push(cipher); + + if (!isRootNode) { + this.result.folderRelationships.push([cipherIndex, folderIndex]); + } + }); + + Array.from(this.querySelectorAllDirectChild(node, 'Group')).forEach((group) => { + this.traverse(group, false, groupName); + }); + } +} From 678b191a32b053e38cc46e7c1b10aaa171d13201 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 10 Jul 2018 17:57:59 -0400 Subject: [PATCH 0381/1626] org support for keepass --- src/importers/keepass2XmlImporter.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/importers/keepass2XmlImporter.ts b/src/importers/keepass2XmlImporter.ts index 46d566713e..128285fed8 100644 --- a/src/importers/keepass2XmlImporter.ts +++ b/src/importers/keepass2XmlImporter.ts @@ -24,6 +24,10 @@ export class KeePass2XmlImporter extends BaseImporter implements Importer { this.traverse(rootGroup, true, ''); + if (this.organization) { + this.moveFoldersToCollections(this.result); + } + this.result.success = true; return this.result; } From 089622d5b19736efc008676bc5f892f7756f60fe Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 10 Jul 2018 22:39:29 -0400 Subject: [PATCH 0382/1626] dont need to turn arrays into arrays, derp.. --- src/importers/keepass2XmlImporter.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/importers/keepass2XmlImporter.ts b/src/importers/keepass2XmlImporter.ts index 128285fed8..21c24f783b 100644 --- a/src/importers/keepass2XmlImporter.ts +++ b/src/importers/keepass2XmlImporter.ts @@ -47,11 +47,11 @@ export class KeePass2XmlImporter extends BaseImporter implements Importer { this.result.folders.push(folder); } - Array.from(this.querySelectorAllDirectChild(node, 'Entry')).forEach((entry) => { + this.querySelectorAllDirectChild(node, 'Entry').forEach((entry) => { const cipherIndex = this.result.ciphers.length; const cipher = this.initLoginCipher(); - Array.from(this.querySelectorAllDirectChild(entry, 'String')).forEach((entryString) => { + this.querySelectorAllDirectChild(entry, 'String').forEach((entryString) => { const valueEl = this.querySelectorDirectChild(entryString, 'Value'); const value = valueEl != null ? valueEl.textContent : null; if (this.isNullOrWhitespace(value)) { @@ -83,7 +83,7 @@ export class KeePass2XmlImporter extends BaseImporter implements Importer { } }); - Array.from(this.querySelectorAllDirectChild(node, 'Group')).forEach((group) => { + this.querySelectorAllDirectChild(node, 'Group').forEach((group) => { this.traverse(group, false, groupName); }); } From 4ba55c8c8bf12385502c1db4c9ba28c33792e433 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 10 Jul 2018 23:17:55 -0400 Subject: [PATCH 0383/1626] add several simple csv importers --- src/importers/chromeCsvImporter.ts | 28 +++++++++++++++++++++++++ src/importers/firefoxCsvImporter.ts | 28 +++++++++++++++++++++++++ src/importers/meldiumCsvImporter.ts | 29 ++++++++++++++++++++++++++ src/importers/saferpassCsvImport.ts | 29 ++++++++++++++++++++++++++ src/importers/upmCsvImporter.ts | 32 +++++++++++++++++++++++++++++ 5 files changed, 146 insertions(+) create mode 100644 src/importers/chromeCsvImporter.ts create mode 100644 src/importers/firefoxCsvImporter.ts create mode 100644 src/importers/meldiumCsvImporter.ts create mode 100644 src/importers/saferpassCsvImport.ts create mode 100644 src/importers/upmCsvImporter.ts diff --git a/src/importers/chromeCsvImporter.ts b/src/importers/chromeCsvImporter.ts new file mode 100644 index 0000000000..5ca97dd20a --- /dev/null +++ b/src/importers/chromeCsvImporter.ts @@ -0,0 +1,28 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +export class ChromeCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(value.name, '--'); + cipher.login.username = this.getValueOrDefault(value.username); + cipher.login.password = this.getValueOrDefault(value.password); + cipher.login.uris = this.makeUriArray(value.url); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} diff --git a/src/importers/firefoxCsvImporter.ts b/src/importers/firefoxCsvImporter.ts new file mode 100644 index 0000000000..8c5b1c1313 --- /dev/null +++ b/src/importers/firefoxCsvImporter.ts @@ -0,0 +1,28 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +export class FirefoxCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(this.nameFromUrl(value.hostname), '--'); + cipher.login.username = this.getValueOrDefault(value.username); + cipher.login.password = this.getValueOrDefault(value.password); + cipher.login.uris = this.makeUriArray(value.hostname); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} diff --git a/src/importers/meldiumCsvImporter.ts b/src/importers/meldiumCsvImporter.ts new file mode 100644 index 0000000000..f73de0124a --- /dev/null +++ b/src/importers/meldiumCsvImporter.ts @@ -0,0 +1,29 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +export class MeldiumCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(value.DisplayName, '--'); + cipher.notes = this.getValueOrDefault(value.Notes); + cipher.login.username = this.getValueOrDefault(value.UserName); + cipher.login.password = this.getValueOrDefault(value.Password); + cipher.login.uris = this.makeUriArray(value.Url); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} diff --git a/src/importers/saferpassCsvImport.ts b/src/importers/saferpassCsvImport.ts new file mode 100644 index 0000000000..e36c3da498 --- /dev/null +++ b/src/importers/saferpassCsvImport.ts @@ -0,0 +1,29 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +export class SaferPassCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(this.nameFromUrl(value.url), '--'); + cipher.notes = this.getValueOrDefault(value.notes); + cipher.login.username = this.getValueOrDefault(value.username); + cipher.login.password = this.getValueOrDefault(value.password); + cipher.login.uris = this.makeUriArray(value.url); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} diff --git a/src/importers/upmCsvImporter.ts b/src/importers/upmCsvImporter.ts new file mode 100644 index 0000000000..8b5002cad7 --- /dev/null +++ b/src/importers/upmCsvImporter.ts @@ -0,0 +1,32 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +export class UpmCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, false); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + if (value.length !== 5) { + return; + } + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(value[0], '--'); + cipher.notes = this.getValueOrDefault(value[4]); + cipher.login.username = this.getValueOrDefault(value[1]); + cipher.login.password = this.getValueOrDefault(value[2]); + cipher.login.uris = this.makeUriArray(value[3]); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} From 06e412a095cb4eab41652a4f70ed2788ccec3f2b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 11 Jul 2018 10:18:25 -0400 Subject: [PATCH 0384/1626] one password importers --- src/importers/baseImporter.ts | 2 +- src/importers/onepassword1PifImporter.ts | 111 +++++++++++++++++++++ src/importers/onepasswordWinCsvImporter.ts | 107 ++++++++++++++++++++ 3 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 src/importers/onepassword1PifImporter.ts create mode 100644 src/importers/onepasswordWinCsvImporter.ts diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index cdef5ef338..21778c240b 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -275,7 +275,7 @@ export abstract class BaseImporter { if (cipher.notes == null) { cipher.notes = ''; } - cipher.notes += (key + ': ' + value + '\n'); + cipher.notes += (key + ': ' + value.split(this.newLineRegex).join('\n') + '\n'); } else { if (cipher.fields == null) { cipher.fields = []; diff --git a/src/importers/onepassword1PifImporter.ts b/src/importers/onepassword1PifImporter.ts new file mode 100644 index 0000000000..fadf535de1 --- /dev/null +++ b/src/importers/onepassword1PifImporter.ts @@ -0,0 +1,111 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CardView } from '../models/view/cardView'; +import { CipherView } from '../models/view/cipherView'; +import { SecureNoteView } from '../models/view/secureNoteView'; + +import { CipherType } from '../enums/cipherType'; +import { SecureNoteType } from '../enums/secureNoteType'; + +export class OnePassword1PifImporter extends BaseImporter implements Importer { + result = new ImportResult(); + + parse(data: string): ImportResult { + data.split(this.newLineRegex).forEach((line) => { + if (this.isNullOrWhitespace(line) || line[0] !== '{') { + return; + } + const item = JSON.parse(line); + const cipher = this.initLoginCipher(); + cipher.favorite = item.openContents && item.openContents.faveIndex ? true : false; + cipher.name = this.getValueOrDefault(item.title, '--'); + + if (item.typeName === 'securenotes.SecureNote') { + cipher.type = CipherType.SecureNote; + cipher.secureNote = new SecureNoteView(); + cipher.secureNote.type = SecureNoteType.Generic; + } else if (item.typeName === 'wallet.financial.CreditCard') { + cipher.type = CipherType.Card; + cipher.card = new CardView(); + } else { + cipher.login.uris = this.makeUriArray(item.location); + } + + if (item.secureContents != null) { + if (!this.isNullOrWhitespace(item.secureContents.notesPlain)) { + cipher.notes = item.secureContents.notesPlain.split(this.newLineRegex).join('\n') + '\n'; + } + if (item.secureContents.fields != null) { + this.parseFields(item.secureContents.fields, cipher, 'designation', 'value', 'name'); + } + if (item.secureContents.sections != null) { + item.secureContents.sections.forEach((section) => { + if (section.fields != null) { + this.parseFields(section.fields, cipher, 'n', 'v', 't'); + } + }); + } + } + + this.cleanupCipher(cipher); + this.result.ciphers.push(cipher); + }); + + this.result.success = true; + return this.result; + } + + private parseFields(fields: any[], cipher: CipherView, designationKey: string, valueKey: string, nameKey: string) { + fields.forEach((field: any) => { + if (field[valueKey] == null || field[valueKey].toString().trim() === '') { + return; + } + + const fieldValue = field[valueKey].toString(); + const fieldDesignation = field[designationKey] != null ? field[designationKey].toString() : null; + + if (cipher.type === CipherType.Login) { + if (this.isNullOrWhitespace(cipher.login.username) && fieldDesignation === 'username') { + cipher.login.username = fieldValue; + return; + } else if (this.isNullOrWhitespace(cipher.login.password) && fieldDesignation === 'password') { + cipher.login.password = fieldValue; + return; + } else if (this.isNullOrWhitespace(cipher.login.totp) && fieldDesignation != null && + fieldDesignation.startsWith('TOTP_')) { + cipher.login.totp = fieldValue; + return; + } + } else if (cipher.type === CipherType.Card) { + if (this.isNullOrWhitespace(cipher.card.number) && fieldDesignation === 'ccnum') { + cipher.card.number = fieldValue; + cipher.card.brand = this.getCardBrand(fieldValue); + return; + } else if (this.isNullOrWhitespace(cipher.card.code) && fieldDesignation === 'cvv') { + cipher.card.code = fieldValue; + return; + } else if (this.isNullOrWhitespace(cipher.card.cardholderName) && fieldDesignation === 'cardholder') { + cipher.card.cardholderName = fieldValue; + return; + } else if (this.isNullOrWhitespace(cipher.card.expiration) && fieldDesignation === 'expiry' && + fieldValue.length === 6) { + cipher.card.expMonth = (fieldValue as string).substr(4, 2); + if (cipher.card.expMonth[0] === '0') { + cipher.card.expMonth = cipher.card.expMonth.substr(1, 1); + } + cipher.card.expYear = (fieldValue as string).substr(0, 4); + return; + } else if (fieldDesignation === 'type') { + // Skip since brand was determined from number above + return; + } + } + + const fieldName = this.isNullOrWhitespace(field[nameKey]) ? 'no_name' : field[nameKey]; + this.processKvp(cipher, fieldName, fieldValue); + }); + } +} diff --git a/src/importers/onepasswordWinCsvImporter.ts b/src/importers/onepasswordWinCsvImporter.ts new file mode 100644 index 0000000000..eaed7dd83a --- /dev/null +++ b/src/importers/onepasswordWinCsvImporter.ts @@ -0,0 +1,107 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { SecureNoteView } from '../models/view/secureNoteView'; + +import { CipherType } from '../enums/cipherType'; +import { SecureNoteType } from '../enums/secureNoteType'; +import { CardView } from '../models/view'; + +const IgnoredProperties = ['ainfo', 'autosubmit', 'notesPlain', 'ps', 'scope', 'tags', 'title', 'uuid']; + +export class OnePasswordWinCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + if (this.isNullOrWhitespace(value.title)) { + return; + } + + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(value.title, '--'); + cipher.notes = this.getValueOrDefault(value.notesPlain, '') + '\n'; + + if (!this.isNullOrWhitespace(value.number) && !this.isNullOrWhitespace(value['expiry date'])) { + cipher.type = CipherType.Card; + cipher.card = new CardView(); + } + + let altUsername: string = null; + for (const property in value) { + if (!value.hasOwnProperty(property) || this.isNullOrWhitespace(value[property])) { + continue; + } + + if (cipher.type === CipherType.Login) { + if (this.isNullOrWhitespace(cipher.login.password) && property === 'password') { + cipher.login.password = value[property]; + continue; + } else if (this.isNullOrWhitespace(cipher.login.username) && property === 'username') { + cipher.login.username = value[property]; + continue; + } else if ((cipher.login.uris == null || cipher.login.uri.length === 0) && property === 'urls') { + const urls = value[property].split(this.newLineRegex); + cipher.login.uris = this.makeUriArray(urls); + continue; + } + } else if (cipher.type === CipherType.Card) { + if (this.isNullOrWhitespace(cipher.card.number) && property === 'number') { + cipher.card.number = value[property]; + cipher.card.brand = this.getCardBrand(value.number); + continue; + } else if (this.isNullOrWhitespace(cipher.card.code) && property === 'verification number') { + cipher.card.code = value[property]; + continue; + } else if (this.isNullOrWhitespace(cipher.card.cardholderName) && property === 'cardholder name') { + cipher.card.cardholderName = value[property]; + continue; + } else if (this.isNullOrWhitespace(cipher.card.expiration) && property === 'expiry date' && + value[property].length === 6) { + cipher.card.expMonth = (value[property] as string).substr(4, 2); + if (cipher.card.expMonth[0] === '0') { + cipher.card.expMonth = cipher.card.expMonth.substr(1, 1); + } + cipher.card.expYear = (value[property] as string).substr(0, 4); + continue; + } else if (property === 'type') { + // Skip since brand was determined from number above + continue; + } + } + + if (IgnoredProperties.indexOf(property) === -1 && !property.startsWith('section:')) { + if (altUsername == null && property === 'email') { + altUsername = value[property]; + } + this.processKvp(cipher, property, value[property]); + } + } + + if (cipher.type === CipherType.Login && !this.isNullOrWhitespace(altUsername) && + this.isNullOrWhitespace(cipher.login.username) && altUsername.indexOf('://') === -1) { + cipher.login.username = altUsername; + } + if (cipher.type === CipherType.Login && this.isNullOrWhitespace(cipher.login.username) && + this.isNullOrWhitespace(cipher.login.password) && + (cipher.login.uris == null || cipher.login.uris.length === 0)) { + cipher.type = CipherType.SecureNote; + cipher.secureNote = new SecureNoteView(); + cipher.secureNote.type = SecureNoteType.Generic; + } + + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} From e9518e104e4d1bdfb24ea82071d28ef0b28061a7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 11 Jul 2018 10:26:32 -0400 Subject: [PATCH 0385/1626] fix implicit any --- src/importers/onepassword1PifImporter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/importers/onepassword1PifImporter.ts b/src/importers/onepassword1PifImporter.ts index fadf535de1..2978a8c8ce 100644 --- a/src/importers/onepassword1PifImporter.ts +++ b/src/importers/onepassword1PifImporter.ts @@ -42,7 +42,7 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { this.parseFields(item.secureContents.fields, cipher, 'designation', 'value', 'name'); } if (item.secureContents.sections != null) { - item.secureContents.sections.forEach((section) => { + item.secureContents.sections.forEach((section: any) => { if (section.fields != null) { this.parseFields(section.fields, cipher, 'n', 'v', 't'); } From d7f3f9425ececd041bc1a3e0be3f9da157ac55c3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 11 Jul 2018 13:30:06 -0400 Subject: [PATCH 0386/1626] user public key apis --- src/abstractions/api.service.ts | 3 ++ src/abstractions/crypto.service.ts | 1 + src/models/response/userKeyResponse.ts | 9 ++++++ src/services/api.service.ts | 8 ++++++ src/services/crypto.service.ts | 38 +++++++++++++------------- 5 files changed, 40 insertions(+), 19 deletions(-) create mode 100644 src/models/response/userKeyResponse.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 07cc8118d0..befcbe8aae 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -71,6 +71,7 @@ import { TwoFactorProviderResponse } from '../models/response/twoFactorProviderR import { TwoFactorRecoverResponse } from '../models/response/twoFactorRescoverResponse'; import { TwoFactorU2fResponse } from '../models/response/twoFactorU2fResponse'; import { TwoFactorYubiKeyResponse } from '../models/response/twoFactorYubiKeyResponse'; +import { UserKeyResponse } from '../models/response/userKeyResponse'; export abstract class ApiService { urlsSet: boolean; @@ -195,5 +196,7 @@ export abstract class ApiService { getEventsOrganizationUser: (organizationId: string, id: string, start: string, end: string, token: string) => Promise>; + getUserPublicKey: (id: string) => Promise; + fetch: (request: Request) => Promise; } diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 9c153efca9..7c19a3487f 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -31,6 +31,7 @@ export abstract class CryptoService { makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise; encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise; + rsaEncrypt: (data: ArrayBuffer, publicKey?: ArrayBuffer, key?: SymmetricCryptoKey) => Promise; decryptToUtf8: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise; decryptFromBytes: (encBuf: ArrayBuffer, key: SymmetricCryptoKey) => Promise; randomNumber: (min: number, max: number) => Promise; diff --git a/src/models/response/userKeyResponse.ts b/src/models/response/userKeyResponse.ts new file mode 100644 index 0000000000..45fbe22b52 --- /dev/null +++ b/src/models/response/userKeyResponse.ts @@ -0,0 +1,9 @@ +export class UserKeyResponse { + userId: string; + publicKey: string; + + constructor(response: any) { + this.userId = response.UserId; + this.publicKey = response.PublicKey; + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 4a72e469ad..cfdfe68b37 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -78,6 +78,7 @@ import { TwoFactorProviderResponse } from '../models/response/twoFactorProviderR import { TwoFactorRecoverResponse } from '../models/response/twoFactorRescoverResponse'; import { TwoFactorU2fResponse } from '../models/response/twoFactorU2fResponse'; import { TwoFactorYubiKeyResponse } from '../models/response/twoFactorYubiKeyResponse'; +import { UserKeyResponse } from '../models/response/userKeyResponse'; export class ApiService implements ApiServiceAbstraction { urlsSet: boolean = false; @@ -649,6 +650,13 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, EventResponse); } + // User APIs + + async getUserPublicKey(id: string): Promise { + const r = await this.send('GET', '/users/' + id + '/public-key', null, true, true); + return new UserKeyResponse(r); + } + // Helpers fetch(request: Request): Promise { diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 43aa0e0fd2..9d362329f9 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -356,6 +356,25 @@ export class CryptoService implements CryptoServiceAbstraction { return encBytes.buffer; } + async rsaEncrypt(data: ArrayBuffer, publicKey?: ArrayBuffer, key?: SymmetricCryptoKey): Promise { + if (publicKey == null) { + publicKey = await this.getPublicKey(); + } + if (publicKey == null) { + throw new Error('Public key unavailable.'); + } + + let type = EncryptionType.Rsa2048_OaepSha1_B64; + const encBytes = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, 'sha1'); + let mac: string = null; + if (key != null && key.macKey != null) { + type = EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64; + const macBytes = await this.cryptoFunctionService.hmac(encBytes, key.macKey, 'sha256'); + mac = Utils.fromBufferToB64(macBytes); + } + return new CipherString(type, Utils.fromBufferToB64(encBytes), null, mac); + } + async decrypt(cipherString: CipherString, key?: SymmetricCryptoKey): Promise { const iv = Utils.fromB64ToArray(cipherString.iv).buffer; const data = Utils.fromB64ToArray(cipherString.data).buffer; @@ -530,25 +549,6 @@ export class CryptoService implements CryptoServiceAbstraction { return await this.cryptoFunctionService.aesDecrypt(data, iv, theKey.encKey); } - private async rsaEncrypt(data: ArrayBuffer, publicKey?: ArrayBuffer, key?: SymmetricCryptoKey) { - if (publicKey == null) { - publicKey = await this.getPublicKey(); - } - if (publicKey == null) { - throw new Error('Public key unavailable.'); - } - - let type = EncryptionType.Rsa2048_OaepSha1_B64; - const encBytes = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, 'sha1'); - let mac: string = null; - if (key != null && key.macKey != null) { - type = EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64; - const macBytes = await this.cryptoFunctionService.hmac(encBytes, key.macKey, 'sha256'); - mac = Utils.fromBufferToB64(macBytes); - } - return new CipherString(type, Utils.fromBufferToB64(encBytes), null, mac); - } - private async rsaDecrypt(encValue: string): Promise { const headerPieces = encValue.split('.'); let encType: EncryptionType = null; From ef75dc735e343f407f43a010ba97f8d3fb7fa583 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 11 Jul 2018 15:47:54 -0400 Subject: [PATCH 0387/1626] search cipher id as well --- src/angular/pipes/search-ciphers.pipe.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/angular/pipes/search-ciphers.pipe.ts b/src/angular/pipes/search-ciphers.pipe.ts index 848f8519e7..ba9c454928 100644 --- a/src/angular/pipes/search-ciphers.pipe.ts +++ b/src/angular/pipes/search-ciphers.pipe.ts @@ -7,14 +7,16 @@ import { CipherView } from '../../models/view/cipherView'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { DeviceType } from '../../enums'; + @Pipe({ name: 'searchCiphers', }) export class SearchCiphersPipe implements PipeTransform { private onlySearchName = false; - constructor(private platformUtilsService: PlatformUtilsService) { - this.onlySearchName = platformUtilsService.isEdge(); + constructor(platformUtilsService: PlatformUtilsService) { + this.onlySearchName = platformUtilsService.getDevice() === DeviceType.EdgeExtension; } transform(ciphers: CipherView[], searchText: string): CipherView[] { @@ -34,6 +36,9 @@ export class SearchCiphersPipe implements PipeTransform { if (this.onlySearchName) { return false; } + if (c.id.substr(0, 8) === searchText) { + return true; + } if (c.subTitle != null && c.subTitle.toLowerCase().indexOf(searchText) > -1) { return true; } From 0c9fd975f76c8edbdee6b7a76b3ae8bb8ec1b5b3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 11 Jul 2018 17:43:26 -0400 Subject: [PATCH 0388/1626] kepper csv importer --- src/importers/baseImporter.ts | 2 +- src/importers/keeperCsvImporter.ts | 70 ++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/importers/keeperCsvImporter.ts diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index 21778c240b..77dd6d3c0d 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -271,7 +271,7 @@ export abstract class BaseImporter { if (this.isNullOrWhitespace(key)) { key = ''; } - if (value.length > 200 || value.search(this.newLineRegex) > -1) { + if (value.length > 200 || value.trim().search(this.newLineRegex) > -1) { if (cipher.notes == null) { cipher.notes = ''; } diff --git a/src/importers/keeperCsvImporter.ts b/src/importers/keeperCsvImporter.ts new file mode 100644 index 0000000000..6e4cee1737 --- /dev/null +++ b/src/importers/keeperCsvImporter.ts @@ -0,0 +1,70 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { FolderView } from '../models/view/folderView'; + +export class KeeperCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, false); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + if (value.length < 6) { + return; + } + + let folderIndex = result.folders.length; + const hasFolder = !this.isNullOrWhitespace(value[0]); + let addFolder = hasFolder; + + if (hasFolder) { + for (let i = 0; i < result.folders.length; i++) { + if (result.folders[i].name === value[0]) { + addFolder = false; + folderIndex = i; + break; + } + } + } + + if (addFolder) { + const f = new FolderView(); + f.name = value[0]; + result.folders.push(f); + } + if (hasFolder) { + result.folderRelationships.push([result.ciphers.length, folderIndex]); + } + + const cipher = this.initLoginCipher(); + cipher.notes = this.getValueOrDefault(value[5]) + '\n'; + cipher.name = this.getValueOrDefault(value[1], '--'); + cipher.login.username = this.getValueOrDefault(value[2]); + cipher.login.password = this.getValueOrDefault(value[3]); + cipher.login.uris = this.makeUriArray(value[4]); + + if (value.length > 7) { + // we have some custom fields. + for (let i = 7; i < value.length; i = i + 2) { + this.processKvp(cipher, value[i], value[i + 1]); + } + } + + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + if (this.organization) { + this.moveFoldersToCollections(result); + } + + result.success = true; + return result; + } +} From 17d50fc90cc21ba547b6f083180ae89d2932de19 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 11 Jul 2018 23:30:15 -0400 Subject: [PATCH 0389/1626] password dragon xml importer --- src/importers/passwordDragonXmlImporter.ts | 80 ++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/importers/passwordDragonXmlImporter.ts diff --git a/src/importers/passwordDragonXmlImporter.ts b/src/importers/passwordDragonXmlImporter.ts new file mode 100644 index 0000000000..9528b7ede9 --- /dev/null +++ b/src/importers/passwordDragonXmlImporter.ts @@ -0,0 +1,80 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { FolderView } from '../models/view/folderView'; + +export class PasswordDragonXmlImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const doc = this.parseXml(data); + if (doc == null) { + result.success = false; + return result; + } + + const records = doc.querySelectorAll('PasswordManager > record'); + Array.from(records).forEach((record) => { + const category = this.querySelectorDirectChild(record, 'Category'); + + let folderIndex = result.folders.length; + const hasFolder = category != null && !this.isNullOrWhitespace(category.textContent) && + category.textContent !== 'Unfiled'; + let addFolder = hasFolder; + + if (hasFolder) { + for (let i = 0; i < result.folders.length; i++) { + if (result.folders[i].name === category.textContent) { + addFolder = false; + folderIndex = i; + break; + } + } + } + + if (addFolder) { + const f = new FolderView(); + f.name = category.textContent; + result.folders.push(f); + } + if (hasFolder) { + result.folderRelationships.push([result.ciphers.length, folderIndex]); + } + + const accountName = this.querySelectorDirectChild(record, 'Account-Name'); + const userId = this.querySelectorDirectChild(record, 'User-Id'); + const password = this.querySelectorDirectChild(record, 'Password'); + const url = this.querySelectorDirectChild(record, 'URL'); + const notes = this.querySelectorDirectChild(record, 'Notes'); + const cipher = this.initLoginCipher(); + cipher.name = accountName != null ? this.getValueOrDefault(accountName.textContent, '--') : '--'; + cipher.notes = notes != null ? this.getValueOrDefault(notes.textContent) : ''; + cipher.login.username = userId != null ? this.getValueOrDefault(userId.textContent) : null; + cipher.login.password = password != null ? this.getValueOrDefault(password.textContent) : null; + cipher.login.uris = url != null ? this.makeUriArray(url.textContent) : null; + + const attributes: string[] = []; + for (let i = 1; i <= 10; i++) { + attributes.push('Attribute-' + i); + } + + this.querySelectorAllDirectChild(record, attributes.join(',')).forEach((attr) => { + if (this.isNullOrWhitespace(attr.textContent) || attr.textContent === 'null') { + return; + } + this.processKvp(cipher, attr.tagName, attr.textContent); + }); + + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + if (this.organization) { + this.moveFoldersToCollections(result); + } + + result.success = true; + return result; + } +} From b4846e5feaf0d5a6122950e0b432d792a443015f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 12 Jul 2018 00:11:09 -0400 Subject: [PATCH 0390/1626] enpass csv importer --- src/importers/enpassCsvImporter.ts | 117 +++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/importers/enpassCsvImporter.ts diff --git a/src/importers/enpassCsvImporter.ts b/src/importers/enpassCsvImporter.ts new file mode 100644 index 0000000000..9be4c0c883 --- /dev/null +++ b/src/importers/enpassCsvImporter.ts @@ -0,0 +1,117 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CipherType } from '../enums/cipherType'; +import { SecureNoteType } from '../enums/secureNoteType'; + +import { CardView } from '../models/view/cardView'; +import { SecureNoteView } from '../models/view/secureNoteView'; + +export class EnpassCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, false); + if (results == null) { + result.success = false; + return result; + } + + let firstRow = true; + results.forEach((value) => { + if (value.length < 2 || (firstRow && value[0] === 'Title')) { + firstRow = false; + return; + } + + const cipher = this.initLoginCipher(); + cipher.notes = this.getValueOrDefault(value[value.length - 1]); + cipher.name = this.getValueOrDefault(value[0], '--'); + + if (value.length === 2) { + cipher.type = CipherType.SecureNote; + cipher.secureNote = new SecureNoteView(); + cipher.secureNote.type = SecureNoteType.Generic; + } + + if (value.indexOf('Cardholder') > -1 && value.indexOf('Number') > -1 && value.indexOf('Expiry date')) { + cipher.type = CipherType.Card; + cipher.card = new CardView(); + } + + if (value.length > 2 && (value.length % 2) === 0) { + for (let i = 0; i < value.length - 2; i += 2) { + const fieldValue: string = value[i + 2]; + if (this.isNullOrWhitespace(fieldValue)) { + continue; + } + + const fieldName: string = value[i + 1]; + const fieldNameLower = fieldName.toLowerCase(); + + if (cipher.type === CipherType.Login) { + if (fieldNameLower === 'url' && (cipher.login.uris == null || cipher.login.uris.length === 0)) { + cipher.login.uris = this.makeUriArray(fieldValue); + continue; + } else if ((fieldNameLower === 'username' || fieldNameLower === 'email') && + this.isNullOrWhitespace(cipher.login.username)) { + cipher.login.username = fieldValue; + continue; + } else if (fieldNameLower === 'password' && this.isNullOrWhitespace(cipher.login.password)) { + cipher.login.password = fieldValue; + continue; + } else if (fieldNameLower === 'totp' && this.isNullOrWhitespace(cipher.login.totp)) { + cipher.login.totp = fieldValue; + continue; + } + } else if (cipher.type === CipherType.Card) { + if (fieldNameLower === 'cardholder' && this.isNullOrWhitespace(cipher.card.cardholderName)) { + cipher.card.cardholderName = fieldValue; + continue; + } else if (fieldNameLower === 'number' && this.isNullOrWhitespace(cipher.card.number)) { + cipher.card.number = fieldValue; + cipher.card.brand = this.getCardBrand(fieldValue); + continue; + } else if (fieldNameLower === 'cvc' && this.isNullOrWhitespace(cipher.card.code)) { + cipher.card.code = fieldValue; + continue; + } else if (fieldNameLower === 'expiry date' && this.isNullOrWhitespace(cipher.card.expMonth) && + this.isNullOrWhitespace(cipher.card.expYear)) { + const parts = fieldValue.split('/'); + if (parts.length === 2) { + let month: string = null; + let year: string = null; + if (parts[0].length === 1 || parts[0].length === 2) { + month = parts[0]; + if (month.length === 2 && month[0] === '0') { + month = month.substr(1, 1); + } + } + if (parts[1].length === 2 || parts[1].length === 4) { + year = month.length === 2 ? '20' + parts[1] : parts[1]; + } + if (month != null && year != null) { + cipher.card.expMonth = month; + cipher.card.expYear = year; + continue; + } + } + } else if (fieldNameLower === 'type') { + // Skip since brand was determined from number above + continue; + } + } + + this.processKvp(cipher, fieldName, fieldValue); + } + } + + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} From 768b41153df673c0bf77eb2f79f643d4f9e118c2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 12 Jul 2018 09:21:35 -0400 Subject: [PATCH 0391/1626] change to secure note if not login --- src/importers/enpassCsvImporter.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/importers/enpassCsvImporter.ts b/src/importers/enpassCsvImporter.ts index 9be4c0c883..5ce49d6d2e 100644 --- a/src/importers/enpassCsvImporter.ts +++ b/src/importers/enpassCsvImporter.ts @@ -29,13 +29,14 @@ export class EnpassCsvImporter extends BaseImporter implements Importer { cipher.notes = this.getValueOrDefault(value[value.length - 1]); cipher.name = this.getValueOrDefault(value[0], '--'); - if (value.length === 2) { + if (value.length === 2 || (value.indexOf('Username') < 0 && value.indexOf('Password') < 0 && + value.indexOf('Email') && value.indexOf('URL') < 0)) { cipher.type = CipherType.SecureNote; cipher.secureNote = new SecureNoteView(); cipher.secureNote.type = SecureNoteType.Generic; } - if (value.indexOf('Cardholder') > -1 && value.indexOf('Number') > -1 && value.indexOf('Expiry date')) { + if (value.indexOf('Cardholder') > -1 && value.indexOf('Number') > -1 && value.indexOf('Expiry date') > -1) { cipher.type = CipherType.Card; cipher.card = new CardView(); } From 7ca2a40478d8d8d50f47f1d6974b85f22eb888f3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 12 Jul 2018 09:48:39 -0400 Subject: [PATCH 0392/1626] folder helps and pwsafe xml importer --- src/importers/baseImporter.ts | 26 ++++++++++ src/importers/passwordSafeXmlImporter.ts | 62 ++++++++++++++++++++++++ src/models/domain/domain.ts | 2 +- 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/importers/passwordSafeXmlImporter.ts diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index 77dd6d3c0d..535f663753 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -8,6 +8,7 @@ import { LoginUriView } from '../models/view/loginUriView'; import { Utils } from '../misc/utils'; import { FieldView } from '../models/view/fieldView'; +import { FolderView } from '../models/view/folderView'; import { LoginView } from '../models/view/loginView'; import { CipherType } from '../enums/cipherType'; @@ -287,4 +288,29 @@ export abstract class BaseImporter { cipher.fields.push(field); } } + + protected processFolder(result: ImportResult, folderName: string) { + let folderIndex = result.folders.length; + const hasFolder = !this.isNullOrWhitespace(folderName); + let addFolder = hasFolder; + + if (hasFolder) { + for (let i = 0; i < result.folders.length; i++) { + if (result.folders[i].name === folderName) { + addFolder = false; + folderIndex = i; + break; + } + } + } + + if (addFolder) { + const f = new FolderView(); + f.name = folderName; + result.folders.push(f); + } + if (hasFolder) { + result.folderRelationships.push([result.ciphers.length, folderIndex]); + } + } } diff --git a/src/importers/passwordSafeXmlImporter.ts b/src/importers/passwordSafeXmlImporter.ts new file mode 100644 index 0000000000..ff799217b6 --- /dev/null +++ b/src/importers/passwordSafeXmlImporter.ts @@ -0,0 +1,62 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +export class PasswordSafeXmlImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const doc = this.parseXml(data); + if (doc == null) { + result.success = false; + return result; + } + + const passwordSafe = doc.querySelector('passwordsafe'); + if (passwordSafe == null) { + result.errorMessage = 'Missing `passwordsafe` node.'; + result.success = false; + return result; + } + + const notesDelimiter = passwordSafe.getAttribute('delimiter'); + const entries = doc.querySelectorAll('passwordsafe > entry'); + Array.from(entries).forEach((entry) => { + const group = this.querySelectorDirectChild(entry, 'group'); + const groupText = group != null && !this.isNullOrWhitespace(group.textContent) ? + group.textContent.split('.').join(' > ') : null; + this.processFolder(result, groupText); + + const title = this.querySelectorDirectChild(entry, 'title'); + const username = this.querySelectorDirectChild(entry, 'username'); + const email = this.querySelectorDirectChild(entry, 'email'); + const password = this.querySelectorDirectChild(entry, 'password'); + const url = this.querySelectorDirectChild(entry, 'url'); + const notes = this.querySelectorDirectChild(entry, 'notes'); + const cipher = this.initLoginCipher(); + cipher.name = title != null ? this.getValueOrDefault(title.textContent, '--') : '--'; + cipher.notes = notes != null ? + this.getValueOrDefault(notes.textContent, '').split(notesDelimiter).join('\n') : null; + cipher.login.username = username != null ? this.getValueOrDefault(username.textContent) : null; + cipher.login.password = password != null ? this.getValueOrDefault(password.textContent) : null; + cipher.login.uris = url != null ? this.makeUriArray(url.textContent) : null; + + if (this.isNullOrWhitespace(cipher.login.username) && email != null) { + cipher.login.username = this.getValueOrDefault(email.textContent); + } else if (email != null && !this.isNullOrWhitespace(email.textContent)) { + cipher.notes = this.isNullOrWhitespace(cipher.notes) ? 'Email: ' + email.textContent + : (cipher.notes + '\n' + 'Email: ' + email.textContent); + } + + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + if (this.organization) { + this.moveFoldersToCollections(result); + } + + result.success = true; + return result; + } +} diff --git a/src/models/domain/domain.ts b/src/models/domain/domain.ts index 8eed0f5699..f5d27b0340 100644 --- a/src/models/domain/domain.ts +++ b/src/models/domain/domain.ts @@ -1,4 +1,4 @@ -import { CipherString } from '../domain/cipherString'; +import { CipherString } from './cipherString'; import { View } from '../view/view'; From 89779db1f22e001bdc8d32f170d0c6eacae3bae8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 12 Jul 2018 09:57:08 -0400 Subject: [PATCH 0393/1626] convert to processFolder helper --- src/importers/bitwardenCsvImporter.ts | 24 +------------------ src/importers/keepassxCsvImporter.ts | 24 +------------------ src/importers/keeperCsvImporter.ts | 24 +------------------ src/importers/padlockCsvImporter.ts | 25 ++------------------ src/importers/passwordDragonXmlImporter.ts | 27 +++------------------- 5 files changed, 8 insertions(+), 116 deletions(-) diff --git a/src/importers/bitwardenCsvImporter.ts b/src/importers/bitwardenCsvImporter.ts index 76ea7aa08b..c8414f5ac2 100644 --- a/src/importers/bitwardenCsvImporter.ts +++ b/src/importers/bitwardenCsvImporter.ts @@ -47,29 +47,7 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer { result.collectionRelationships.push([result.ciphers.length, collectionIndex]); }); } else if (!this.organization) { - let folderIndex = result.folders.length; - const hasFolder = !this.isNullOrWhitespace(value.folder); - let addFolder = hasFolder; - - if (hasFolder) { - for (let i = 0; i < result.folders.length; i++) { - if (result.folders[i].name === value.folder) { - addFolder = false; - folderIndex = i; - break; - } - } - } - - if (addFolder) { - const f = new FolderView(); - f.name = value.folder; - result.folders.push(f); - } - - if (hasFolder) { - result.folderRelationships.push([result.ciphers.length, folderIndex]); - } + this.processFolder(result, value.folder); } const cipher = new CipherView(); diff --git a/src/importers/keepassxCsvImporter.ts b/src/importers/keepassxCsvImporter.ts index c679a45cec..7d99a33909 100644 --- a/src/importers/keepassxCsvImporter.ts +++ b/src/importers/keepassxCsvImporter.ts @@ -22,29 +22,7 @@ export class KeePassXCsvImporter extends BaseImporter implements Importer { value.Group = !this.isNullOrWhitespace(value.Group) && value.Group.startsWith('Root/') ? value.Group.replace('Root/', '') : value.Group; const groupName = !this.isNullOrWhitespace(value.Group) ? value.Group.split('/').join(' > ') : null; - - let folderIndex = result.folders.length; - const hasFolder = groupName != null; - let addFolder = hasFolder; - - if (hasFolder) { - for (let i = 0; i < result.folders.length; i++) { - if (result.folders[i].name === groupName) { - addFolder = false; - folderIndex = i; - break; - } - } - } - - if (addFolder) { - const f = new FolderView(); - f.name = groupName; - result.folders.push(f); - } - if (hasFolder) { - result.folderRelationships.push([result.ciphers.length, folderIndex]); - } + this.processFolder(result, groupName); const cipher = this.initLoginCipher(); cipher.notes = this.getValueOrDefault(value.Notes); diff --git a/src/importers/keeperCsvImporter.ts b/src/importers/keeperCsvImporter.ts index 6e4cee1737..27a520e52f 100644 --- a/src/importers/keeperCsvImporter.ts +++ b/src/importers/keeperCsvImporter.ts @@ -19,29 +19,7 @@ export class KeeperCsvImporter extends BaseImporter implements Importer { return; } - let folderIndex = result.folders.length; - const hasFolder = !this.isNullOrWhitespace(value[0]); - let addFolder = hasFolder; - - if (hasFolder) { - for (let i = 0; i < result.folders.length; i++) { - if (result.folders[i].name === value[0]) { - addFolder = false; - folderIndex = i; - break; - } - } - } - - if (addFolder) { - const f = new FolderView(); - f.name = value[0]; - result.folders.push(f); - } - if (hasFolder) { - result.folderRelationships.push([result.ciphers.length, folderIndex]); - } - + this.processFolder(result, value[0]); const cipher = this.initLoginCipher(); cipher.notes = this.getValueOrDefault(value[5]) + '\n'; cipher.name = this.getValueOrDefault(value[1], '--'); diff --git a/src/importers/padlockCsvImporter.ts b/src/importers/padlockCsvImporter.ts index 7e0cd4f153..b85e4113bc 100644 --- a/src/importers/padlockCsvImporter.ts +++ b/src/importers/padlockCsvImporter.ts @@ -52,29 +52,8 @@ export class PadlockCsvImporter extends BaseImporter implements Importer { }); } else { const tags = (value[1] as string).split(','); - let folderIndex = result.folders.length; - const hasFolder = tags.length > 0 && !this.isNullOrWhitespace(tags[0].trim()); - let addFolder = hasFolder; - const tag = tags[0].trim(); - - if (hasFolder) { - for (let i = 0; i < result.folders.length; i++) { - if (result.folders[i].name === tag) { - addFolder = false; - folderIndex = i; - break; - } - } - } - - if (addFolder) { - const f = new FolderView(); - f.name = tag; - result.folders.push(f); - } - if (hasFolder) { - result.folderRelationships.push([result.ciphers.length, folderIndex]); - } + const tag = tags.length > 0 ? tags[0].trim() : null; + this.processFolder(result, tag); } } diff --git a/src/importers/passwordDragonXmlImporter.ts b/src/importers/passwordDragonXmlImporter.ts index 9528b7ede9..5b5db151fb 100644 --- a/src/importers/passwordDragonXmlImporter.ts +++ b/src/importers/passwordDragonXmlImporter.ts @@ -17,30 +17,9 @@ export class PasswordDragonXmlImporter extends BaseImporter implements Importer const records = doc.querySelectorAll('PasswordManager > record'); Array.from(records).forEach((record) => { const category = this.querySelectorDirectChild(record, 'Category'); - - let folderIndex = result.folders.length; - const hasFolder = category != null && !this.isNullOrWhitespace(category.textContent) && - category.textContent !== 'Unfiled'; - let addFolder = hasFolder; - - if (hasFolder) { - for (let i = 0; i < result.folders.length; i++) { - if (result.folders[i].name === category.textContent) { - addFolder = false; - folderIndex = i; - break; - } - } - } - - if (addFolder) { - const f = new FolderView(); - f.name = category.textContent; - result.folders.push(f); - } - if (hasFolder) { - result.folderRelationships.push([result.ciphers.length, folderIndex]); - } + const categoryText = category != null && !this.isNullOrWhitespace(category.textContent) && + category.textContent !== 'Unfiled' ? category.textContent : null; + this.processFolder(result, categoryText); const accountName = this.querySelectorDirectChild(record, 'Account-Name'); const userId = this.querySelectorDirectChild(record, 'User-Id'); From 1a2b8684e245fae3c2f7f1a91c0a073cbeb3c5cf Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 12 Jul 2018 10:05:32 -0400 Subject: [PATCH 0394/1626] 30 secon timeout on keypair tests --- spec/node/services/nodeCryptoFunction.service.spec.ts | 2 +- spec/web/services/webCryptoFunction.service.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/node/services/nodeCryptoFunction.service.spec.ts b/spec/node/services/nodeCryptoFunction.service.spec.ts index 828b8decec..5f79750d5f 100644 --- a/spec/node/services/nodeCryptoFunction.service.spec.ts +++ b/spec/node/services/nodeCryptoFunction.service.spec.ts @@ -318,7 +318,7 @@ function testRsaGenerateKeyPair(length: 1024 | 2048 | 4096) { expect(keyPair[0] == null || keyPair[1] == null).toBe(false); const publicKey = await cryptoFunctionService.rsaExtractPublicKey(keyPair[1]); expect(Utils.fromBufferToB64(keyPair[0])).toBe(Utils.fromBufferToB64(publicKey)); - }, 10000); + }, 30000); } function makeStaticByteArray(length: number) { diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index 8ee1fe691e..7be59e73db 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -372,7 +372,7 @@ function testRsaGenerateKeyPair(length: 1024 | 2048 | 4096) { expect(keyPair[0] == null || keyPair[1] == null).toBe(false); const publicKey = await cryptoFunctionService.rsaExtractPublicKey(keyPair[1]); expect(Utils.fromBufferToB64(keyPair[0])).toBe(Utils.fromBufferToB64(publicKey)); - }); + }, 30000); } function getWebCryptoFunctionService() { From 152c44185b6509ad2769f1c1bc306a0e4dd576d5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 12 Jul 2018 10:10:05 -0400 Subject: [PATCH 0395/1626] optimize id search --- src/angular/pipes/search-ciphers.pipe.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/pipes/search-ciphers.pipe.ts b/src/angular/pipes/search-ciphers.pipe.ts index ba9c454928..e81371c4e8 100644 --- a/src/angular/pipes/search-ciphers.pipe.ts +++ b/src/angular/pipes/search-ciphers.pipe.ts @@ -36,7 +36,7 @@ export class SearchCiphersPipe implements PipeTransform { if (this.onlySearchName) { return false; } - if (c.id.substr(0, 8) === searchText) { + if (searchText.length >= 8 && c.id.startsWith(searchText)) { return true; } if (c.subTitle != null && c.subTitle.toLowerCase().indexOf(searchText) > -1) { From 24d608d36526c21ce4bcff8d9dc91e08b201dea8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 12 Jul 2018 11:35:04 -0400 Subject: [PATCH 0396/1626] verify email apis --- src/abstractions/api.service.ts | 3 +++ src/models/request/verifyEmailRequest.ts | 9 +++++++++ src/services/api.service.ts | 9 +++++++++ 3 files changed, 21 insertions(+) create mode 100644 src/models/request/verifyEmailRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index befcbe8aae..aaaae3e4da 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -38,6 +38,7 @@ import { UpdateTwoFactorDuoRequest } from '../models/request/updateTwoFactorDuoR import { UpdateTwoFactorEmailRequest } from '../models/request/updateTwoFactorEmailRequest'; import { UpdateTwoFactorU2fRequest } from '../models/request/updateTwoFactorU2fRequest'; import { UpdateTwoFactorYubioOtpRequest } from '../models/request/updateTwoFactorYubioOtpRequest'; +import { VerifyEmailRequest } from '../models/request/verifyEmailRequest'; import { BillingResponse } from '../models/response/billingResponse'; import { CipherResponse } from '../models/response/cipherResponse'; @@ -100,6 +101,8 @@ export abstract class ApiService { postAccountPayment: (request: PaymentRequest) => Promise; postAccountLicense: (data: FormData) => Promise; postAccountKeys: (request: KeysRequest) => Promise; + postAccountVerifyEmail: () => Promise; + postAccountVerifyEmailToken: (request: VerifyEmailRequest) => Promise; postFolder: (request: FolderRequest) => Promise; putFolder: (id: string, request: FolderRequest) => Promise; diff --git a/src/models/request/verifyEmailRequest.ts b/src/models/request/verifyEmailRequest.ts new file mode 100644 index 0000000000..7609fd06a7 --- /dev/null +++ b/src/models/request/verifyEmailRequest.ts @@ -0,0 +1,9 @@ +export class VerifyEmailRequest { + userId: string; + token: string; + + constructor(userId: string, token: string) { + this.userId = userId; + this.token = token; + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index cfdfe68b37..ba502cb50a 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -44,6 +44,7 @@ import { UpdateTwoFactorDuoRequest } from '../models/request/updateTwoFactorDuoR import { UpdateTwoFactorEmailRequest } from '../models/request/updateTwoFactorEmailRequest'; import { UpdateTwoFactorU2fRequest } from '../models/request/updateTwoFactorU2fRequest'; import { UpdateTwoFactorYubioOtpRequest } from '../models/request/updateTwoFactorYubioOtpRequest'; +import { VerifyEmailRequest } from '../models/request/verifyEmailRequest'; import { BillingResponse } from '../models/response/billingResponse'; import { CipherResponse } from '../models/response/cipherResponse'; @@ -249,6 +250,14 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/accounts/keys', request, true, false); } + postAccountVerifyEmail(): Promise { + return this.send('POST', '/accounts/verify-email', null, true, false); + } + + postAccountVerifyEmailToken(request: VerifyEmailRequest): Promise { + return this.send('POST', '/accounts/verify-email-token', request, false, false); + } + // Folder APIs async postFolder(request: FolderRequest): Promise { From f6a597933473141cc955fbbd13db8f4cbc829818 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 12 Jul 2018 11:52:55 -0400 Subject: [PATCH 0397/1626] rate limit message on 429 --- src/models/response/errorResponse.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/models/response/errorResponse.ts b/src/models/response/errorResponse.ts index 4297427711..74abda15f4 100644 --- a/src/models/response/errorResponse.ts +++ b/src/models/response/errorResponse.ts @@ -14,6 +14,10 @@ export class ErrorResponse { if (errorModel) { this.message = errorModel.Message; this.validationErrors = errorModel.ValidationErrors; + } else { + if (status === 429) { + this.message = 'Rate limit exceeded. Try again later.'; + } } this.statusCode = status; } From 67b2b5318556f2d21bf4f2d117af8228b9f9549c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 12 Jul 2018 14:19:39 -0400 Subject: [PATCH 0398/1626] allow null name --- src/angular/components/login.component.ts | 1 - src/services/token.service.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/angular/components/login.component.ts b/src/angular/components/login.component.ts index 79d5589622..8d8295681c 100644 --- a/src/angular/components/login.component.ts +++ b/src/angular/components/login.component.ts @@ -8,7 +8,6 @@ import { AuthResult } from '../../models/domain/authResult'; import { AuthService } from '../../abstractions/auth.service'; import { I18nService } from '../../abstractions/i18n.service'; -import { SyncService } from '../../abstractions/sync.service'; export class LoginComponent { @Input() email: string = ''; diff --git a/src/services/token.service.ts b/src/services/token.service.ts index 1540d1a0ef..4036d342ed 100644 --- a/src/services/token.service.ts +++ b/src/services/token.service.ts @@ -160,7 +160,7 @@ export class TokenService implements TokenServiceAbstraction { getName(): string { const decoded = this.decodeToken(); if (typeof decoded.name === 'undefined') { - throw new Error('No name found'); + return null; } return decoded.name as string; From 41dd6b1f2c2d12bfa23162fa4ff5200620b6e8ac Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 12 Jul 2018 15:53:14 -0400 Subject: [PATCH 0399/1626] dashlane csv importer --- src/importers/dashlaneCsvImporter.ts | 90 ++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/importers/dashlaneCsvImporter.ts diff --git a/src/importers/dashlaneCsvImporter.ts b/src/importers/dashlaneCsvImporter.ts new file mode 100644 index 0000000000..028a79a055 --- /dev/null +++ b/src/importers/dashlaneCsvImporter.ts @@ -0,0 +1,90 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +export class DashlaneCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, false); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + let skip = false; + if (value.length < 2) { + return; + } + + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(value[0], '--'); + + if (value.length === 2) { + cipher.login.uris = this.makeUriArray(value[1]); + } else if (value.length === 3) { + cipher.login.uris = this.makeUriArray(value[1]); + cipher.login.username = this.getValueOrDefault(value[2]); + } else if (value.length === 4) { + if (this.isNullOrWhitespace(value[2]) && this.isNullOrWhitespace(value[3])) { + cipher.login.username = value[1]; + cipher.notes = value[2] + '\n' + value[3]; + } else { + cipher.login.username = value[2]; + cipher.notes = value[1] + '\n' + value[3]; + } + } else if (value.length === 5) { + cipher.login.uris = this.makeUriArray(value[1]); + cipher.login.username = this.getValueOrDefault(value[2]); + cipher.login.password = this.getValueOrDefault(value[3]); + cipher.notes = this.getValueOrDefault(value[4]); + } else if (value.length === 6) { + if (this.isNullOrWhitespace(value[2])) { + cipher.login.username = this.getValueOrDefault(value[3]); + cipher.login.password = this.getValueOrDefault(value[4]); + cipher.notes = this.getValueOrDefault(value[5]); + } else { + cipher.login.username = this.getValueOrDefault(value[2]); + cipher.login.password = this.getValueOrDefault(value[3]); + cipher.notes = this.getValueOrDefault(value[4], '') + '\n' + this.getValueOrDefault(value[5], ''); + } + cipher.login.uris = this.makeUriArray(value[1]); + } else if (value.length === 7) { + if (this.isNullOrWhitespace(value[2])) { + cipher.login.username = this.getValueOrDefault(value[3]); + cipher.notes = this.getValueOrDefault(value[4], '') + '\n' + this.getValueOrDefault(value[6], ''); + } else { + cipher.login.username = this.getValueOrDefault(value[2]); + cipher.notes = this.getValueOrDefault(value[3], '') + '\n' + + this.getValueOrDefault(value[4], '') + '\n' + this.getValueOrDefault(value[6], ''); + } + cipher.login.uris = this.makeUriArray(value[1]); + cipher.login.password = this.getValueOrDefault(value[5]); + } else { + for (let i = 1; i < value.length; i++) { + cipher.notes += (value[i] + '\n'); + if (value[i] === 'NO_TYPE') { + skip = true; + break; + } + } + } + + if (skip) { + return; + } + if (this.isNullOrWhitespace(cipher.login.username)) { + cipher.login.username = null; + } + if (this.isNullOrWhitespace(cipher.login.password)) { + cipher.login.password = null; + } + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} From 5fac06771310a083a004542db4c67028308f40c9 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 12 Jul 2018 16:27:24 -0400 Subject: [PATCH 0400/1626] msecure csv importer --- src/importers/baseImporter.ts | 2 +- src/importers/keepassxCsvImporter.ts | 2 - src/importers/msecureCsvImporter.ts | 62 ++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 src/importers/msecureCsvImporter.ts diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index 535f663753..c00564c315 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -276,7 +276,7 @@ export abstract class BaseImporter { if (cipher.notes == null) { cipher.notes = ''; } - cipher.notes += (key + ': ' + value.split(this.newLineRegex).join('\n') + '\n'); + cipher.notes += (key + ': ' + this.splitNewLine(value).join('\n') + '\n'); } else { if (cipher.fields == null) { cipher.fields = []; diff --git a/src/importers/keepassxCsvImporter.ts b/src/importers/keepassxCsvImporter.ts index 7d99a33909..3113bf5345 100644 --- a/src/importers/keepassxCsvImporter.ts +++ b/src/importers/keepassxCsvImporter.ts @@ -3,8 +3,6 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; -import { FolderView } from '../models/view/folderView'; - export class KeePassXCsvImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { const result = new ImportResult(); diff --git a/src/importers/msecureCsvImporter.ts b/src/importers/msecureCsvImporter.ts new file mode 100644 index 0000000000..60c4e3252e --- /dev/null +++ b/src/importers/msecureCsvImporter.ts @@ -0,0 +1,62 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CipherType } from '../enums/cipherType'; +import { SecureNoteType } from '../enums/secureNoteType'; + +import { SecureNoteView } from '../models/view/secureNoteView'; + +export class MSecureCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, false); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + if (value.length < 3) { + return; + } + + const folderName = this.getValueOrDefault(value[0], 'Unassigned') !== 'Unassigned' ? value[0] : null; + this.processFolder(result, folderName); + + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(value[2], '--'); + + if (value[1] === 'Web Logins') { + cipher.login.uris = this.makeUriArray(value[4]); + cipher.login.username = this.getValueOrDefault(value[5]); + cipher.login.password = this.getValueOrDefault(value[6]); + cipher.notes = !this.isNullOrWhitespace(value[3]) ? value[3].split('\\n').join('\n') : null; + } else if (value.length > 3) { + cipher.type = CipherType.SecureNote; + cipher.secureNote = new SecureNoteView(); + cipher.secureNote.type = SecureNoteType.Generic; + for (let i = 3; i < value.length; i++) { + if (!this.isNullOrWhitespace(value[i])) { + cipher.notes += (value[i] + '\n'); + } + } + } + + if (!this.isNullOrWhitespace(value[1]) && cipher.type !== CipherType.Login) { + cipher.name = value[1] + ': ' + cipher.name; + } + + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + if (this.organization) { + this.moveFoldersToCollections(result); + } + + result.success = true; + return result; + } +} From cc6f732a140e5f49f0c2fe22c34da660ffe5ea89 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 12 Jul 2018 17:07:06 -0400 Subject: [PATCH 0401/1626] hasEncKey checks --- src/abstractions/crypto.service.ts | 1 + src/angular/components/attachments.component.ts | 3 +-- src/services/crypto.service.ts | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 7c19a3487f..9014a77726 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -17,6 +17,7 @@ export abstract class CryptoService { getOrgKeys: () => Promise>; getOrgKey: (orgId: string) => Promise; hasKey: () => Promise; + hasEncKey: () => Promise; clearKey: () => Promise; clearKeyHash: () => Promise; clearEncKey: (memoryOnly?: boolean) => Promise; diff --git a/src/angular/components/attachments.component.ts b/src/angular/components/attachments.component.ts index 6429c1cdd9..c4b287c94c 100644 --- a/src/angular/components/attachments.component.ts +++ b/src/angular/components/attachments.component.ts @@ -40,8 +40,7 @@ export class AttachmentsComponent implements OnInit { this.cipherDomain = await this.loadCipher(); this.cipher = await this.cipherDomain.decrypt(); - const key = await this.cryptoService.getEncKey(); - this.hasUpdatedKey = key != null; + this.hasUpdatedKey = await this.cryptoService.hasEncKey(); const isPremium = this.tokenService.getPremium(); this.canAccessAttachments = isPremium || this.cipher.organizationId != null; diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 9d362329f9..effb5825c8 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -211,6 +211,11 @@ export class CryptoService implements CryptoServiceAbstraction { return (await this.getKey()) != null; } + async hasEncKey(): Promise { + const encKey = await this.storageService.get(Keys.encKey); + return encKey != null; + } + clearKey(): Promise { this.key = this.legacyEtmKey = null; return this.secureStorageService.remove(Keys.key); From 0217fdf7c1f402974e9ed80e216d84dffb88a696 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 13 Jul 2018 09:13:10 -0400 Subject: [PATCH 0402/1626] remember email on login --- src/angular/components/login.component.ts | 35 +++++++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/angular/components/login.component.ts b/src/angular/components/login.component.ts index 8d8295681c..e7dc0fe9d4 100644 --- a/src/angular/components/login.component.ts +++ b/src/angular/components/login.component.ts @@ -1,4 +1,7 @@ -import { Input } from '@angular/core'; +import { + Input, + OnInit, +} from '@angular/core'; import { Router } from '@angular/router'; import { ToasterService } from 'angular2-toaster'; @@ -8,9 +11,16 @@ import { AuthResult } from '../../models/domain/authResult'; import { AuthService } from '../../abstractions/auth.service'; import { I18nService } from '../../abstractions/i18n.service'; +import { StorageService } from '../../abstractions/storage.service'; -export class LoginComponent { +const Keys = { + rememberedEmail: 'rememberedEmail', + rememberEmail: 'rememberEmail', +}; + +export class LoginComponent implements OnInit { @Input() email: string = ''; + @Input() rememberEmail = true; masterPassword: string = ''; showPassword: boolean = false; @@ -22,7 +32,20 @@ export class LoginComponent { constructor(protected authService: AuthService, protected router: Router, protected analytics: Angulartics2, protected toasterService: ToasterService, - protected i18nService: I18nService) { } + protected i18nService: I18nService, private storageService: StorageService) { } + + async ngOnInit() { + if (this.email == null || this.email === '') { + this.email = await this.storageService.get(Keys.rememberedEmail); + if (this.email == null) { + this.email = ''; + } + } + this.rememberEmail = await this.storageService.get(Keys.rememberEmail); + if (this.rememberEmail == null) { + this.rememberEmail = true; + } + } async submit() { if (this.email == null || this.email === '') { @@ -44,6 +67,12 @@ export class LoginComponent { try { this.formPromise = this.authService.logIn(this.email, this.masterPassword); const response = await this.formPromise; + await this.storageService.save(Keys.rememberEmail, this.rememberEmail); + if (this.rememberEmail) { + await this.storageService.save(Keys.rememberedEmail, this.email); + } else { + await this.storageService.remove(Keys.rememberedEmail); + } if (response.twoFactor) { this.analytics.eventTrack.next({ action: 'Logged In To Two-step' }); this.router.navigate([this.twoFactorRoute]); From 747cd6373305042640f5f0d861eda0fdeb22cab7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 13 Jul 2018 09:28:45 -0400 Subject: [PATCH 0403/1626] adjust focus on fields for browser apps --- src/angular/components/login.component.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/angular/components/login.component.ts b/src/angular/components/login.component.ts index e7dc0fe9d4..a9e8cd733c 100644 --- a/src/angular/components/login.component.ts +++ b/src/angular/components/login.component.ts @@ -13,6 +13,8 @@ import { AuthService } from '../../abstractions/auth.service'; import { I18nService } from '../../abstractions/i18n.service'; import { StorageService } from '../../abstractions/storage.service'; +import { Utils } from 'jslib/misc/utils'; + const Keys = { rememberedEmail: 'rememberedEmail', rememberEmail: 'rememberEmail', @@ -45,6 +47,9 @@ export class LoginComponent implements OnInit { if (this.rememberEmail == null) { this.rememberEmail = true; } + if (Utils.isBrowser) { + document.getElementById(this.email == null || this.email === '' ? 'email' : 'masterPassword').focus(); + } } async submit() { From 6db55bbae8b54f76b4fe84102999446c4aad419e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 13 Jul 2018 09:31:14 -0400 Subject: [PATCH 0404/1626] fix path to utils --- src/angular/components/login.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/login.component.ts b/src/angular/components/login.component.ts index a9e8cd733c..9b955dc36d 100644 --- a/src/angular/components/login.component.ts +++ b/src/angular/components/login.component.ts @@ -13,7 +13,7 @@ import { AuthService } from '../../abstractions/auth.service'; import { I18nService } from '../../abstractions/i18n.service'; import { StorageService } from '../../abstractions/storage.service'; -import { Utils } from 'jslib/misc/utils'; +import { Utils } from '../../misc/utils'; const Keys = { rememberedEmail: 'rememberedEmail', From b3bd13fe2bbad26ea9d07d82ace958f466218403 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 13 Jul 2018 10:44:47 -0400 Subject: [PATCH 0405/1626] pass email param on login/register --- src/angular/components/login.component.ts | 13 +++++++++---- src/angular/components/register.component.ts | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/angular/components/login.component.ts b/src/angular/components/login.component.ts index 9b955dc36d..9e5557f9c3 100644 --- a/src/angular/components/login.component.ts +++ b/src/angular/components/login.component.ts @@ -27,7 +27,8 @@ export class LoginComponent implements OnInit { masterPassword: string = ''; showPassword: boolean = false; formPromise: Promise; - onSuccessfullLogin: () => Promise; + onSuccessfulLogin: () => Promise; + onSuccessfulLoginNavigate: () => Promise; protected twoFactorRoute = '2fa'; protected successRoute = 'vault'; @@ -82,11 +83,15 @@ export class LoginComponent implements OnInit { this.analytics.eventTrack.next({ action: 'Logged In To Two-step' }); this.router.navigate([this.twoFactorRoute]); } else { - if (this.onSuccessfullLogin != null) { - this.onSuccessfullLogin(); + if (this.onSuccessfulLogin != null) { + this.onSuccessfulLogin(); } this.analytics.eventTrack.next({ action: 'Logged In' }); - this.router.navigate([this.successRoute]); + if (this.onSuccessfulLoginNavigate != null) { + this.onSuccessfulLoginNavigate(); + } else { + this.router.navigate([this.successRoute]); + } } } catch { } } diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index a83c89f5f0..1650afd116 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -69,7 +69,7 @@ export class RegisterComponent { await this.formPromise; this.analytics.eventTrack.next({ action: 'Registered' }); this.toasterService.popAsync('success', null, this.i18nService.t('newAccountCreated')); - this.router.navigate([this.successRoute]); + this.router.navigate([this.successRoute], { queryParams: { email: this.email } }); } catch { } } From a949f499acb28ddb1ce7197fd35c0d02df957618 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 13 Jul 2018 10:49:37 -0400 Subject: [PATCH 0406/1626] onSuccessfulLoginNavigate for 2fa page --- src/angular/components/two-factor.component.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 3ebaa32126..1e676f5be1 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -38,7 +38,8 @@ export class TwoFactorComponent implements OnInit, OnDestroy { twoFactorEmail: string = null; formPromise: Promise; emailPromise: Promise; - onSuccessfullLogin: () => Promise; + onSuccessfulLogin: () => Promise; + onSuccessfulLoginNavigate: () => Promise; protected loginRoute = 'login'; protected successRoute = 'vault'; @@ -167,11 +168,15 @@ export class TwoFactorComponent implements OnInit, OnDestroy { try { this.formPromise = this.authService.logInTwoFactor(this.selectedProviderType, this.token, this.remember); await this.formPromise; - if (this.onSuccessfullLogin != null) { - this.onSuccessfullLogin(); + if (this.onSuccessfulLogin != null) { + this.onSuccessfulLogin(); } this.analytics.eventTrack.next({ action: 'Logged In From Two-step' }); - this.router.navigate([this.successRoute]); + if (this.onSuccessfulLoginNavigate != null) { + this.onSuccessfulLoginNavigate(); + } else { + this.router.navigate([this.successRoute]); + } } catch (e) { if (this.selectedProviderType === TwoFactorProviderType.U2f && this.u2f != null) { this.u2f.start(); From 9bc7459eacb55a234da8dd8aa71c059a4cd3e1ee Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 13 Jul 2018 11:06:44 -0400 Subject: [PATCH 0407/1626] add org invite token to registration if present --- src/angular/components/register.component.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index 1650afd116..77f9959243 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -10,6 +10,7 @@ import { ApiService } from '../../abstractions/api.service'; import { AuthService } from '../../abstractions/auth.service'; import { CryptoService } from '../../abstractions/crypto.service'; import { I18nService } from '../../abstractions/i18n.service'; +import { StateService } from '../../abstractions/state.service'; export class RegisterComponent { name: string = ''; @@ -25,7 +26,7 @@ export class RegisterComponent { constructor(protected authService: AuthService, protected router: Router, protected analytics: Angulartics2, protected toasterService: ToasterService, protected i18nService: I18nService, protected cryptoService: CryptoService, - protected apiService: ApiService) { } + protected apiService: ApiService, protected stateService: StateService) { } async submit() { if (this.email == null || this.email === '') { @@ -63,6 +64,11 @@ export class RegisterComponent { const request = new RegisterRequest(this.email, this.name, hashedPassword, this.hint, encKey[1].encryptedString); request.keys = new KeysRequest(keys[0], keys[1].encryptedString); + const orgInvite = await this.stateService.get('orgInvitation'); + if (orgInvite != null && orgInvite.token != null && orgInvite.organizationUserId != null) { + request.token = orgInvite.token; + request.organizationUserId = orgInvite.organizationUserId; + } try { this.formPromise = this.apiService.postRegister(request); From 51f041a9596168af0caa5ca94c0c1c6e7568f084 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 13 Jul 2018 15:55:02 -0400 Subject: [PATCH 0408/1626] recover delete apis --- src/abstractions/api.service.ts | 4 ++++ src/models/request/deleteRecoverRequest.ts | 3 +++ src/models/request/verifyDeleteRecoverRequest.ts | 9 +++++++++ src/services/api.service.ts | 10 ++++++++++ 4 files changed, 26 insertions(+) create mode 100644 src/models/request/deleteRecoverRequest.ts create mode 100644 src/models/request/verifyDeleteRecoverRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index aaaae3e4da..f45469b40a 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -7,6 +7,7 @@ import { CipherCollectionsRequest } from '../models/request/cipherCollectionsReq import { CipherRequest } from '../models/request/cipherRequest'; import { CipherShareRequest } from '../models/request/cipherShareRequest'; import { CollectionRequest } from '../models/request/collectionRequest'; +import { DeleteRecoverRequest } from '../models/request/deleteRecoverRequest'; import { EmailRequest } from '../models/request/emailRequest'; import { EmailTokenRequest } from '../models/request/emailTokenRequest'; import { FolderRequest } from '../models/request/folderRequest'; @@ -38,6 +39,7 @@ import { UpdateTwoFactorDuoRequest } from '../models/request/updateTwoFactorDuoR import { UpdateTwoFactorEmailRequest } from '../models/request/updateTwoFactorEmailRequest'; import { UpdateTwoFactorU2fRequest } from '../models/request/updateTwoFactorU2fRequest'; import { UpdateTwoFactorYubioOtpRequest } from '../models/request/updateTwoFactorYubioOtpRequest'; +import { VerifyDeleteRecoverRequest } from '../models/request/verifyDeleteRecoverRequest'; import { VerifyEmailRequest } from '../models/request/verifyEmailRequest'; import { BillingResponse } from '../models/response/billingResponse'; @@ -103,6 +105,8 @@ export abstract class ApiService { postAccountKeys: (request: KeysRequest) => Promise; postAccountVerifyEmail: () => Promise; postAccountVerifyEmailToken: (request: VerifyEmailRequest) => Promise; + postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise; + postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise; postFolder: (request: FolderRequest) => Promise; putFolder: (id: string, request: FolderRequest) => Promise; diff --git a/src/models/request/deleteRecoverRequest.ts b/src/models/request/deleteRecoverRequest.ts new file mode 100644 index 0000000000..90d6064f63 --- /dev/null +++ b/src/models/request/deleteRecoverRequest.ts @@ -0,0 +1,3 @@ +export class DeleteRecoverRequest { + email: string; +} diff --git a/src/models/request/verifyDeleteRecoverRequest.ts b/src/models/request/verifyDeleteRecoverRequest.ts new file mode 100644 index 0000000000..9372b4bf0b --- /dev/null +++ b/src/models/request/verifyDeleteRecoverRequest.ts @@ -0,0 +1,9 @@ +export class VerifyDeleteRecoverRequest { + userId: string; + token: string; + + constructor(userId: string, token: string) { + this.userId = userId; + this.token = token; + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index ba502cb50a..ccdac7d6a5 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -13,6 +13,7 @@ import { CipherCollectionsRequest } from '../models/request/cipherCollectionsReq import { CipherRequest } from '../models/request/cipherRequest'; import { CipherShareRequest } from '../models/request/cipherShareRequest'; import { CollectionRequest } from '../models/request/collectionRequest'; +import { DeleteRecoverRequest } from '../models/request/deleteRecoverRequest'; import { EmailRequest } from '../models/request/emailRequest'; import { EmailTokenRequest } from '../models/request/emailTokenRequest'; import { FolderRequest } from '../models/request/folderRequest'; @@ -44,6 +45,7 @@ import { UpdateTwoFactorDuoRequest } from '../models/request/updateTwoFactorDuoR import { UpdateTwoFactorEmailRequest } from '../models/request/updateTwoFactorEmailRequest'; import { UpdateTwoFactorU2fRequest } from '../models/request/updateTwoFactorU2fRequest'; import { UpdateTwoFactorYubioOtpRequest } from '../models/request/updateTwoFactorYubioOtpRequest'; +import { VerifyDeleteRecoverRequest } from '../models/request/verifyDeleteRecoverRequest'; import { VerifyEmailRequest } from '../models/request/verifyEmailRequest'; import { BillingResponse } from '../models/response/billingResponse'; @@ -258,6 +260,14 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/accounts/verify-email-token', request, false, false); } + postAccountRecoverDelete(request: DeleteRecoverRequest): Promise { + return this.send('POST', '/accounts/delete-recover', request, false, false); + } + + postAccountRecoverDeleteToken(request: VerifyDeleteRecoverRequest): Promise { + return this.send('POST', '/accounts/delete-recover-token', request, false, false); + } + // Folder APIs async postFolder(request: FolderRequest): Promise { From 76ece834d1d18e9cca71bb3c182d2284dae80958 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 13 Jul 2018 17:11:19 -0400 Subject: [PATCH 0409/1626] stick password xml importer --- src/importers/passwordDragonXmlImporter.ts | 2 - src/importers/stickyPasswordXmlImporter.ts | 79 ++++++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 src/importers/stickyPasswordXmlImporter.ts diff --git a/src/importers/passwordDragonXmlImporter.ts b/src/importers/passwordDragonXmlImporter.ts index 5b5db151fb..5fc52b524e 100644 --- a/src/importers/passwordDragonXmlImporter.ts +++ b/src/importers/passwordDragonXmlImporter.ts @@ -3,8 +3,6 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; -import { FolderView } from '../models/view/folderView'; - export class PasswordDragonXmlImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { const result = new ImportResult(); diff --git a/src/importers/stickyPasswordXmlImporter.ts b/src/importers/stickyPasswordXmlImporter.ts new file mode 100644 index 0000000000..6bd992f814 --- /dev/null +++ b/src/importers/stickyPasswordXmlImporter.ts @@ -0,0 +1,79 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +export class StickyPasswordXmlImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const doc = this.parseXml(data); + if (doc == null) { + result.success = false; + return result; + } + + const loginNodes = doc.querySelectorAll('root > Database > Logins > Login'); + Array.from(loginNodes).forEach((loginNode) => { + const accountId = loginNode.getAttribute('ID'); + if (this.isNullOrWhitespace(accountId)) { + return; + } + + const usernameText = loginNode.getAttribute('Name'); + const passwordText = loginNode.getAttribute('Password'); + let titleText: string = null; + let linkText: string = null; + let notesText: string = null; + let groupId: string = null; + let groupText: string = null; + + const accountLogin = doc.querySelector('root > Database > Accounts > Account > ' + + 'LoginLinks > Login[SourceLoginID="' + accountId + '"]'); + if (accountLogin != null) { + const account = accountLogin.parentElement.parentElement; + if (account != null) { + titleText = account.getAttribute('Name'); + linkText = account.getAttribute('Link'); + groupId = account.getAttribute('ParentID'); + notesText = account.getAttribute('Comments'); + if (!this.isNullOrWhitespace(notesText)) { + notesText = notesText.split('/n').join('\n'); + } + } + } + + if (!this.isNullOrWhitespace(groupId)) { + groupText = this.buildGroupText(doc, groupId, ''); + this.processFolder(result, groupText); + } + + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(titleText, '--'); + cipher.notes = this.getValueOrDefault(notesText); + cipher.login.username = this.getValueOrDefault(usernameText); + cipher.login.password = this.getValueOrDefault(passwordText); + cipher.login.uris = this.makeUriArray(linkText); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + if (this.organization) { + this.moveFoldersToCollections(result); + } + + result.success = true; + return result; + } + + buildGroupText(doc: Document, groupId: string, groupText: string): string { + const group = doc.querySelector('root > Database > Groups > Group[ID="' + groupId + '"]'); + if (group == null) { + return groupText; + } + if (!this.isNullOrWhitespace(groupText)) { + groupText = ' > ' + groupText; + } + groupText = group.getAttribute('Name') + groupText; + return this.buildGroupText(doc, group.getAttribute('ParentID'), groupText); + } +} From 3649e2fffe414477334a56f7f4bad7610eaca2b7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 16 Jul 2018 12:30:45 -0400 Subject: [PATCH 0410/1626] org apis --- src/abstractions/api.service.ts | 4 ++++ src/models/request/organizationUpdateRequest.ts | 5 +++++ src/services/api.service.ts | 15 +++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 src/models/request/organizationUpdateRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index f45469b40a..8b2a39f098 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -17,6 +17,7 @@ import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest'; import { KeysRequest } from '../models/request/keysRequest'; import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest'; +import { OrganizationUpdateRequest } from '../models/request/organizationUpdateRequest'; import { OrganizationUserAcceptRequest } from '../models/request/organizationUserAcceptRequest'; import { OrganizationUserConfirmRequest } from '../models/request/organizationUserConfirmRequest'; import { OrganizationUserInviteRequest } from '../models/request/organizationUserInviteRequest'; @@ -192,9 +193,12 @@ export abstract class ApiService { postTwoFactorEmailSetup: (request: TwoFactorEmailRequest) => Promise; postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise; + getOrganization: (id: string) => Promise; postOrganization: (request: OrganizationCreateRequest) => Promise; + putOrganization: (id: string, request: OrganizationUpdateRequest) => Promise; postLeaveOrganization: (id: string) => Promise; postOrganizationLicense: (data: FormData) => Promise; + deleteOrganization: (id: string) => Promise; getEvents: (start: string, end: string, token: string) => Promise>; getEventsCipher: (id: string, start: string, end: string, token: string) => Promise>; diff --git a/src/models/request/organizationUpdateRequest.ts b/src/models/request/organizationUpdateRequest.ts new file mode 100644 index 0000000000..ffb9149b34 --- /dev/null +++ b/src/models/request/organizationUpdateRequest.ts @@ -0,0 +1,5 @@ +export class OrganizationUpdateRequest { + name: string; + businessName: string; + billingEmail: string; +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index ccdac7d6a5..06d0c299e6 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -23,6 +23,7 @@ import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest'; import { KeysRequest } from '../models/request/keysRequest'; import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest'; +import { OrganizationUpdateRequest } from '../models/request/organizationUpdateRequest'; import { OrganizationUserAcceptRequest } from '../models/request/organizationUserAcceptRequest'; import { OrganizationUserConfirmRequest } from '../models/request/organizationUserConfirmRequest'; import { OrganizationUserInviteRequest } from '../models/request/organizationUserInviteRequest'; @@ -626,11 +627,21 @@ export class ApiService implements ApiServiceAbstraction { // Organization APIs + async getOrganization(id: string): Promise { + const r = await this.send('GET', '/organizations/' + id, null, true, true); + return new OrganizationResponse(r); + } + async postOrganization(request: OrganizationCreateRequest): Promise { const r = await this.send('POST', '/organizations', request, true, true); return new OrganizationResponse(r); } + async putOrganization(id: string, request: OrganizationUpdateRequest): Promise { + const r = await this.send('PUT', '/organizations/' + id, request, true, true); + return new OrganizationResponse(r); + } + postLeaveOrganization(id: string): Promise { return this.send('POST', '/organizations/' + id + '/leave', null, true, false); } @@ -640,6 +651,10 @@ export class ApiService implements ApiServiceAbstraction { return new OrganizationResponse(r); } + deleteOrganization(id: string): Promise { + return this.send('DELETE', '/organizations/' + id, null, true, false); + } + // Event APIs async getEvents(start: string, end: string, token: string): Promise> { From b2c700ad285d9336426284f766b434be8c509f0a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 16 Jul 2018 12:41:54 -0400 Subject: [PATCH 0411/1626] org delete apis --- src/abstractions/api.service.ts | 2 +- src/services/api.service.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 8b2a39f098..7809b9e397 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -198,7 +198,7 @@ export abstract class ApiService { putOrganization: (id: string, request: OrganizationUpdateRequest) => Promise; postLeaveOrganization: (id: string) => Promise; postOrganizationLicense: (data: FormData) => Promise; - deleteOrganization: (id: string) => Promise; + postDeleteOrganization: (id: string, request: PasswordVerificationRequest) => Promise; getEvents: (start: string, end: string, token: string) => Promise>; getEventsCipher: (id: string, start: string, end: string, token: string) => Promise>; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 06d0c299e6..8b0c6fd825 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -651,8 +651,8 @@ export class ApiService implements ApiServiceAbstraction { return new OrganizationResponse(r); } - deleteOrganization(id: string): Promise { - return this.send('DELETE', '/organizations/' + id, null, true, false); + postDeleteOrganization(id: string, request: PasswordVerificationRequest): Promise { + return this.send('POST', '/organizations/' + id + '/delete', request, true, false); } // Event APIs From 6b4ae1b8d591e973fe4036d3c10b6f0be81debc9 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 16 Jul 2018 17:16:15 -0400 Subject: [PATCH 0412/1626] api adjustments and org additions --- src/abstractions/api.service.ts | 8 +++-- .../response/organizationBillingResponse.ts | 32 +++++++++++++++++++ src/services/api.service.ts | 22 ++++++++++--- 3 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 src/models/response/organizationBillingResponse.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 7809b9e397..56b6f61836 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -61,6 +61,7 @@ import { GroupUserResponse } from '../models/response/groupUserResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; import { ListResponse } from '../models/response/listResponse'; +import { OrganizationBillingResponse } from '../models/response/organizationBillingResponse'; import { OrganizationResponse } from '../models/response/organizationResponse'; import { OrganizationUserDetailsResponse, @@ -93,7 +94,7 @@ export abstract class ApiService { postEmail: (request: EmailRequest) => Promise; postPassword: (request: PasswordRequest) => Promise; postSecurityStamp: (request: PasswordVerificationRequest) => Promise; - postDeleteAccount: (request: PasswordVerificationRequest) => Promise; + deleteAccount: (request: PasswordVerificationRequest) => Promise; getAccountRevisionDate: () => Promise; postPasswordHint: (request: PasswordHintRequest) => Promise; postRegister: (request: RegisterRequest) => Promise; @@ -194,11 +195,14 @@ export abstract class ApiService { postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise; getOrganization: (id: string) => Promise; + getOrganizationBilling: (id: string) => Promise; postOrganization: (request: OrganizationCreateRequest) => Promise; putOrganization: (id: string, request: OrganizationUpdateRequest) => Promise; postLeaveOrganization: (id: string) => Promise; postOrganizationLicense: (data: FormData) => Promise; - postDeleteOrganization: (id: string, request: PasswordVerificationRequest) => Promise; + postOrganizationStorage: (id: string, request: StorageRequest) => Promise; + postOrganizationPayment: (id: string, request: PaymentRequest) => Promise; + deleteOrganization: (id: string, request: PasswordVerificationRequest) => Promise; getEvents: (start: string, end: string, token: string) => Promise>; getEventsCipher: (id: string, start: string, end: string, token: string) => Promise>; diff --git a/src/models/response/organizationBillingResponse.ts b/src/models/response/organizationBillingResponse.ts new file mode 100644 index 0000000000..db5e5ca2bb --- /dev/null +++ b/src/models/response/organizationBillingResponse.ts @@ -0,0 +1,32 @@ +import { + BillingChargeResponse, + BillingInvoiceResponse, + BillingSourceResponse, + BillingSubscriptionResponse, +} from './billingResponse'; +import { OrganizationResponse } from './organizationResponse'; + +export class OrganizationBillingResponse extends OrganizationResponse { + storageName: string; + storageGb: number; + paymentSource: BillingSourceResponse; + subscription: BillingSubscriptionResponse; + upcomingInvoice: BillingInvoiceResponse; + charges: BillingChargeResponse[] = []; + expiration: Date; + + constructor(response: any) { + super(response); + this.storageName = response.StorageName; + this.storageGb = response.StorageGb; + this.paymentSource = response.PaymentSource == null ? null : new BillingSourceResponse(response.PaymentSource); + this.subscription = response.Subscription == null ? + null : new BillingSubscriptionResponse(response.Subscription); + this.upcomingInvoice = response.UpcomingInvoice == null ? + null : new BillingInvoiceResponse(response.UpcomingInvoice); + if (response.Charges != null) { + this.charges = response.Charges.map((c: any) => new BillingChargeResponse(c)); + } + this.expiration = response.Expiration; + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 8b0c6fd825..1a09753848 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -68,6 +68,7 @@ import { GroupUserResponse } from '../models/response/groupUserResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; import { ListResponse } from '../models/response/listResponse'; +import { OrganizationBillingResponse } from '../models/response/organizationBillingResponse'; import { OrganizationResponse } from '../models/response/organizationResponse'; import { OrganizationUserDetailsResponse, @@ -208,8 +209,8 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/accounts/security-stamp', request, true, false); } - postDeleteAccount(request: PasswordVerificationRequest): Promise { - return this.send('POST', '/accounts/delete', request, true, false); + deleteAccount(request: PasswordVerificationRequest): Promise { + return this.send('DELETE', '/accounts/delete', request, true, false); } async getAccountRevisionDate(): Promise { @@ -632,6 +633,11 @@ export class ApiService implements ApiServiceAbstraction { return new OrganizationResponse(r); } + async getOrganizationBilling(id: string): Promise { + const r = await this.send('GET', '/organizations/' + id + '/billing', null, true, true); + return new OrganizationBillingResponse(r); + } + async postOrganization(request: OrganizationCreateRequest): Promise { const r = await this.send('POST', '/organizations', request, true, true); return new OrganizationResponse(r); @@ -651,8 +657,16 @@ export class ApiService implements ApiServiceAbstraction { return new OrganizationResponse(r); } - postDeleteOrganization(id: string, request: PasswordVerificationRequest): Promise { - return this.send('POST', '/organizations/' + id + '/delete', request, true, false); + postOrganizationStorage(id: string, request: StorageRequest): Promise { + return this.send('POST', '/organizations/' + id + '/storage', request, true, false); + } + + postOrganizationPayment(id: string, request: PaymentRequest): Promise { + return this.send('POST', '/organizations/' + id + '/payment', request, true, false); + } + + deleteOrganization(id: string, request: PasswordVerificationRequest): Promise { + return this.send('DELETE', '/organizations/' + id, request, true, false); } // Event APIs From d677bad76a5e2d95691ffcf164e11094b1d01ee4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 17 Jul 2018 00:15:14 -0400 Subject: [PATCH 0413/1626] truekey csv importer --- src/importers/truekeyCsvImporter.ts | 74 +++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/importers/truekeyCsvImporter.ts diff --git a/src/importers/truekeyCsvImporter.ts b/src/importers/truekeyCsvImporter.ts new file mode 100644 index 0000000000..e8d30a88a2 --- /dev/null +++ b/src/importers/truekeyCsvImporter.ts @@ -0,0 +1,74 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CardView } from '../models/view/cardView'; +import { SecureNoteView } from '../models/view/secureNoteView'; + +import { CipherType } from '../enums/cipherType'; +import { SecureNoteType } from '../enums/secureNoteType'; + +const PropertiesToIgnore = ['kind', 'autologin', 'favorite', 'hexcolor', 'protectedwithpassword', 'subdomainonly', + 'type', 'tk_export_version', 'note', 'title', 'document_content', +]; + +export class TrueKeyCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + const cipher = this.initLoginCipher(); + cipher.favorite = this.getValueOrDefault(value.favorite, '').toLowerCase() === 'true'; + cipher.name = this.getValueOrDefault(value.name, '--'); + cipher.notes = this.getValueOrDefault(value.memo, ''); + cipher.login.username = this.getValueOrDefault(value.login); + cipher.login.password = this.getValueOrDefault(value.password); + cipher.login.uris = this.makeUriArray(value.url); + + if (value.kind !== 'login') { + cipher.name = this.getValueOrDefault(value.title, '--'); + cipher.notes = this.getValueOrDefault(value.note, ''); + } + + if (value.kind === 'cc') { + cipher.type = CipherType.Card; + cipher.card = new CardView(); + cipher.card.cardholderName = this.getValueOrDefault(value.cardholder); + cipher.card.number = this.getValueOrDefault(value.number); + cipher.card.brand = this.getCardBrand(cipher.card.number); + if (!this.isNullOrWhitespace(value.expiryDate)) { + try { + const expDate = new Date(value.expiryDate); + cipher.card.expYear = expDate.getFullYear().toString(); + cipher.card.expMonth = (expDate.getMonth() + 1).toString(); + } catch { } + } + } else if (value.kind !== 'login') { + cipher.type = CipherType.SecureNote; + cipher.secureNote = new SecureNoteView(); + cipher.secureNote.type = SecureNoteType.Generic; + if (!this.isNullOrWhitespace(cipher.notes)) { + cipher.notes = this.getValueOrDefault(value.document_content, ''); + } + for (const property in value) { + if (value.hasOwnProperty(property) && PropertiesToIgnore.indexOf(property.toLowerCase()) < 0 && + !this.isNullOrWhitespace(value[property])) { + this.processKvp(cipher, property, value[property]); + } + } + } + + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} From 4be7c6aa4cf08171fecec26476a3ea045ada1124 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 17 Jul 2018 10:08:35 -0400 Subject: [PATCH 0414/1626] clipperz html importer --- src/importers/clipperzHtmlImporter.ts | 89 +++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/importers/clipperzHtmlImporter.ts diff --git a/src/importers/clipperzHtmlImporter.ts b/src/importers/clipperzHtmlImporter.ts new file mode 100644 index 0000000000..5e8d78d50b --- /dev/null +++ b/src/importers/clipperzHtmlImporter.ts @@ -0,0 +1,89 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; +import { SecureNoteView } from '../models/view/secureNoteView'; + +import { CipherType } from '../enums/cipherType'; +import { SecureNoteType } from '../enums/secureNoteType'; + +export class ClipperzHtmlImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const doc = this.parseXml(data); + if (doc == null) { + result.success = false; + return result; + } + + const textarea = doc.querySelector('textarea'); + if (textarea == null || this.isNullOrWhitespace(textarea.textContent)) { + result.errorMessage = 'Missing textarea.'; + result.success = false; + return result; + } + + const entries = JSON.parse(textarea.textContent); + entries.forEach((entry: any) => { + const cipher = this.initLoginCipher(); + if (!this.isNullOrWhitespace(entry.label)) { + cipher.name = entry.label.split(' ')[0]; + } + if (entry.data != null && !this.isNullOrWhitespace(entry.data.notes)) { + cipher.notes = entry.data.notes.split('\\n').join('\n'); + } + + if (entry.currentVersion != null && entry.currentVersion.fields != null) { + for (const property in entry.currentVersion.fields) { + if (!entry.currentVersion.fields.hasOwnProperty(property)) { + continue; + } + + const field = entry.currentVersion.fields[property]; + const actionType = field.actionType != null ? field.actionType.toLowerCase() : null; + switch (actionType) { + case 'password': + cipher.login.password = this.getValueOrDefault(field.value); + break; + case 'email': + case 'username': + case 'user': + case 'name': + cipher.login.username = this.getValueOrDefault(field.value); + break; + case 'url': + cipher.login.uris = this.makeUriArray(field.value); + break; + default: + const labelLower = field.label != null ? field.label.toLowerCase() : null; + if (cipher.login.password == null && this.passwordFieldNames.indexOf(labelLower) > -1) { + cipher.login.password = this.getValueOrDefault(field.value); + } else if (cipher.login.username == null && + this.usernameFieldNames.indexOf(labelLower) > -1) { + cipher.login.username = this.getValueOrDefault(field.value); + } else if ((cipher.login.uris == null || cipher.login.uris.length === 0) && + this.uriFieldNames.indexOf(labelLower) > -1) { + cipher.login.uris = this.makeUriArray(field.value); + } else { + this.processKvp(cipher, field.label, field.value); + } + break; + } + } + } + + if (this.isNullOrWhitespace(cipher.login.username) && this.isNullOrWhitespace(cipher.login.password) && + (cipher.login.uris == null || cipher.login.uris.length === 0)) { + cipher.type = CipherType.SecureNote; + cipher.secureNote = new SecureNoteView(); + cipher.secureNote.type = SecureNoteType.Generic; + } + + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} From 9c0b4b897387045d08c5e9651ba4c103379d3ac6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 17 Jul 2018 11:04:19 -0400 Subject: [PATCH 0415/1626] org billing apis --- src/abstractions/api.service.ts | 4 ++++ src/models/request/verifyBankRequest.ts | 4 ++++ src/services/api.service.ts | 13 +++++++++++++ 3 files changed, 21 insertions(+) create mode 100644 src/models/request/verifyBankRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 56b6f61836..15dde2ac27 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -40,6 +40,7 @@ import { UpdateTwoFactorDuoRequest } from '../models/request/updateTwoFactorDuoR import { UpdateTwoFactorEmailRequest } from '../models/request/updateTwoFactorEmailRequest'; import { UpdateTwoFactorU2fRequest } from '../models/request/updateTwoFactorU2fRequest'; import { UpdateTwoFactorYubioOtpRequest } from '../models/request/updateTwoFactorYubioOtpRequest'; +import { VerifyBankRequest } from '../models/request/verifyBankRequest'; import { VerifyDeleteRecoverRequest } from '../models/request/verifyDeleteRecoverRequest'; import { VerifyEmailRequest } from '../models/request/verifyEmailRequest'; @@ -202,6 +203,9 @@ export abstract class ApiService { postOrganizationLicense: (data: FormData) => Promise; postOrganizationStorage: (id: string, request: StorageRequest) => Promise; postOrganizationPayment: (id: string, request: PaymentRequest) => Promise; + postOrganizationVerifyBank: (id: string, request: VerifyBankRequest) => Promise; + postOrganizationCancel: (id: string) => Promise; + postOrganizationReinstate: (id: string) => Promise; deleteOrganization: (id: string, request: PasswordVerificationRequest) => Promise; getEvents: (start: string, end: string, token: string) => Promise>; diff --git a/src/models/request/verifyBankRequest.ts b/src/models/request/verifyBankRequest.ts new file mode 100644 index 0000000000..bddd452d93 --- /dev/null +++ b/src/models/request/verifyBankRequest.ts @@ -0,0 +1,4 @@ +export class VerifyBankRequest { + amount1: number; + amount2: number; +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 1a09753848..75720dc353 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -46,6 +46,7 @@ import { UpdateTwoFactorDuoRequest } from '../models/request/updateTwoFactorDuoR import { UpdateTwoFactorEmailRequest } from '../models/request/updateTwoFactorEmailRequest'; import { UpdateTwoFactorU2fRequest } from '../models/request/updateTwoFactorU2fRequest'; import { UpdateTwoFactorYubioOtpRequest } from '../models/request/updateTwoFactorYubioOtpRequest'; +import { VerifyBankRequest } from '../models/request/verifyBankRequest'; import { VerifyDeleteRecoverRequest } from '../models/request/verifyDeleteRecoverRequest'; import { VerifyEmailRequest } from '../models/request/verifyEmailRequest'; @@ -665,6 +666,18 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/organizations/' + id + '/payment', request, true, false); } + postOrganizationVerifyBank(id: string, request: VerifyBankRequest): Promise { + return this.send('POST', '/organizations/' + id + '/verify-bank', request, true, false); + } + + postOrganizationCancel(id: string): Promise { + return this.send('POST', '/organizations/' + id + '/cancel', null, true, false); + } + + postOrganizationReinstate(id: string): Promise { + return this.send('POST', '/organizations/' + id + '/reinstate', null, true, false); + } + deleteOrganization(id: string, request: PasswordVerificationRequest): Promise { return this.send('DELETE', '/organizations/' + id, request, true, false); } From 4228277d23503d563560b44a652293d23233aa1b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 17 Jul 2018 11:25:15 -0400 Subject: [PATCH 0416/1626] get org license apis --- src/abstractions/api.service.ts | 1 + src/services/api.service.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 15dde2ac27..425e5cb075 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -197,6 +197,7 @@ export abstract class ApiService { getOrganization: (id: string) => Promise; getOrganizationBilling: (id: string) => Promise; + getOrganizationLicense: (id: string, installationId: string) => Promise; postOrganization: (request: OrganizationCreateRequest) => Promise; putOrganization: (id: string, request: OrganizationUpdateRequest) => Promise; postLeaveOrganization: (id: string) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 75720dc353..749073f743 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -639,6 +639,11 @@ export class ApiService implements ApiServiceAbstraction { return new OrganizationBillingResponse(r); } + async getOrganizationLicense(id: string, installationId: string): Promise { + return this.send('GET', '/organizations/' + id + '/license?installationId=' + installationId, + null, true, true); + } + async postOrganization(request: OrganizationCreateRequest): Promise { const r = await this.send('POST', '/organizations', request, true, true); return new OrganizationResponse(r); From 1cb3447bdd3531d08eb77a8b7a0ad65124428a09 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 17 Jul 2018 12:05:19 -0400 Subject: [PATCH 0417/1626] org seat billing api --- src/abstractions/api.service.ts | 2 ++ src/models/request/seatRequest.ts | 3 +++ src/services/api.service.ts | 5 +++++ 3 files changed, 10 insertions(+) create mode 100644 src/models/request/seatRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 425e5cb075..b3fb8512dc 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -28,6 +28,7 @@ import { PasswordRequest } from '../models/request/passwordRequest'; import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; import { PaymentRequest } from '../models/request/paymentRequest'; import { RegisterRequest } from '../models/request/registerRequest'; +import { SeatRequest } from '../models/request/seatRequest'; import { StorageRequest } from '../models/request/storageRequest'; import { TokenRequest } from '../models/request/tokenRequest'; import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; @@ -202,6 +203,7 @@ export abstract class ApiService { putOrganization: (id: string, request: OrganizationUpdateRequest) => Promise; postLeaveOrganization: (id: string) => Promise; postOrganizationLicense: (data: FormData) => Promise; + postOrganizationSeat: (id: string, request: SeatRequest) => Promise; postOrganizationStorage: (id: string, request: StorageRequest) => Promise; postOrganizationPayment: (id: string, request: PaymentRequest) => Promise; postOrganizationVerifyBank: (id: string, request: VerifyBankRequest) => Promise; diff --git a/src/models/request/seatRequest.ts b/src/models/request/seatRequest.ts new file mode 100644 index 0000000000..92d47669b8 --- /dev/null +++ b/src/models/request/seatRequest.ts @@ -0,0 +1,3 @@ +export class SeatRequest { + seatAdjustment: number; +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 749073f743..12e40e0df5 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -34,6 +34,7 @@ import { PasswordRequest } from '../models/request/passwordRequest'; import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; import { PaymentRequest } from '../models/request/paymentRequest'; import { RegisterRequest } from '../models/request/registerRequest'; +import { SeatRequest } from '../models/request/seatRequest'; import { StorageRequest } from '../models/request/storageRequest'; import { TokenRequest } from '../models/request/tokenRequest'; import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; @@ -663,6 +664,10 @@ export class ApiService implements ApiServiceAbstraction { return new OrganizationResponse(r); } + postOrganizationSeat(id: string, request: SeatRequest): Promise { + return this.send('POST', '/organizations/' + id + '/seat', request, true, false); + } + postOrganizationStorage(id: string, request: StorageRequest): Promise { return this.send('POST', '/organizations/' + id + '/storage', request, true, false); } From 3354f0b8180c319b4ae72c062874e9f8f5a1fe61 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 17 Jul 2018 15:56:49 -0400 Subject: [PATCH 0418/1626] is owner get prop --- src/models/domain/organization.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/models/domain/organization.ts b/src/models/domain/organization.ts index bd38d15883..c6e136a368 100644 --- a/src/models/domain/organization.ts +++ b/src/models/domain/organization.ts @@ -50,4 +50,8 @@ export class Organization { get isAdmin() { return this.type === OrganizationUserType.Owner || this.type === OrganizationUserType.Admin; } + + get isOwner() { + return this.type === OrganizationUserType.Owner; + } } From f35ecf0cd8bd1b91f2544e07bb7b766d227f54ff Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 17 Jul 2018 17:22:45 -0400 Subject: [PATCH 0419/1626] update key api changes --- src/abstractions/api.service.ts | 2 ++ src/abstractions/cipher.service.ts | 2 +- src/abstractions/folder.service.ts | 3 ++- src/models/request/folderWithIdRequest.ts | 12 ++++++++++++ src/models/request/updateKeyRequest.ts | 10 ++++++++++ src/services/api.service.ts | 5 +++++ src/services/cipher.service.ts | 6 ++++-- src/services/folder.service.ts | 5 +++-- 8 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 src/models/request/folderWithIdRequest.ts create mode 100644 src/models/request/updateKeyRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index b3fb8512dc..ffb6ecb279 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -35,6 +35,7 @@ import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; import { TwoFactorProviderRequest } from '../models/request/twoFactorProviderRequest'; import { TwoFactorRecoveryRequest } from '../models/request/twoFactorRecoveryRequest'; import { UpdateDomainsRequest } from '../models/request/updateDomainsRequest'; +import { UpdateKeyRequest } from '../models/request/updateKeyRequest'; import { UpdateProfileRequest } from '../models/request/updateProfileRequest'; import { UpdateTwoFactorAuthenticatorRequest } from '../models/request/updateTwoFactorAuthenticatorRequest'; import { UpdateTwoFactorDuoRequest } from '../models/request/updateTwoFactorDuoRequest'; @@ -106,6 +107,7 @@ export abstract class ApiService { postAccountStorage: (request: StorageRequest) => Promise; postAccountPayment: (request: PaymentRequest) => Promise; postAccountLicense: (data: FormData) => Promise; + postAccountKey: (request: UpdateKeyRequest) => Promise; postAccountKeys: (request: KeysRequest) => Promise; postAccountVerifyEmail: () => Promise; postAccountVerifyEmailToken: (request: VerifyEmailRequest) => Promise; diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index 39176d1204..1046077659 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -14,7 +14,7 @@ export abstract class CipherService { decryptedCipherCache: CipherView[]; clearCache: () => void; - encrypt: (model: CipherView) => Promise; + encrypt: (model: CipherView, key?: SymmetricCryptoKey) => Promise; encryptFields: (fieldsModel: FieldView[], key: SymmetricCryptoKey) => Promise; encryptField: (fieldModel: FieldView, key: SymmetricCryptoKey) => Promise; get: (id: string) => Promise; diff --git a/src/abstractions/folder.service.ts b/src/abstractions/folder.service.ts index 381bc01317..1fec203798 100644 --- a/src/abstractions/folder.service.ts +++ b/src/abstractions/folder.service.ts @@ -1,6 +1,7 @@ import { FolderData } from '../models/data/folderData'; import { Folder } from '../models/domain/folder'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; import { FolderView } from '../models/view/folderView'; @@ -8,7 +9,7 @@ export abstract class FolderService { decryptedFolderCache: FolderView[]; clearCache: () => void; - encrypt: (model: FolderView) => Promise; + encrypt: (model: FolderView, key?: SymmetricCryptoKey) => Promise; get: (id: string) => Promise; getAll: () => Promise; getAllDecrypted: () => Promise; diff --git a/src/models/request/folderWithIdRequest.ts b/src/models/request/folderWithIdRequest.ts new file mode 100644 index 0000000000..a97ad6d813 --- /dev/null +++ b/src/models/request/folderWithIdRequest.ts @@ -0,0 +1,12 @@ +import { FolderRequest } from './folderRequest'; + +import { Folder } from '../domain/folder'; + +export class FolderWithIdRequest extends FolderRequest { + id: string; + + constructor(folder: Folder) { + super(folder); + this.id = folder.id; + } +} diff --git a/src/models/request/updateKeyRequest.ts b/src/models/request/updateKeyRequest.ts new file mode 100644 index 0000000000..0464e5ce53 --- /dev/null +++ b/src/models/request/updateKeyRequest.ts @@ -0,0 +1,10 @@ +import { CipherWithIdRequest } from './cipherWithIdRequest'; +import { FolderWithIdRequest } from './folderWithIdRequest'; + +export class UpdateKeyRequest { + ciphers: CipherWithIdRequest[] = []; + folders: FolderWithIdRequest[] = []; + masterPasswordHash: string; + privateKey: string; + key: string; +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 12e40e0df5..f035ad0aec 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -41,6 +41,7 @@ import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; import { TwoFactorProviderRequest } from '../models/request/twoFactorProviderRequest'; import { TwoFactorRecoveryRequest } from '../models/request/twoFactorRecoveryRequest'; import { UpdateDomainsRequest } from '../models/request/updateDomainsRequest'; +import { UpdateKeyRequest } from '../models/request/updateKeyRequest'; import { UpdateProfileRequest } from '../models/request/updateProfileRequest'; import { UpdateTwoFactorAuthenticatorRequest } from '../models/request/updateTwoFactorAuthenticatorRequest'; import { UpdateTwoFactorDuoRequest } from '../models/request/updateTwoFactorDuoRequest'; @@ -256,6 +257,10 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/accounts/keys', request, true, false); } + postAccountKey(request: UpdateKeyRequest): Promise { + return this.send('POST', '/accounts/key', request, true, false); + } + postAccountVerifyEmail(): Promise { return this.send('POST', '/accounts/verify-email', null, true, false); } diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 54cc1517b8..b7b82eece9 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -60,7 +60,7 @@ export class CipherService implements CipherServiceAbstraction { this.decryptedCipherCache = null; } - async encrypt(model: CipherView): Promise { + async encrypt(model: CipherView, key?: SymmetricCryptoKey): Promise { const cipher = new Cipher(); cipher.id = model.id; cipher.folderId = model.folderId; @@ -69,7 +69,9 @@ export class CipherService implements CipherServiceAbstraction { cipher.type = model.type; cipher.collectionIds = model.collectionIds; - const key = await this.cryptoService.getOrgKey(cipher.organizationId); + if (key == null && cipher.organizationId != null) { + key = await this.cryptoService.getOrgKey(cipher.organizationId); + } await Promise.all([ this.encryptObjProperty(model, cipher, { name: null, diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index 955cc2387a..c03cb0c298 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -1,6 +1,7 @@ import { FolderData } from '../models/data/folderData'; import { Folder } from '../models/domain/folder'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; import { FolderRequest } from '../models/request/folderRequest'; @@ -35,10 +36,10 @@ export class FolderService implements FolderServiceAbstraction { this.decryptedFolderCache = null; } - async encrypt(model: FolderView): Promise { + async encrypt(model: FolderView, key?: SymmetricCryptoKey): Promise { const folder = new Folder(); folder.id = model.id; - folder.name = await this.cryptoService.encrypt(model.name); + folder.name = await this.cryptoService.encrypt(model.name, key); return folder; } From 493770510f1a26025710380285f08212b92c1aca Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 17 Jul 2018 22:50:04 -0400 Subject: [PATCH 0420/1626] org update license api --- src/abstractions/api.service.ts | 1 + src/services/api.service.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index ffb6ecb279..3c7857af64 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -205,6 +205,7 @@ export abstract class ApiService { putOrganization: (id: string, request: OrganizationUpdateRequest) => Promise; postLeaveOrganization: (id: string) => Promise; postOrganizationLicense: (data: FormData) => Promise; + postOrganizationLicenseUpdate: (id: string, data: FormData) => Promise postOrganizationSeat: (id: string, request: SeatRequest) => Promise; postOrganizationStorage: (id: string, request: StorageRequest) => Promise; postOrganizationPayment: (id: string, request: PaymentRequest) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index f035ad0aec..8226d26047 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -669,6 +669,10 @@ export class ApiService implements ApiServiceAbstraction { return new OrganizationResponse(r); } + async postOrganizationLicenseUpdate(id: string, data: FormData): Promise { + return this.send('POST', '/organizations/' + id + '/license', data, true, false); + } + postOrganizationSeat(id: string, request: SeatRequest): Promise { return this.send('POST', '/organizations/' + id + '/seat', request, true, false); } From 1660d7570b41ddc433ff7b2ac4d6d6798ac4fc94 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 17 Jul 2018 23:14:42 -0400 Subject: [PATCH 0421/1626] lint fix --- src/abstractions/api.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 3c7857af64..576befa317 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -205,7 +205,7 @@ export abstract class ApiService { putOrganization: (id: string, request: OrganizationUpdateRequest) => Promise; postLeaveOrganization: (id: string) => Promise; postOrganizationLicense: (data: FormData) => Promise; - postOrganizationLicenseUpdate: (id: string, data: FormData) => Promise + postOrganizationLicenseUpdate: (id: string, data: FormData) => Promise; postOrganizationSeat: (id: string, request: SeatRequest) => Promise; postOrganizationStorage: (id: string, request: StorageRequest) => Promise; postOrganizationPayment: (id: string, request: PaymentRequest) => Promise; From 4d702f4de3dc299ac7c3295b61ff1d21cce17592 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 17 Jul 2018 23:47:28 -0400 Subject: [PATCH 0422/1626] roboform csv importer --- src/importers/roboformCsvImporter.ts | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/importers/roboformCsvImporter.ts diff --git a/src/importers/roboformCsvImporter.ts b/src/importers/roboformCsvImporter.ts new file mode 100644 index 0000000000..6d0cc5a2a4 --- /dev/null +++ b/src/importers/roboformCsvImporter.ts @@ -0,0 +1,44 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +export class RoboFormCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + let i = 1; + results.forEach((value) => { + const folder = !this.isNullOrWhitespace(value.Folder) && value.Folder.startsWith('/') ? + value.Folder.replace('/', '') : value.Folder; + const folderName = !this.isNullOrWhitespace(folder) ? folder.split('/').join(' > ') : null; + this.processFolder(result, folderName); + + const cipher = this.initLoginCipher(); + cipher.notes = this.getValueOrDefault(value.Note); + cipher.name = this.getValueOrDefault(value.Name, '--'); + cipher.login.username = this.getValueOrDefault(value.Login); + cipher.login.password = this.getValueOrDefault(value.Pwd); + cipher.login.uris = this.makeUriArray(value.Url); + this.cleanupCipher(cipher); + + if (i === results.length && cipher.name === '--' && this.isNullOrWhitespace(cipher.login.password)) { + return; + } + result.ciphers.push(cipher); + i++; + }); + + if (this.organization) { + this.moveFoldersToCollections(result); + } + + result.success = true; + return result; + } +} From aa811e51c34774e531aab6649799d50de0204a0f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 18 Jul 2018 09:21:34 -0400 Subject: [PATCH 0423/1626] add storage gb info to org --- src/models/data/organizationData.ts | 2 ++ src/models/domain/organization.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/models/data/organizationData.ts b/src/models/data/organizationData.ts index 0f5bc0da95..75212bcf06 100644 --- a/src/models/data/organizationData.ts +++ b/src/models/data/organizationData.ts @@ -18,6 +18,7 @@ export class OrganizationData { usersGetPremium: boolean; seats: number; maxCollections: number; + maxStorageGb?: number; constructor(response: ProfileOrganizationResponse) { this.id = response.id; @@ -34,5 +35,6 @@ export class OrganizationData { this.usersGetPremium = response.usersGetPremium; this.seats = response.seats; this.maxCollections = response.maxCollections; + this.maxStorageGb = response.maxStorageGb; } } diff --git a/src/models/domain/organization.ts b/src/models/domain/organization.ts index c6e136a368..122dc27fe0 100644 --- a/src/models/domain/organization.ts +++ b/src/models/domain/organization.ts @@ -18,6 +18,7 @@ export class Organization { usersGetPremium: boolean; seats: number; maxCollections: number; + maxStorageGb?: number; constructor(obj?: OrganizationData) { if (obj == null) { @@ -38,6 +39,7 @@ export class Organization { this.usersGetPremium = obj.usersGetPremium; this.seats = obj.seats; this.maxCollections = obj.maxCollections; + this.maxStorageGb = obj.maxStorageGb; } get canAccess() { From 257b1d7d7a628b7e8f17dd84efa5a6630fe84d0e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 18 Jul 2018 13:12:48 -0400 Subject: [PATCH 0424/1626] stip guids from analytics page paths --- src/misc/analytics.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/misc/analytics.ts b/src/misc/analytics.ts index 37d665f72d..5812821b2e 100644 --- a/src/misc/analytics.ts +++ b/src/misc/analytics.ts @@ -104,6 +104,15 @@ export class Analytics { if (pagePath.indexOf('!/') === 0 || pagePath.indexOf('#/') === 0) { pagePath = pagePath.substring(1); } - return encodeURIComponent(pagePath); + const pathParts = pagePath.split('/'); + const newPathParts: string[] = []; + pathParts.forEach((p) => { + if (p.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i)) { + newPathParts.push('__guid__'); + } else { + newPathParts.push(p); + } + }); + return encodeURIComponent(newPathParts.join('/')); } } From f4ed6a5566b49e1a7175d931408bbe14e4dc0af7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 18 Jul 2018 15:09:13 -0400 Subject: [PATCH 0425/1626] regular fetch for password check --- src/services/audit.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/audit.service.ts b/src/services/audit.service.ts index 9b7b605765..057be1646e 100644 --- a/src/services/audit.service.ts +++ b/src/services/audit.service.ts @@ -18,7 +18,7 @@ export class AuditService implements AuditServiceAbstraction { const hashStart = hash.substr(0, 5); const hashEnding = hash.substr(5); - const response = await this.apiService.fetch(new Request(PwnedPasswordsApi + hashStart)); + const response = await fetch(new Request(PwnedPasswordsApi + hashStart)); const leakedHashes = await response.text(); const match = leakedHashes.split(/\r?\n/).find((v) => { return v.split(':')[0] === hashEnding; From e555536f2413927508db91affa371560ba633c88 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 18 Jul 2018 17:07:59 -0400 Subject: [PATCH 0426/1626] add org 2fa apis --- src/abstractions/api.service.ts | 7 +++++++ src/services/api.service.ts | 25 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 576befa317..debb78580d 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -181,9 +181,12 @@ export abstract class ApiService { putSettingsDomains: (request: UpdateDomainsRequest) => Promise; getTwoFactorProviders: () => Promise>; + getTwoFactorOrganizationProviders: (organizationId: string) => Promise>; getTwoFactorAuthenticator: (request: PasswordVerificationRequest) => Promise; getTwoFactorEmail: (request: PasswordVerificationRequest) => Promise; getTwoFactorDuo: (request: PasswordVerificationRequest) => Promise; + getTwoFactorOrganizationDuo: (organizationId: string, + request: PasswordVerificationRequest) => Promise; getTwoFactorYubiKey: (request: PasswordVerificationRequest) => Promise; getTwoFactorU2f: (request: PasswordVerificationRequest) => Promise; getTwoFactorRecover: (request: PasswordVerificationRequest) => Promise; @@ -191,9 +194,13 @@ export abstract class ApiService { request: UpdateTwoFactorAuthenticatorRequest) => Promise; putTwoFactorEmail: (request: UpdateTwoFactorEmailRequest) => Promise; putTwoFactorDuo: (request: UpdateTwoFactorDuoRequest) => Promise; + putTwoFactorOrganizationDuo: (organizationId: string, + request: UpdateTwoFactorDuoRequest) => Promise; putTwoFactorYubiKey: (request: UpdateTwoFactorYubioOtpRequest) => Promise; putTwoFactorU2f: (request: UpdateTwoFactorU2fRequest) => Promise; putTwoFactorDisable: (request: TwoFactorProviderRequest) => Promise; + putTwoFactorOrganizationDisable: (organizationId: string, + request: TwoFactorProviderRequest) => Promise; postTwoFactorRecover: (request: TwoFactorRecoveryRequest) => Promise; postTwoFactorEmailSetup: (request: TwoFactorEmailRequest) => Promise; postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 8226d26047..39dc06a8fb 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -560,6 +560,11 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, TwoFactorProviderResponse); } + async getTwoFactorOrganizationProviders(organizationId: string): Promise> { + const r = await this.send('GET', '/organizations/' + organizationId + '/two-factor', null, true, true); + return new ListResponse(r, TwoFactorProviderResponse); + } + async getTwoFactorAuthenticator(request: PasswordVerificationRequest): Promise { const r = await this.send('POST', '/two-factor/get-authenticator', request, true, true); return new TwoFactorAuthenticatorResponse(r); @@ -575,6 +580,13 @@ export class ApiService implements ApiServiceAbstraction { return new TwoFactorDuoResponse(r); } + async getTwoFactorOrganizationDuo(organizationId: string, + request: PasswordVerificationRequest): Promise { + const r = await this.send('POST', '/organizations/' + organizationId + '/two-factor/get-duo', + request, true, true); + return new TwoFactorDuoResponse(r); + } + async getTwoFactorYubiKey(request: PasswordVerificationRequest): Promise { const r = await this.send('POST', '/two-factor/get-yubikey', request, true, true); return new TwoFactorYubiKeyResponse(r); @@ -606,6 +618,12 @@ export class ApiService implements ApiServiceAbstraction { return new TwoFactorDuoResponse(r); } + async putTwoFactorOrganizationDuo(organizationId: string, + request: UpdateTwoFactorDuoRequest): Promise { + const r = await this.send('PUT', '/organizations/' + organizationId + '/two-factor/duo', request, true, true); + return new TwoFactorDuoResponse(r); + } + async putTwoFactorYubiKey(request: UpdateTwoFactorYubioOtpRequest): Promise { const r = await this.send('PUT', '/two-factor/yubikey', request, true, true); return new TwoFactorYubiKeyResponse(r); @@ -621,6 +639,13 @@ export class ApiService implements ApiServiceAbstraction { return new TwoFactorProviderResponse(r); } + async putTwoFactorOrganizationDisable(organizationId: string, + request: TwoFactorProviderRequest): Promise { + const r = await this.send('PUT', '/organizations/' + organizationId + '/two-factor/disable', + request, true, true); + return new TwoFactorProviderResponse(r); + } + postTwoFactorRecover(request: TwoFactorRecoveryRequest): Promise { return this.send('POST', '/two-factor/recover', request, false, false); } From 51ee0b065abb965980e8fe398b568c015e557a2f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 18 Jul 2018 22:47:55 -0400 Subject: [PATCH 0427/1626] make getAllDecrypted synchronous --- src/services/cipher.service.ts | 9 +++------ src/services/collection.service.ts | 8 +++----- src/services/folder.service.ts | 9 +++------ 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index b7b82eece9..053606858c 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -181,13 +181,10 @@ export class CipherService implements CipherServiceAbstraction { throw new Error('No key.'); } - const promises: any[] = []; const ciphers = await this.getAll(); - ciphers.forEach((cipher) => { - promises.push(cipher.decrypt().then((c) => decCiphers.push(c))); - }); - - await Promise.all(promises); + for (let i = 0; i < ciphers.length; i++) { + decCiphers.push(await ciphers[i].decrypt()); + } decCiphers.sort(this.getLocaleSortingFunction()); this.decryptedCipherCache = decCiphers; return this.decryptedCipherCache; diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index 152d91713b..cd259fbc47 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -48,11 +48,9 @@ export class CollectionService implements CollectionServiceAbstraction { return []; } const decCollections: CollectionView[] = []; - const promises: Array> = []; - collections.forEach((collection) => { - promises.push(collection.decrypt().then((c) => decCollections.push(c))); - }); - await Promise.all(promises); + for (let i = 0; i < collections.length; i++) { + decCollections.push(await collections[i].decrypt()); + } return decCollections.sort(Utils.getSortFunction(this.i18nService, 'name')); } diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index c03cb0c298..b37b97786b 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -78,13 +78,10 @@ export class FolderService implements FolderServiceAbstraction { } const decFolders: FolderView[] = []; - const promises: Array> = []; const folders = await this.getAll(); - folders.forEach((folder) => { - promises.push(folder.decrypt().then((f) => decFolders.push(f))); - }); - - await Promise.all(promises); + for (let i = 0; i < folders.length; i++) { + decFolders.push(await folders[i].decrypt()); + } decFolders.sort(Utils.getSortFunction(this.i18nService, 'name')); const noneFolder = new FolderView(); From 4b5e192457efd2ca6f364274a395bc21ce172c70 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 19 Jul 2018 00:01:18 -0400 Subject: [PATCH 0428/1626] ascendo dv csv importer --- src/importers/ascendoCsvImporter.ts | 66 +++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/importers/ascendoCsvImporter.ts diff --git a/src/importers/ascendoCsvImporter.ts b/src/importers/ascendoCsvImporter.ts new file mode 100644 index 0000000000..c007d9f931 --- /dev/null +++ b/src/importers/ascendoCsvImporter.ts @@ -0,0 +1,66 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { SecureNoteView } from '../models/view/secureNoteView'; + +import { CipherType } from '../enums/cipherType'; +import { SecureNoteType } from '../enums/secureNoteType'; + +export class AscendoCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, false); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + if (value.length < 2) { + return; + } + + const cipher = this.initLoginCipher(); + cipher.notes = this.getValueOrDefault(value[value.length - 1]); + cipher.name = this.getValueOrDefault(value[0], '--'); + + if (value.length > 2 && (value.length % 2) === 0) { + for (let i = 0; i < value.length - 2; i += 2) { + const val: string = value[i + 2]; + const field: string = value[i + 1]; + if (this.isNullOrWhitespace(val) || this.isNullOrWhitespace(field)) { + continue; + } + + const fieldLower = field.toLowerCase(); + if (cipher.login.password == null && this.passwordFieldNames.indexOf(fieldLower) > -1) { + cipher.login.password = this.getValueOrDefault(val); + } else if (cipher.login.username == null && + this.usernameFieldNames.indexOf(fieldLower) > -1) { + cipher.login.username = this.getValueOrDefault(val); + } else if ((cipher.login.uris == null || cipher.login.uris.length === 0) && + this.uriFieldNames.indexOf(fieldLower) > -1) { + cipher.login.uris = this.makeUriArray(val); + } else { + this.processKvp(cipher, field, val); + } + } + } + + if (this.isNullOrWhitespace(cipher.login.username) && this.isNullOrWhitespace(cipher.login.password) && + (cipher.login.uris == null || cipher.login.uris.length === 0)) { + cipher.type = CipherType.SecureNote; + cipher.secureNote = new SecureNoteView(); + cipher.secureNote.type = SecureNoteType.Generic; + } + + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} From 4750a64ece77a53c36b2cca97a759e0598a4873c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 19 Jul 2018 08:05:45 -0400 Subject: [PATCH 0429/1626] pasword boss importer. convert to note helper --- src/importers/ascendoCsvImporter.ts | 13 +-- src/importers/baseImporter.ts | 13 +++ src/importers/clipperzHtmlImporter.ts | 12 +-- src/importers/onepasswordWinCsvImporter.ts | 11 +-- src/importers/passwordBossJsonImporter.ts | 93 ++++++++++++++++++++++ 5 files changed, 109 insertions(+), 33 deletions(-) create mode 100644 src/importers/passwordBossJsonImporter.ts diff --git a/src/importers/ascendoCsvImporter.ts b/src/importers/ascendoCsvImporter.ts index c007d9f931..52b9f0808b 100644 --- a/src/importers/ascendoCsvImporter.ts +++ b/src/importers/ascendoCsvImporter.ts @@ -3,11 +3,6 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; -import { SecureNoteView } from '../models/view/secureNoteView'; - -import { CipherType } from '../enums/cipherType'; -import { SecureNoteType } from '../enums/secureNoteType'; - export class AscendoCsvImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { const result = new ImportResult(); @@ -49,13 +44,7 @@ export class AscendoCsvImporter extends BaseImporter implements Importer { } } - if (this.isNullOrWhitespace(cipher.login.username) && this.isNullOrWhitespace(cipher.login.password) && - (cipher.login.uris == null || cipher.login.uris.length === 0)) { - cipher.type = CipherType.SecureNote; - cipher.secureNote = new SecureNoteView(); - cipher.secureNote.type = SecureNoteType.Generic; - } - + this.convertToNoteIfNeeded(cipher); this.cleanupCipher(cipher); result.ciphers.push(cipher); }); diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index c00564c315..291d558c5e 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -7,12 +7,15 @@ import { CollectionView } from '../models/view/collectionView'; import { LoginUriView } from '../models/view/loginUriView'; import { Utils } from '../misc/utils'; + import { FieldView } from '../models/view/fieldView'; import { FolderView } from '../models/view/folderView'; import { LoginView } from '../models/view/loginView'; +import { SecureNoteView } from '../models/view/secureNoteView'; import { CipherType } from '../enums/cipherType'; import { FieldType } from '../enums/fieldType'; +import { SecureNoteType } from '../enums/secureNoteType'; export abstract class BaseImporter { organization = false; @@ -313,4 +316,14 @@ export abstract class BaseImporter { result.folderRelationships.push([result.ciphers.length, folderIndex]); } } + + protected convertToNoteIfNeeded(cipher: CipherView) { + if (cipher.type === CipherType.Login && this.isNullOrWhitespace(cipher.login.username) && + this.isNullOrWhitespace(cipher.login.password) && + (cipher.login.uris == null || cipher.login.uris.length === 0)) { + cipher.type = CipherType.SecureNote; + cipher.secureNote = new SecureNoteView(); + cipher.secureNote.type = SecureNoteType.Generic; + } + } } diff --git a/src/importers/clipperzHtmlImporter.ts b/src/importers/clipperzHtmlImporter.ts index 5e8d78d50b..0443114811 100644 --- a/src/importers/clipperzHtmlImporter.ts +++ b/src/importers/clipperzHtmlImporter.ts @@ -2,10 +2,6 @@ import { BaseImporter } from './baseImporter'; import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; -import { SecureNoteView } from '../models/view/secureNoteView'; - -import { CipherType } from '../enums/cipherType'; -import { SecureNoteType } from '../enums/secureNoteType'; export class ClipperzHtmlImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { @@ -72,13 +68,7 @@ export class ClipperzHtmlImporter extends BaseImporter implements Importer { } } - if (this.isNullOrWhitespace(cipher.login.username) && this.isNullOrWhitespace(cipher.login.password) && - (cipher.login.uris == null || cipher.login.uris.length === 0)) { - cipher.type = CipherType.SecureNote; - cipher.secureNote = new SecureNoteView(); - cipher.secureNote.type = SecureNoteType.Generic; - } - + this.convertToNoteIfNeeded(cipher); this.cleanupCipher(cipher); result.ciphers.push(cipher); }); diff --git a/src/importers/onepasswordWinCsvImporter.ts b/src/importers/onepasswordWinCsvImporter.ts index eaed7dd83a..5b1a433704 100644 --- a/src/importers/onepasswordWinCsvImporter.ts +++ b/src/importers/onepasswordWinCsvImporter.ts @@ -3,10 +3,7 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; -import { SecureNoteView } from '../models/view/secureNoteView'; - import { CipherType } from '../enums/cipherType'; -import { SecureNoteType } from '../enums/secureNoteType'; import { CardView } from '../models/view'; const IgnoredProperties = ['ainfo', 'autosubmit', 'notesPlain', 'ps', 'scope', 'tags', 'title', 'uuid']; @@ -89,14 +86,8 @@ export class OnePasswordWinCsvImporter extends BaseImporter implements Importer this.isNullOrWhitespace(cipher.login.username) && altUsername.indexOf('://') === -1) { cipher.login.username = altUsername; } - if (cipher.type === CipherType.Login && this.isNullOrWhitespace(cipher.login.username) && - this.isNullOrWhitespace(cipher.login.password) && - (cipher.login.uris == null || cipher.login.uris.length === 0)) { - cipher.type = CipherType.SecureNote; - cipher.secureNote = new SecureNoteView(); - cipher.secureNote.type = SecureNoteType.Generic; - } + this.convertToNoteIfNeeded(cipher); this.cleanupCipher(cipher); result.ciphers.push(cipher); }); diff --git a/src/importers/passwordBossJsonImporter.ts b/src/importers/passwordBossJsonImporter.ts new file mode 100644 index 0000000000..843fcdfb1b --- /dev/null +++ b/src/importers/passwordBossJsonImporter.ts @@ -0,0 +1,93 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CardView } from '../models/view/cardView'; + +import { CipherType } from '../enums/cipherType'; + +export class PasswordBossJsonImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = JSON.parse(data); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(value.name, '--'); + cipher.login.uris = this.makeUriArray(value.login_url); + + if (value.identifiers == null) { + return; + } + + if (!this.isNullOrWhitespace(value.identifiers.notes)) { + cipher.notes = value.identifiers.notes.split('\\r\\n').join('\n').split('\\n').join('\n'); + } + + if (value.type === 'CreditCard') { + cipher.card = new CardView(); + cipher.type = CipherType.Card; + } + + for (const property in value.identifiers) { + if (!value.identifiers.hasOwnProperty(property)) { + continue; + } + const valObj = value.identifiers[property]; + const val = valObj != null ? valObj.toString() : null; + if (this.isNullOrWhitespace(val) || property === 'notes' || property === 'ignoreItemInSecurityScore') { + continue; + } + + if (cipher.type === CipherType.Card) { + if (property === 'cardNumber') { + cipher.card.number = val; + cipher.card.brand = this.getCardBrand(val); + continue; + } else if (property === 'nameOnCard') { + cipher.card.cardholderName = val; + continue; + } else if (property === 'security_code') { + cipher.card.code = val; + continue; + } else if (property === 'expires') { + try { + const expDate = new Date(val); + cipher.card.expYear = expDate.getFullYear().toString(); + cipher.card.expMonth = (expDate.getMonth() + 1).toString(); + } catch { } + continue; + } else if (property === 'cardType') { + continue; + } + } else { + if (property === 'username') { + cipher.login.username = val; + continue; + } else if (property === 'password') { + cipher.login.password = val; + continue; + } else if ((cipher.login.uris == null || cipher.login.uris.length === 0) && + this.uriFieldNames.indexOf(property) > -1) { + cipher.login.uris = this.makeUriArray(val); + continue; + } + } + + this.processKvp(cipher, property, val); + } + + this.convertToNoteIfNeeded(cipher); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} From 3f329ca6132ef03b562f0ae67d047daed11cd099 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 19 Jul 2018 08:30:07 -0400 Subject: [PATCH 0430/1626] fix implicit any --- src/importers/passwordBossJsonImporter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/importers/passwordBossJsonImporter.ts b/src/importers/passwordBossJsonImporter.ts index 843fcdfb1b..2f6cf8f416 100644 --- a/src/importers/passwordBossJsonImporter.ts +++ b/src/importers/passwordBossJsonImporter.ts @@ -16,7 +16,7 @@ export class PasswordBossJsonImporter extends BaseImporter implements Importer { return result; } - results.forEach((value) => { + results.forEach((value: any) => { const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(value.name, '--'); cipher.login.uris = this.makeUriArray(value.login_url); From 8149d7877d97c9c8a945ae934ab9c6a1fe74fd63 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 19 Jul 2018 15:12:58 -0400 Subject: [PATCH 0431/1626] zoho csv importer --- src/importers/baseImporter.ts | 8 +++- src/importers/zohoVaultCsvImporter.ts | 66 +++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/importers/zohoVaultCsvImporter.ts diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index 291d558c5e..d7ee9d5f72 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -105,6 +105,9 @@ export abstract class BaseImporter { if (typeof uri === 'string') { const loginUri = new LoginUriView(); loginUri.uri = this.fixUri(uri); + if (this.isNullOrWhitespace(loginUri.uri)) { + return null; + } loginUri.match = null; return [loginUri]; } @@ -114,10 +117,13 @@ export abstract class BaseImporter { uri.forEach((u) => { const loginUri = new LoginUriView(); loginUri.uri = this.fixUri(u); + if (this.isNullOrWhitespace(loginUri.uri)) { + return; + } loginUri.match = null; returnArr.push(loginUri); }); - return returnArr; + return returnArr.length === 0 ? null : returnArr; } return null; diff --git a/src/importers/zohoVaultCsvImporter.ts b/src/importers/zohoVaultCsvImporter.ts new file mode 100644 index 0000000000..f2cc12d1c1 --- /dev/null +++ b/src/importers/zohoVaultCsvImporter.ts @@ -0,0 +1,66 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; +import { CipherView } from '../models/view'; + +export class ZohoVaultCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + if (this.isNullOrWhitespace(value['Secret Name'])) { + return; + } + this.processFolder(result, this.getValueOrDefault(value.ChamberName)); + const cipher = this.initLoginCipher(); + cipher.favorite = this.getValueOrDefault(value.Favorite, '0') === '1'; + cipher.notes = this.getValueOrDefault(value.Notes); + cipher.name = this.getValueOrDefault(value['Secret Name'], '--'); + cipher.login.uris = this.makeUriArray(value['Secret URL']); + this.parseData(cipher, value.SecretData); + this.parseData(cipher, value.CustomData); + this.convertToNoteIfNeeded(cipher); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + if (this.organization) { + this.moveFoldersToCollections(result); + } + + result.success = true; + return result; + } + + private parseData(cipher: CipherView, data: string) { + if (this.isNullOrWhitespace(data)) { + return; + } + const dataLines = this.splitNewLine(data); + dataLines.forEach((line) => { + const delimPosition = line.indexOf(':'); + if (delimPosition < 0) { + return; + } + const field = line.substring(0, delimPosition); + const value = line.length > delimPosition ? line.substring(delimPosition + 1) : null; + if (this.isNullOrWhitespace(field) || this.isNullOrWhitespace(value) || field === 'SecretType') { + return; + } + const fieldLower = field.toLowerCase(); + if (cipher.login.username == null && this.usernameFieldNames.indexOf(fieldLower) > -1) { + cipher.login.username = value; + } else if (cipher.login.password == null && this.passwordFieldNames.indexOf(fieldLower) > -1) { + cipher.login.password = value; + } else { + this.processKvp(cipher, field, value); + } + }); + } +} From cc8dd89b2b095028187edbbc44785307a373db22 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 20 Jul 2018 13:01:48 -0400 Subject: [PATCH 0432/1626] fix delete account endpoint --- src/services/api.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 39dc06a8fb..091c58a663 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -213,7 +213,7 @@ export class ApiService implements ApiServiceAbstraction { } deleteAccount(request: PasswordVerificationRequest): Promise { - return this.send('DELETE', '/accounts/delete', request, true, false); + return this.send('DELETE', '/accounts', request, true, false); } async getAccountRevisionDate(): Promise { From b5353551a8a8eef6bbfe38b64456671f944ed27e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 21 Jul 2018 00:32:41 -0400 Subject: [PATCH 0433/1626] null check on u2f challenge --- src/models/response/twoFactorU2fResponse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/response/twoFactorU2fResponse.ts b/src/models/response/twoFactorU2fResponse.ts index 334b7994e8..7ab62cc627 100644 --- a/src/models/response/twoFactorU2fResponse.ts +++ b/src/models/response/twoFactorU2fResponse.ts @@ -4,7 +4,7 @@ export class TwoFactorU2fResponse { constructor(response: any) { this.enabled = response.Enabled; - this.challenge = new ChallengeResponse(response.Challenge); + this.challenge = response.Challenge == null ? null : new ChallengeResponse(response.Challenge); } } From 2fe65e27e2664ebef92bd8e63129d5097bacc180 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 21 Jul 2018 08:21:08 -0400 Subject: [PATCH 0434/1626] normalize new lines and trim csv data --- src/importers/baseImporter.ts | 1 + src/importers/lastpassCsvImporter.ts | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index d7ee9d5f72..ca642666cf 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -72,6 +72,7 @@ export abstract class BaseImporter { } protected parseCsv(data: string, header: boolean): any[] { + data = this.splitNewLine(data).join('\n').trim(); const result = papa.parse(data, { header: header, encoding: 'UTF-8', diff --git a/src/importers/lastpassCsvImporter.ts b/src/importers/lastpassCsvImporter.ts index cdaa0629aa..acb839c589 100644 --- a/src/importers/lastpassCsvImporter.ts +++ b/src/importers/lastpassCsvImporter.ts @@ -39,11 +39,6 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { } const cipher = this.buildBaseCipher(value); - if (cipher.name === '--' && results.length > 2 && index >= (results.length - 2)) { - // LastPass file traditionally has two empty lines at the end - return; - } - if (cipher.type === CipherType.Login) { cipher.notes = this.getValueOrDefault(value.extra); cipher.login = new LoginView(); From 57ce9d5315b6495c4359714f1a2dbc279fe10e8b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 21 Jul 2018 09:59:37 -0400 Subject: [PATCH 0435/1626] null check on u2f data --- src/misc/u2f.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/misc/u2f.ts b/src/misc/u2f.ts index cb1696c62f..0c178bf62d 100644 --- a/src/misc/u2f.ts +++ b/src/misc/u2f.ts @@ -63,7 +63,8 @@ export class U2f { } private validMessage(event: any) { - if (!event.origin || event.origin === '' || event.origin !== (this.connectorLink as any).origin) { + if (!event.origin || event.origin === '' || + event.origin !== (this.connectorLink as any).origin || !event.data) { return false; } From 7b89ad6bf78d5f619b659e25e181befd79cd8517 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 21 Jul 2018 14:07:06 -0400 Subject: [PATCH 0436/1626] check event.data type. some debug logging --- src/misc/u2f.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/misc/u2f.ts b/src/misc/u2f.ts index 0c178bf62d..6186df60d4 100644 --- a/src/misc/u2f.ts +++ b/src/misc/u2f.ts @@ -47,6 +47,8 @@ export class U2f { } private parseMessage(event: any) { + console.log('got u2f event'); + console.log(event); if (!this.validMessage(event)) { this.errorCallback('Invalid message.'); return; @@ -63,8 +65,8 @@ export class U2f { } private validMessage(event: any) { - if (!event.origin || event.origin === '' || - event.origin !== (this.connectorLink as any).origin || !event.data) { + if (event.origin == null || event.origin === '' || event.origin !== (this.connectorLink as any).origin || + event.data == null || typeof (event.data) !== 'string') { return false; } From 240e5ba5ab6fe072db9aa9d7acac441a8daaf6cd Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 21 Jul 2018 14:38:21 -0400 Subject: [PATCH 0437/1626] parse function --- src/misc/u2f.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/misc/u2f.ts b/src/misc/u2f.ts index 6186df60d4..4e215fa51a 100644 --- a/src/misc/u2f.ts +++ b/src/misc/u2f.ts @@ -1,6 +1,7 @@ export class U2f { private iframe: HTMLIFrameElement = null; private connectorLink: HTMLAnchorElement; + private parseFunction = this.parseMessage.bind(this); constructor(private win: Window, private webVaultUrl: string, private successCallback: Function, private errorCallback: Function, private infoCallback: Function) { @@ -17,7 +18,7 @@ export class U2f { this.iframe = this.win.document.getElementById('u2f_iframe') as HTMLIFrameElement; this.iframe.src = this.connectorLink.href; - this.win.addEventListener('message', (e) => this.parseMessage(e), false); + this.win.addEventListener('message', this.parseFunction, false); } stop() { @@ -43,14 +44,11 @@ export class U2f { } cleanup() { - this.win.removeEventListener('message', (e) => this.parseMessage(e), false); + this.win.removeEventListener('message', this.parseFunction, false); } - private parseMessage(event: any) { - console.log('got u2f event'); - console.log(event); + private parseMessage(event: MessageEvent) { if (!this.validMessage(event)) { - this.errorCallback('Invalid message.'); return; } @@ -64,7 +62,7 @@ export class U2f { } } - private validMessage(event: any) { + private validMessage(event: MessageEvent) { if (event.origin == null || event.origin === '' || event.origin !== (this.connectorLink as any).origin || event.data == null || typeof (event.data) !== 'string') { return false; From 790e50f2b68b2ed956533f46ded6b393a5dd96b5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 21 Jul 2018 15:35:17 -0400 Subject: [PATCH 0438/1626] check SafariExtension instead of safari --- src/angular/components/two-factor.component.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 1e676f5be1..39ce3578b9 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -7,8 +7,7 @@ import { Router } from '@angular/router'; import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; -import { TwoFactorOptionsComponent } from './two-factor-options.component'; - +import { DeviceType } from '../../enums/deviceType'; import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; import { TwoFactorEmailRequest } from '../../models/request/twoFactorEmailRequest'; @@ -18,7 +17,6 @@ import { AuthService } from '../../abstractions/auth.service'; import { EnvironmentService } from '../../abstractions/environment.service'; import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; -import { SyncService } from '../../abstractions/sync.service'; import { TwoFactorProviders } from '../../services/auth.service'; @@ -117,7 +115,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { break; case TwoFactorProviderType.Duo: case TwoFactorProviderType.OrganizationDuo: - if (this.platformUtilsService.isSafari()) { + if (this.platformUtilsService.getDevice() === DeviceType.SafariExtension) { break; } From 06682e267259dcc4ddbf813550451ea620ad9b98 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 23 Jul 2018 11:23:20 -0400 Subject: [PATCH 0439/1626] splash id importer --- src/importers/splashIdCsvImporter.ts | 57 ++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/importers/splashIdCsvImporter.ts diff --git a/src/importers/splashIdCsvImporter.ts b/src/importers/splashIdCsvImporter.ts new file mode 100644 index 0000000000..39dd76aea4 --- /dev/null +++ b/src/importers/splashIdCsvImporter.ts @@ -0,0 +1,57 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; +import { CipherView } from '../models/view/cipherView'; + +export class SplashIdCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, false); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + if (value.length < 3) { + return; + } + + this.processFolder(result, this.getValueOrDefault(value[value.length - 1])); + const cipher = this.initLoginCipher(); + cipher.notes = this.getValueOrDefault(value[value.length - 2], ''); + cipher.name = this.getValueOrDefault(value[1], '--'); + + if (value[0] === 'Web Logins' || value[0] === 'Servers' || value[0] === 'Email Accounts') { + cipher.login.username = this.getValueOrDefault(value[2]); + cipher.login.password = this.getValueOrDefault(value[3]); + cipher.login.uris = this.makeUriArray(value[4]); + this.parseFieldsToNotes(cipher, 5, value); + } else { + this.parseFieldsToNotes(cipher, 2, value); + } + + this.convertToNoteIfNeeded(cipher); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + if (this.organization) { + this.moveFoldersToCollections(result); + } + + result.success = true; + return result; + } + + private parseFieldsToNotes(cipher: CipherView, startIndex: number, value: any) { + // last 3 rows do not get parsed + for (let i = startIndex; i < value.length - 3; i++) { + if (this.isNullOrWhitespace(value[i])) { + continue; + } + cipher.notes += (value[i] + '\n'); + } + } +} From e5d060c80b6c6ca4f863534ec73ed410bb30a21e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 23 Jul 2018 11:41:12 -0400 Subject: [PATCH 0440/1626] passkeep csv importer --- src/importers/passkeepCsvImporter.ts | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/importers/passkeepCsvImporter.ts diff --git a/src/importers/passkeepCsvImporter.ts b/src/importers/passkeepCsvImporter.ts new file mode 100644 index 0000000000..b51ffdaec5 --- /dev/null +++ b/src/importers/passkeepCsvImporter.ts @@ -0,0 +1,39 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +export class PassKeepCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + this.processFolder(result, this.getValue('category', value)); + const cipher = this.initLoginCipher(); + cipher.notes = this.getValue('description', value); + cipher.name = this.getValueOrDefault(this.getValue('title', value), '--'); + cipher.login.username = this.getValue('username', value); + cipher.login.password = this.getValue('password', value); + cipher.login.uris = this.makeUriArray(this.getValue('site', value)); + this.processKvp(cipher, 'Password 2', this.getValue('password2', value)); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + if (this.organization) { + this.moveFoldersToCollections(result); + } + + result.success = true; + return result; + } + + private getValue(key: string, value: any) { + return this.getValueOrDefault(value[key], this.getValueOrDefault(value[(' ' + key)])); + } +} From 3a34d3b174a1cad8eac9fe950c814336902b4cab Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 23 Jul 2018 12:04:34 -0400 Subject: [PATCH 0441/1626] gnome json importer --- src/importers/gnomeJsonImporter.ts | 60 ++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/importers/gnomeJsonImporter.ts diff --git a/src/importers/gnomeJsonImporter.ts b/src/importers/gnomeJsonImporter.ts new file mode 100644 index 0000000000..be62e8cfbe --- /dev/null +++ b/src/importers/gnomeJsonImporter.ts @@ -0,0 +1,60 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +export class GnomeJsonImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = JSON.parse(data); + if (results == null || Object.keys(results).length === 0) { + result.success = false; + return result; + } + + for (const keyRing in results) { + if (!results.hasOwnProperty(keyRing) || this.isNullOrWhitespace(keyRing) || + results[keyRing].length === 0) { + continue; + } + + results[keyRing].forEach((value: any) => { + if (this.isNullOrWhitespace(value.display_name) || value.display_name.indexOf('http') !== 0) { + return; + } + + this.processFolder(result, keyRing); + const cipher = this.initLoginCipher(); + cipher.name = value.display_name.replace('http://', '').replace('https://', ''); + if (cipher.name.length > 30) { + cipher.name = cipher.name.substring(0, 30); + } + cipher.login.password = this.getValueOrDefault(value.secret); + cipher.login.uris = this.makeUriArray(value.display_name); + + if (value.attributes != null) { + cipher.login.username = value.attributes != null ? + this.getValueOrDefault(value.attributes.username_value) : null; + for (const attr in value.attributes) { + if (!value.attributes.hasOwnProperty(attr) || attr === 'username_value' || + attr === 'xdg:schema') { + continue; + } + this.processKvp(cipher, attr, value.attributes[attr]); + } + } + + this.convertToNoteIfNeeded(cipher); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + } + + if (this.organization) { + this.moveFoldersToCollections(result); + } + + result.success = true; + return result; + } +} From 04014a8e78568f925cd6f44917562718f2568f57 Mon Sep 17 00:00:00 2001 From: Fred Cox Date: Mon, 23 Jul 2018 21:23:30 +0300 Subject: [PATCH 0442/1626] Add sequentialize to prevent parralel loading of cipher keys (#7) * Add sequentialize to prevent parralel loading of cipher keys Massively improves start up performance of extensions * Add tests for sequentialize * Fix sequentialize as it was caching calls for all instances together * Add sequentialize to the functions that have internal caches * Adding sequentialize to getOrgKeys makes big performance difference * Update cipher.service.ts * Update collection.service.ts * Update folder.service.ts --- spec/common/misc/sequentialize.spec.ts | 141 +++++++++++++++++++++++++ src/misc/sequentialize.ts | 52 +++++++++ src/models/domain/cipherString.ts | 2 + src/services/crypto.service.ts | 2 + 4 files changed, 197 insertions(+) create mode 100644 spec/common/misc/sequentialize.spec.ts create mode 100644 src/misc/sequentialize.ts diff --git a/spec/common/misc/sequentialize.spec.ts b/spec/common/misc/sequentialize.spec.ts new file mode 100644 index 0000000000..33c8e958e4 --- /dev/null +++ b/spec/common/misc/sequentialize.spec.ts @@ -0,0 +1,141 @@ +import { sequentialize } from '../../../src/misc/sequentialize'; + +describe('sequentialize decorator', () => { + it('should call the function once', async () => { + const foo = new Foo(); + const promises = []; + for (let i = 0; i < 10; i++) { + promises.push(foo.bar(1)); + } + await Promise.all(promises); + + expect(foo.calls).toBe(1); + }); + + it('should call the function once for each instance of the object', async () => { + const foo = new Foo(); + const foo2 = new Foo(); + const promises = []; + for (let i = 0; i < 10; i++) { + promises.push(foo.bar(1)); + promises.push(foo2.bar(1)); + } + await Promise.all(promises); + + expect(foo.calls).toBe(1); + expect(foo2.calls).toBe(1); + }); + + it('should call the function once with key function', async () => { + const foo = new Foo(); + const promises = []; + for (let i = 0; i < 10; i++) { + promises.push(foo.baz(1)); + } + await Promise.all(promises); + + expect(foo.calls).toBe(1); + }); + + it('should call the function again when already resolved', async () => { + const foo = new Foo(); + await foo.bar(1); + expect(foo.calls).toBe(1); + await foo.bar(1); + expect(foo.calls).toBe(2); + }); + + it('should call the function again when already resolved with a key function', async () => { + const foo = new Foo(); + await foo.baz(1); + expect(foo.calls).toBe(1); + await foo.baz(1); + expect(foo.calls).toBe(2); + }); + + it('should call the function for each argument', async () => { + const foo = new Foo(); + await Promise.all([ + foo.bar(1), + foo.bar(1), + foo.bar(2), + foo.bar(2), + foo.bar(3), + foo.bar(3), + ]); + expect(foo.calls).toBe(3); + }); + + it('should call the function for each argument with key function', async () => { + const foo = new Foo(); + await Promise.all([ + foo.baz(1), + foo.baz(1), + foo.baz(2), + foo.baz(2), + foo.baz(3), + foo.baz(3), + ]); + expect(foo.calls).toBe(3); + }); + + it('should return correct result for each call', async () => { + const foo = new Foo(); + const allRes = []; + + await Promise.all([ + foo.bar(1).then((res) => allRes.push(res)), + foo.bar(1).then((res) => allRes.push(res)), + foo.bar(2).then((res) => allRes.push(res)), + foo.bar(2).then((res) => allRes.push(res)), + foo.bar(3).then((res) => allRes.push(res)), + foo.bar(3).then((res) => allRes.push(res)), + ]); + expect(foo.calls).toBe(3); + expect(allRes.length).toBe(6); + allRes.sort(); + expect(allRes).toEqual([2, 2, 4, 4, 6, 6]); + }); + + it('should return correct result for each call with key function', async () => { + const foo = new Foo(); + const allRes = []; + + await Promise.all([ + foo.baz(1).then((res) => allRes.push(res)), + foo.baz(1).then((res) => allRes.push(res)), + foo.baz(2).then((res) => allRes.push(res)), + foo.baz(2).then((res) => allRes.push(res)), + foo.baz(3).then((res) => allRes.push(res)), + foo.baz(3).then((res) => allRes.push(res)), + ]); + expect(foo.calls).toBe(3); + expect(allRes.length).toBe(6); + allRes.sort(); + expect(allRes).toEqual([3, 3, 6, 6, 9, 9]); + }); +}); + +class Foo { + calls = 0; + + @sequentialize() + bar(a) { + this.calls++; + return new Promise((res) => { + setTimeout(() => { + res(a * 2); + }, Math.random() * 100); + }); + } + + @sequentialize((args) => args[0]) + baz(a) { + this.calls++; + return new Promise((res) => { + setTimeout(() => { + res(a * 3); + }, Math.random() * 100); + }); + } +} diff --git a/src/misc/sequentialize.ts b/src/misc/sequentialize.ts new file mode 100644 index 0000000000..221cadd118 --- /dev/null +++ b/src/misc/sequentialize.ts @@ -0,0 +1,52 @@ +/** + * Use as a Decorator on async functions, it will prevent multiple 'active' calls as the same time + * + * If a promise was returned from a previous call to this function, that hasn't yet resolved it will + * be returned, instead of calling the original function again + * + * Results are not cached, once the promise has returned, the next call will result in a fresh call + */ +export function sequentialize(key: (args: any[]) => string = JSON.stringify) { + return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => { + const originalMethod: () => Promise = descriptor.value; + + const caches = new Map>>(); + const getCache = (obj: any) => { + let cache = caches.get(obj); + if (cache) { + return cache; + } + cache = new Map>(); + caches.set(obj, cache); + + return cache; + }; + + return { + value: function(...args: any[]) { + const argsKey = key(args); + const cache = getCache(this); + + let res = cache.get(argsKey); + if (res) { + return res; + } + + res = originalMethod.apply(this, args) + .then((val: any) => { + cache.delete(argsKey); + + return val; + }) + .catch((err: any) => { + cache.delete(argsKey); + + throw err; + }); + cache.set(argsKey, res); + + return res; + }, + }; + }; +} diff --git a/src/models/domain/cipherString.ts b/src/models/domain/cipherString.ts index 6d49d1724e..fdc4c9098c 100644 --- a/src/models/domain/cipherString.ts +++ b/src/models/domain/cipherString.ts @@ -2,6 +2,7 @@ import { EncryptionType } from '../../enums/encryptionType'; import { CryptoService } from '../../abstractions/crypto.service'; +import { sequentialize } from '../../misc/sequentialize'; import { Utils } from '../../misc/utils'; export class CipherString { @@ -89,6 +90,7 @@ export class CipherString { } } + @sequentialize((args) => args[0]) async decrypt(orgId: string): Promise { if (this.decryptedValue) { return Promise.resolve(this.decryptedValue); diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index effb5825c8..54adfc7945 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -11,6 +11,7 @@ import { StorageService } from '../abstractions/storage.service'; import { ConstantsService } from './constants.service'; +import { sequentialize } from '../misc/sequentialize'; import { Utils } from '../misc/utils'; const Keys = { @@ -164,6 +165,7 @@ export class CryptoService implements CryptoServiceAbstraction { return this.privateKey; } + @sequentialize() async getOrgKeys(): Promise> { if (this.orgKeys != null && this.orgKeys.size > 0) { return this.orgKeys; From c7e8f1d13f6268c290655d68b57349131af0bbe9 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 23 Jul 2018 14:24:27 -0400 Subject: [PATCH 0443/1626] Revert "make getAllDecrypted synchronous" This reverts commit 51ee0b065abb965980e8fe398b568c015e557a2f. --- src/services/cipher.service.ts | 9 ++++++--- src/services/collection.service.ts | 8 +++++--- src/services/folder.service.ts | 9 ++++++--- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 053606858c..b7b82eece9 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -181,10 +181,13 @@ export class CipherService implements CipherServiceAbstraction { throw new Error('No key.'); } + const promises: any[] = []; const ciphers = await this.getAll(); - for (let i = 0; i < ciphers.length; i++) { - decCiphers.push(await ciphers[i].decrypt()); - } + ciphers.forEach((cipher) => { + promises.push(cipher.decrypt().then((c) => decCiphers.push(c))); + }); + + await Promise.all(promises); decCiphers.sort(this.getLocaleSortingFunction()); this.decryptedCipherCache = decCiphers; return this.decryptedCipherCache; diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index cd259fbc47..152d91713b 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -48,9 +48,11 @@ export class CollectionService implements CollectionServiceAbstraction { return []; } const decCollections: CollectionView[] = []; - for (let i = 0; i < collections.length; i++) { - decCollections.push(await collections[i].decrypt()); - } + const promises: Array> = []; + collections.forEach((collection) => { + promises.push(collection.decrypt().then((c) => decCollections.push(c))); + }); + await Promise.all(promises); return decCollections.sort(Utils.getSortFunction(this.i18nService, 'name')); } diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index b37b97786b..c03cb0c298 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -78,10 +78,13 @@ export class FolderService implements FolderServiceAbstraction { } const decFolders: FolderView[] = []; + const promises: Array> = []; const folders = await this.getAll(); - for (let i = 0; i < folders.length; i++) { - decFolders.push(await folders[i].decrypt()); - } + folders.forEach((folder) => { + promises.push(folder.decrypt().then((f) => decFolders.push(f))); + }); + + await Promise.all(promises); decFolders.sort(Utils.getSortFunction(this.i18nService, 'name')); const noneFolder = new FolderView(); From 003c730eb1f959d30d1b83a5101a1c0530d9f369 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 23 Jul 2018 14:42:37 -0400 Subject: [PATCH 0444/1626] sequentialize updates --- src/misc/sequentialize.ts | 27 ++++++++++----------------- src/models/domain/cipherString.ts | 2 -- src/services/crypto.service.ts | 1 + 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/src/misc/sequentialize.ts b/src/misc/sequentialize.ts index 221cadd118..10925c5951 100644 --- a/src/misc/sequentialize.ts +++ b/src/misc/sequentialize.ts @@ -9,42 +9,35 @@ export function sequentialize(key: (args: any[]) => string = JSON.stringify) { return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => { const originalMethod: () => Promise = descriptor.value; - const caches = new Map>>(); const getCache = (obj: any) => { let cache = caches.get(obj); - if (cache) { + if (cache != null) { return cache; } cache = new Map>(); caches.set(obj, cache); - return cache; }; return { - value: function(...args: any[]) { + value: (...args: any[]) => { const argsKey = key(args); const cache = getCache(this); - let res = cache.get(argsKey); - if (res) { + if (res != null) { return res; } - res = originalMethod.apply(this, args) - .then((val: any) => { - cache.delete(argsKey); + res = originalMethod.apply(this, args).then((val: any) => { + cache.delete(argsKey); + return val; + }).catch((err: any) => { + cache.delete(argsKey); + throw err; + }); - return val; - }) - .catch((err: any) => { - cache.delete(argsKey); - - throw err; - }); cache.set(argsKey, res); - return res; }, }; diff --git a/src/models/domain/cipherString.ts b/src/models/domain/cipherString.ts index fdc4c9098c..6d49d1724e 100644 --- a/src/models/domain/cipherString.ts +++ b/src/models/domain/cipherString.ts @@ -2,7 +2,6 @@ import { EncryptionType } from '../../enums/encryptionType'; import { CryptoService } from '../../abstractions/crypto.service'; -import { sequentialize } from '../../misc/sequentialize'; import { Utils } from '../../misc/utils'; export class CipherString { @@ -90,7 +89,6 @@ export class CipherString { } } - @sequentialize((args) => args[0]) async decrypt(orgId: string): Promise { if (this.decryptedValue) { return Promise.resolve(this.decryptedValue); diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 54adfc7945..5315621992 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -104,6 +104,7 @@ export class CryptoService implements CryptoServiceAbstraction { return this.storageService.get(Keys.keyHash); } + @sequentialize() async getEncKey(): Promise { if (this.encKey != null) { return this.encKey; From 8e586437e0780ddf9683e8edd158a7dcc812947a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 23 Jul 2018 14:47:28 -0400 Subject: [PATCH 0445/1626] sequentialize fixes --- src/misc/sequentialize.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/misc/sequentialize.ts b/src/misc/sequentialize.ts index 10925c5951..8b946b6547 100644 --- a/src/misc/sequentialize.ts +++ b/src/misc/sequentialize.ts @@ -21,15 +21,15 @@ export function sequentialize(key: (args: any[]) => string = JSON.stringify) { }; return { - value: (...args: any[]) => { + value: function(...args: any[]) { const argsKey = key(args); const cache = getCache(this); - let res = cache.get(argsKey); - if (res != null) { - return res; + let response = cache.get(argsKey); + if (response != null) { + return response; } - res = originalMethod.apply(this, args).then((val: any) => { + response = originalMethod.apply(this, args).then((val: any) => { cache.delete(argsKey); return val; }).catch((err: any) => { @@ -37,8 +37,8 @@ export function sequentialize(key: (args: any[]) => string = JSON.stringify) { throw err; }); - cache.set(argsKey, res); - return res; + cache.set(argsKey, response); + return response; }, }; }; From 61d2040518a02ddb9b8edfb2df6e13db186b1ef7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 23 Jul 2018 15:12:32 -0400 Subject: [PATCH 0446/1626] sequentialize updates --- src/misc/sequentialize.ts | 13 +++++++------ src/services/crypto.service.ts | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/misc/sequentialize.ts b/src/misc/sequentialize.ts index 8b946b6547..46b665f64b 100644 --- a/src/misc/sequentialize.ts +++ b/src/misc/sequentialize.ts @@ -6,10 +6,11 @@ * * Results are not cached, once the promise has returned, the next call will result in a fresh call */ -export function sequentialize(key: (args: any[]) => string = JSON.stringify) { +export function sequentialize(cacheKey: (args: any[]) => string) { return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => { const originalMethod: () => Promise = descriptor.value; const caches = new Map>>(); + const getCache = (obj: any) => { let cache = caches.get(obj); if (cache != null) { @@ -22,22 +23,22 @@ export function sequentialize(key: (args: any[]) => string = JSON.stringify) { return { value: function(...args: any[]) { - const argsKey = key(args); + const argsCacheKey = cacheKey(args); const cache = getCache(this); - let response = cache.get(argsKey); + let response = cache.get(argsCacheKey); if (response != null) { return response; } response = originalMethod.apply(this, args).then((val: any) => { - cache.delete(argsKey); + cache.delete(argsCacheKey); return val; }).catch((err: any) => { - cache.delete(argsKey); + cache.delete(argsCacheKey); throw err; }); - cache.set(argsKey, response); + cache.set(argsCacheKey, response); return response; }, }; diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 5315621992..9cf4d673d8 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -104,7 +104,7 @@ export class CryptoService implements CryptoServiceAbstraction { return this.storageService.get(Keys.keyHash); } - @sequentialize() + @sequentialize(() => 'getEncKey') async getEncKey(): Promise { if (this.encKey != null) { return this.encKey; @@ -166,7 +166,7 @@ export class CryptoService implements CryptoServiceAbstraction { return this.privateKey; } - @sequentialize() + @sequentialize(() => 'getOrgKeys') async getOrgKeys(): Promise> { if (this.orgKeys != null && this.orgKeys.size > 0) { return this.orgKeys; From d07ee4c01bee7d277ff13088e2ebb69f7a8878f8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 23 Jul 2018 15:32:22 -0400 Subject: [PATCH 0447/1626] fix sequentialize tests --- spec/common/misc/sequentialize.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/common/misc/sequentialize.spec.ts b/spec/common/misc/sequentialize.spec.ts index 33c8e958e4..8bb2701cb2 100644 --- a/spec/common/misc/sequentialize.spec.ts +++ b/spec/common/misc/sequentialize.spec.ts @@ -119,8 +119,8 @@ describe('sequentialize decorator', () => { class Foo { calls = 0; - @sequentialize() - bar(a) { + @sequentialize((args) => 'bar' + args[0]) + bar(a: any) { this.calls++; return new Promise((res) => { setTimeout(() => { @@ -129,8 +129,8 @@ class Foo { }); } - @sequentialize((args) => args[0]) - baz(a) { + @sequentialize((args) => 'baz' + args[0]) + baz(a: any) { this.calls++; return new Promise((res) => { setTimeout(() => { From 70a0044ac5ec55b5b113647e8afcac688d2baa45 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 23 Jul 2018 15:41:54 -0400 Subject: [PATCH 0448/1626] added memory leak warning --- src/misc/sequentialize.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/misc/sequentialize.ts b/src/misc/sequentialize.ts index 46b665f64b..c3e330f7ed 100644 --- a/src/misc/sequentialize.ts +++ b/src/misc/sequentialize.ts @@ -5,6 +5,8 @@ * be returned, instead of calling the original function again * * Results are not cached, once the promise has returned, the next call will result in a fresh call + * + * WARNING: The decorator's scope is singleton, so using it on transient objects can lead to memory leaks. */ export function sequentialize(cacheKey: (args: any[]) => string) { return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => { From 0b2fab43d53804cafe0ba48526853393fce1ec6f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 23 Jul 2018 15:46:59 -0400 Subject: [PATCH 0449/1626] ref PR for mem leak explanation --- src/misc/sequentialize.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/misc/sequentialize.ts b/src/misc/sequentialize.ts index c3e330f7ed..b9501a8ad2 100644 --- a/src/misc/sequentialize.ts +++ b/src/misc/sequentialize.ts @@ -7,6 +7,7 @@ * Results are not cached, once the promise has returned, the next call will result in a fresh call * * WARNING: The decorator's scope is singleton, so using it on transient objects can lead to memory leaks. + * Read more at https://github.com/bitwarden/jslib/pull/7 */ export function sequentialize(cacheKey: (args: any[]) => string) { return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => { From bbcbd6d119396e80d02849c67204fa53434896e6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 23 Jul 2018 17:15:23 -0400 Subject: [PATCH 0450/1626] null checks --- src/angular/components/two-factor.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 39ce3578b9..b638796908 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -59,9 +59,9 @@ export class TwoFactorComponent implements OnInit, OnDestroy { if (this.win != null && this.u2fSupported) { let customWebVaultUrl: string = null; - if (this.environmentService.baseUrl) { + if (this.environmentService.baseUrl != null) { customWebVaultUrl = this.environmentService.baseUrl; - } else if (this.environmentService.webVaultUrl) { + } else if (this.environmentService.webVaultUrl != null) { customWebVaultUrl = this.environmentService.webVaultUrl; } From 20ac5a98a3c9e796b26fb7e59d33c426e3d9fb81 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 23 Jul 2018 17:33:35 -0400 Subject: [PATCH 0451/1626] password agent csv importer --- src/importers/passwordAgentCsvImporter.ts | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/importers/passwordAgentCsvImporter.ts diff --git a/src/importers/passwordAgentCsvImporter.ts b/src/importers/passwordAgentCsvImporter.ts new file mode 100644 index 0000000000..444585661c --- /dev/null +++ b/src/importers/passwordAgentCsvImporter.ts @@ -0,0 +1,40 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +export class PasswordAgentCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, false); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + if (value.length < 9) { + return; + } + const folder = this.getValueOrDefault(value[8], '(None)'); + const folderName = folder !== '(None)' ? folder.split('\\').join('/') : null; + this.processFolder(result, folderName); + const cipher = this.initLoginCipher(); + cipher.notes = this.getValueOrDefault(value[3]); + cipher.name = this.getValueOrDefault(value[0], '--'); + cipher.login.username = this.getValueOrDefault(value[1]); + cipher.login.password = this.getValueOrDefault(value[2]); + cipher.login.uris = this.makeUriArray(value[4]); + this.convertToNoteIfNeeded(cipher); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + if (this.organization) { + this.moveFoldersToCollections(result); + } + + result.success = true; + return result; + } +} From 6dc13e8579a02f2acb6a748624add7b1b2647fb7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 23 Jul 2018 17:37:32 -0400 Subject: [PATCH 0452/1626] nested folders use `/` --- src/importers/keepass2XmlImporter.ts | 2 +- src/importers/keepassxCsvImporter.ts | 2 +- src/importers/passwordSafeXmlImporter.ts | 2 +- src/importers/roboformCsvImporter.ts | 2 +- src/importers/stickyPasswordXmlImporter.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/importers/keepass2XmlImporter.ts b/src/importers/keepass2XmlImporter.ts index 21c24f783b..0bced5a80d 100644 --- a/src/importers/keepass2XmlImporter.ts +++ b/src/importers/keepass2XmlImporter.ts @@ -38,7 +38,7 @@ export class KeePass2XmlImporter extends BaseImporter implements Importer { if (!isRootNode) { if (groupName !== '') { - groupName += ' > '; + groupName += '/'; } const nameEl = this.querySelectorDirectChild(node, 'Name'); groupName += nameEl == null ? '-' : nameEl.textContent; diff --git a/src/importers/keepassxCsvImporter.ts b/src/importers/keepassxCsvImporter.ts index 3113bf5345..0d07fcc998 100644 --- a/src/importers/keepassxCsvImporter.ts +++ b/src/importers/keepassxCsvImporter.ts @@ -19,7 +19,7 @@ export class KeePassXCsvImporter extends BaseImporter implements Importer { value.Group = !this.isNullOrWhitespace(value.Group) && value.Group.startsWith('Root/') ? value.Group.replace('Root/', '') : value.Group; - const groupName = !this.isNullOrWhitespace(value.Group) ? value.Group.split('/').join(' > ') : null; + const groupName = !this.isNullOrWhitespace(value.Group) ? value.Group : null; this.processFolder(result, groupName); const cipher = this.initLoginCipher(); diff --git a/src/importers/passwordSafeXmlImporter.ts b/src/importers/passwordSafeXmlImporter.ts index ff799217b6..e4bbedb836 100644 --- a/src/importers/passwordSafeXmlImporter.ts +++ b/src/importers/passwordSafeXmlImporter.ts @@ -24,7 +24,7 @@ export class PasswordSafeXmlImporter extends BaseImporter implements Importer { Array.from(entries).forEach((entry) => { const group = this.querySelectorDirectChild(entry, 'group'); const groupText = group != null && !this.isNullOrWhitespace(group.textContent) ? - group.textContent.split('.').join(' > ') : null; + group.textContent.split('.').join('/') : null; this.processFolder(result, groupText); const title = this.querySelectorDirectChild(entry, 'title'); diff --git a/src/importers/roboformCsvImporter.ts b/src/importers/roboformCsvImporter.ts index 6d0cc5a2a4..741cc69e4e 100644 --- a/src/importers/roboformCsvImporter.ts +++ b/src/importers/roboformCsvImporter.ts @@ -16,7 +16,7 @@ export class RoboFormCsvImporter extends BaseImporter implements Importer { results.forEach((value) => { const folder = !this.isNullOrWhitespace(value.Folder) && value.Folder.startsWith('/') ? value.Folder.replace('/', '') : value.Folder; - const folderName = !this.isNullOrWhitespace(folder) ? folder.split('/').join(' > ') : null; + const folderName = !this.isNullOrWhitespace(folder) ? folder : null; this.processFolder(result, folderName); const cipher = this.initLoginCipher(); diff --git a/src/importers/stickyPasswordXmlImporter.ts b/src/importers/stickyPasswordXmlImporter.ts index 6bd992f814..6bb1dc88ee 100644 --- a/src/importers/stickyPasswordXmlImporter.ts +++ b/src/importers/stickyPasswordXmlImporter.ts @@ -71,7 +71,7 @@ export class StickyPasswordXmlImporter extends BaseImporter implements Importer return groupText; } if (!this.isNullOrWhitespace(groupText)) { - groupText = ' > ' + groupText; + groupText = '/' + groupText; } groupText = group.getAttribute('Name') + groupText; return this.buildGroupText(doc, group.getAttribute('ParentID'), groupText); From 799361d93343409d185ec42fd6f3c4bfe8ad7854 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 24 Jul 2018 12:07:50 -0400 Subject: [PATCH 0453/1626] update some libs --- package-lock.json | 744 ++++++++++++++++++++++++++++------------------ package.json | 8 +- 2 files changed, 458 insertions(+), 294 deletions(-) diff --git a/package-lock.json b/package-lock.json index 91dce5732d..b78ee02962 100644 --- a/package-lock.json +++ b/package-lock.json @@ -99,12 +99,6 @@ "integrity": "sha512-clg9raJTY0EOo5pVZKX3ZlMjlYzVU73L71q5OV1jhE2Uezb7oF94jh4CvwrW6wInquQAdhOxJz5VDF2TLUGmmA==", "dev": true }, - "@types/keytar": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/keytar/-/keytar-4.0.1.tgz", - "integrity": "sha512-loKBID6UL4QjhD2scuvv6oAPlQ/WAY7aYTDyKlKo7fIgriLS8EZExqT567cHL5CY6si51MRoX1+r3mitD3eYrA==", - "dev": true - }, "@types/lodash": { "version": "4.14.109", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.109.tgz", @@ -259,8 +253,7 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { "version": "2.2.1", @@ -293,6 +286,20 @@ "default-require-extensions": "1.0.0" } }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.6" + } + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -632,9 +639,9 @@ "dev": true }, "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", "dev": true, "optional": true, "requires": { @@ -656,6 +663,15 @@ "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", "dev": true }, + "bl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "requires": { + "readable-stream": "2.3.6", + "safe-buffer": "5.1.1" + } + }, "blob": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", @@ -699,15 +715,6 @@ "type-is": "1.6.16" } }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "dev": true, - "requires": { - "hoek": "4.2.1" - } - }, "boxen": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", @@ -913,10 +920,29 @@ "ieee754": "1.1.11" } }, - "buffer-from": { + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "requires": { + "buffer-alloc-unsafe": "1.1.0", + "buffer-fill": "1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, + "buffer-fill": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", - "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==" + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" + }, + "buffer-from": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", + "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ==" }, "buffer-xor": { "version": "1.0.3", @@ -925,13 +951,13 @@ "dev": true }, "builder-util-runtime": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-4.2.1.tgz", - "integrity": "sha512-6Ufp6ExT40RDYNXQgD4xG0fgtpUHyc8XIld6lptKr0re1DNnUrQP4sSV/lJOajpzyercMP/YIzO60/mNuAFiWg==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-4.4.1.tgz", + "integrity": "sha512-8L2pbL6D3VdI1f8OMknlZJpw0c7KK15BRz3cY77AOUElc4XlCv2UhVV01jJM7+6Lx7henaQh80ALULp64eFYAQ==", "requires": { "bluebird-lst": "1.0.5", "debug": "3.1.0", - "fs-extra-p": "4.6.0", + "fs-extra-p": "4.6.1", "sax": "1.2.4" }, "dependencies": { @@ -1092,6 +1118,11 @@ } } }, + "chownr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=" + }, "ci-info": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.3.tgz", @@ -1199,8 +1230,7 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "collection-visit": { "version": "1.0.0", @@ -1312,11 +1342,12 @@ "dev": true }, "concat-stream": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", - "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "dev": true, "requires": { + "buffer-from": "1.1.0", "inherits": "2.0.3", "readable-stream": "2.3.6", "typedarray": "0.0.6" @@ -1408,18 +1439,6 @@ } } }, - "conf": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/conf/-/conf-1.4.0.tgz", - "integrity": "sha512-bzlVWS2THbMetHqXKB8ypsXN4DQ/1qopGwNJi1eYbpwesJcd86FBjFciCQX/YwAhp9bM7NVnPFqZ5LpV7gP0Dg==", - "requires": { - "dot-prop": "4.2.0", - "env-paths": "1.0.0", - "make-dir": "1.2.0", - "pkg-up": "2.0.0", - "write-file-atomic": "2.3.0" - } - }, "configstore": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", @@ -1455,6 +1474,11 @@ "date-now": "0.1.4" } }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, "constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", @@ -1493,8 +1517,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "create-ecdh": { "version": "4.0.1", @@ -1569,26 +1592,6 @@ "which": "1.3.0" } }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "dev": true, - "requires": { - "boom": "5.2.0" - }, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "dev": true, - "requires": { - "hoek": "4.2.1" - } - } - } - }, "crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", @@ -1695,11 +1698,18 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "requires": { + "mimic-response": "1.0.1" + } + }, "deep-extend": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", - "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", - "dev": true + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" }, "deep-is": { "version": "0.1.3", @@ -1783,6 +1793,11 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -1816,6 +1831,11 @@ "repeating": "2.0.1" } }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, "di": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", @@ -1861,6 +1881,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "dev": true, "requires": { "is-obj": "1.0.1" } @@ -1900,20 +1921,20 @@ "dev": true }, "electron": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/electron/-/electron-2.0.2.tgz", - "integrity": "sha512-XmkGVoHLOqmjZ2nU/0zEzMl3TZEz452Q1fTJFKjylg4pLYaq7na7V2uxzydVQNQukZGbERoA7ayjxXzTsXbtdA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/electron/-/electron-2.0.5.tgz", + "integrity": "sha512-NbWsgAvcxxQrDNaLA2L5adZTKWO6mZwC57uSPQiZiFjpO0K6uVNCjFyRbLnhq8AWq2tmcuzs6mFpIzQXmvlnUQ==", "dev": true, "requires": { - "@types/node": "8.10.17", + "@types/node": "8.10.21", "electron-download": "3.3.0", - "extract-zip": "1.6.6" + "extract-zip": "1.6.7" }, "dependencies": { "@types/node": { - "version": "8.10.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.17.tgz", - "integrity": "sha512-3N3FRd/rA1v5glXjb90YdYUa+sOB7WrkU2rAhKZnF4TKD86Cym9swtulGuH0p9nxo7fP5woRNa8b0oFTpCO1bg==", + "version": "8.10.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.21.tgz", + "integrity": "sha512-87XkD9qDXm8fIax+5y7drx84cXsu34ZZqfB7Cial3Q/2lxSoJ/+DRaWckkCbxP41wFSIrrb939VhzaNxj4eY1w==", "dev": true } } @@ -1926,7 +1947,7 @@ "requires": { "debug": "2.6.9", "fs-extra": "0.30.0", - "home-path": "1.0.5", + "home-path": "1.0.6", "minimist": "1.2.0", "nugget": "2.0.1", "path-exists": "2.1.0", @@ -1959,34 +1980,26 @@ "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.14.tgz", "integrity": "sha512-Rj+XyK4nShe/nv9v1Uks4KEfjtQ6N+eSnx5CLpAjG6rlyUdAflyFHoybcHSLoq9l9pGavclULWS5IXgk8umc2g==" }, - "electron-store": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-1.3.0.tgz", - "integrity": "sha512-r1Pdl5MwpiCxgbsl0qnwv/GABO5+J/JTO16+KyqL+bOITIk9o3cq3Sw69uO9NgPkpfcKeEwxtJFbtbiBlGTiDA==", - "requires": { - "conf": "1.4.0" - } - }, "electron-updater": { - "version": "2.21.4", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-2.21.4.tgz", - "integrity": "sha512-x6QSbyxgGR3szIOBtFoCJH0TfgB55AWHaXmilNgorfvpnCdEMQEATxEzLOW0JCzzcB5y3vBrawvmMUEdXwutmA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-3.0.3.tgz", + "integrity": "sha512-7gJLZp34Db+lXiJsFzW8DunGnvxJgZclBZa1DNLbXOet3lRXkVKbFJ73mClbv+UTW6hW/EJ6MmSsofRiK1s6Dw==", "requires": { "bluebird-lst": "1.0.5", - "builder-util-runtime": "4.2.1", + "builder-util-runtime": "4.4.1", "electron-is-dev": "0.3.0", - "fs-extra-p": "4.6.0", - "js-yaml": "3.11.0", + "fs-extra-p": "4.6.1", + "js-yaml": "3.12.0", "lazy-val": "1.0.3", "lodash.isequal": "4.5.0", "semver": "5.5.0", - "source-map-support": "0.5.5" + "source-map-support": "0.5.6" }, "dependencies": { "js-yaml": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz", - "integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", "requires": { "argparse": "1.0.10", "esprima": "4.0.0" @@ -2028,6 +2041,29 @@ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", "dev": true }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "1.4.0" + }, + "dependencies": { + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } + }, "engine.io": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.8.3.tgz", @@ -2122,11 +2158,6 @@ "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", "dev": true }, - "env-paths": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz", - "integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=" - }, "error-ex": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", @@ -2325,6 +2356,11 @@ "fill-range": "2.2.3" } }, + "expand-template": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-1.1.1.tgz", + "integrity": "sha512-cebqLtV8KOZfw0UI8TEFWxtczxxC1jvyUvx6H4fyp1K1FN7A4Q+uggVUlOsI1K8AGU0rwOGqP8nCapdrw8CYQg==" + }, "extend": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", @@ -2358,32 +2394,15 @@ } }, "extract-zip": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.6.tgz", - "integrity": "sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw=", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz", + "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=", "dev": true, "requires": { - "concat-stream": "1.6.0", + "concat-stream": "1.6.2", "debug": "2.6.9", - "mkdirp": "0.5.0", + "mkdirp": "0.5.1", "yauzl": "2.4.1" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mkdirp": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", - "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - } } }, "extsprintf": { @@ -2626,6 +2645,11 @@ "null-check": "1.0.0" } }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "fs-extra": { "version": "0.30.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", @@ -2648,22 +2672,22 @@ } }, "fs-extra-p": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.6.0.tgz", - "integrity": "sha512-nSVqB5UfWZQdU6pzBwcFh+7lJpBynnTsVtNJTBhAnAppUQRut0W7WeM271iS0TqQ9FoCqDXqyL0+h+h8DQUCpg==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.6.1.tgz", + "integrity": "sha512-IsTMbUS0svZKZTvqF4vDS9c/L7Mw9n8nZQWWeSzAGacOSe+8CzowhUN0tdZEZFIJNP5HC7L9j3MMikz/G4hDeQ==", "requires": { "bluebird-lst": "1.0.5", - "fs-extra": "6.0.0" + "fs-extra": "6.0.1" }, "dependencies": { "fs-extra": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.0.tgz", - "integrity": "sha512-lk2cUCo8QzbiEWEbt7Cw3m27WMiRG321xsssbcIpfMhpRjrlC08WBOVQqj1/nQYYNnPtyIhP1oqLO3QwT2tPCw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", + "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", "requires": { "graceful-fs": "4.1.11", "jsonfile": "4.0.0", - "universalify": "0.1.1" + "universalify": "0.1.2" } }, "jsonfile": { @@ -3585,6 +3609,41 @@ } } }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.0", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.3" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, "get-stdin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", @@ -3612,6 +3671,11 @@ "assert-plus": "1.0.0" } }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" + }, "glob": { "version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", @@ -3749,6 +3813,11 @@ "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", "dev": true }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -3845,18 +3914,6 @@ } } }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", - "dev": true, - "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.1", - "sntp": "2.1.0" - } - }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -3868,16 +3925,10 @@ "minimalistic-crypto-utils": "1.0.1" } }, - "hoek": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", - "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", - "dev": true - }, "home-path": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/home-path/-/home-path-1.0.5.tgz", - "integrity": "sha1-eIspgVsS1Tus9XVkhHbm+QQdEz8=", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/home-path/-/home-path-1.0.6.tgz", + "integrity": "sha512-wo+yjrdAtoXt43Vy92a+0IPCYViiyLAHyp0QVS4xL/tfvVz5sXIW1ubLZk3nhVkD92fQpUMKX+fzMjr5F489vw==", "dev": true }, "hosted-git-info": { @@ -3924,7 +3975,7 @@ "requires": { "assert-plus": "1.0.0", "jsprim": "1.4.1", - "sshpk": "1.14.1" + "sshpk": "1.14.2" } }, "https-browserify": { @@ -3960,7 +4011,8 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true }, "indent-string": { "version": "2.1.0", @@ -3994,8 +4046,7 @@ "ini": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "inline-source-map": { "version": "0.6.2", @@ -4138,8 +4189,7 @@ "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "is-glob": { "version": "2.0.1", @@ -4178,7 +4228,8 @@ "is-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true }, "is-odd": { "version": "2.0.0", @@ -4279,8 +4330,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isbinaryfile": { "version": "3.0.2", @@ -5338,17 +5388,18 @@ } }, "keytar": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-4.1.0.tgz", - "integrity": "sha512-L3KqiSMtpVitlug4uuI+K5XLne9SAVEFWE8SCQIhQiH0IA/CTbon5v5prVLKK0Ken54o2O8V9HceKagpwJum+Q==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-4.2.1.tgz", + "integrity": "sha1-igamV3/fY3PgqmsRInfmPex3/RI=", "requires": { - "nan": "2.5.1" + "nan": "2.8.0", + "prebuild-install": "2.5.3" }, "dependencies": { "nan": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.5.1.tgz", - "integrity": "sha1-1bAWkSUzJql6K77p5hxV2NYDUeI=" + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", + "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=" } } }, @@ -5414,22 +5465,6 @@ "strip-bom": "2.0.0" } }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "requires": { - "p-locate": "2.0.0", - "path-exists": "3.0.0" - }, - "dependencies": { - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - } - } - }, "lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", @@ -5557,6 +5592,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.2.0.tgz", "integrity": "sha512-aNUAa4UMg/UougV25bbrU4ZaaKNjJ/3/xnvg/twpmKROPdKZPZ9wGgI0opdZzO8q/zUFawoUuixuOv33eZ61Iw==", + "dev": true, "requires": { "pify": "3.0.0" }, @@ -5564,7 +5600,8 @@ "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true } } }, @@ -5695,6 +5732,11 @@ "mime-db": "1.33.0" } }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -5746,7 +5788,6 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, "requires": { "minimist": "0.0.8" }, @@ -5754,8 +5795,7 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" } } }, @@ -5836,6 +5876,21 @@ "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", "dev": true }, + "node-abi": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.4.3.tgz", + "integrity": "sha512-b656V5C0628gOOA2kwcpNA/bxdlqYF9FvxJ+qqVX0ctdXNVZpS8J6xEUYir3WAKc7U0BH/NRlSpNbGsy+azjeg==", + "requires": { + "semver": "5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" + } + } + }, "node-fetch": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz", @@ -6309,6 +6364,11 @@ } } }, + "noop-logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", + "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" + }, "nopt": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", @@ -6348,6 +6408,17 @@ "path-key": "2.0.1" } }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "1.1.5", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, "nugget": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/nugget/-/nugget-2.0.1.tgz", @@ -6358,7 +6429,7 @@ "minimist": "1.2.0", "pretty-bytes": "1.0.4", "progress-stream": "1.2.0", - "request": "2.85.0", + "request": "2.87.0", "single-line-log": "1.1.2", "throttleit": "0.0.2" }, @@ -6380,8 +6451,7 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "oauth-sign": { "version": "0.8.2", @@ -6392,8 +6462,7 @@ "object-assign": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", - "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=", - "dev": true + "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=" }, "object-component": { "version": "0.0.3", @@ -6534,6 +6603,11 @@ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", "dev": true }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -6546,27 +6620,6 @@ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", "dev": true }, - "p-limit": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", - "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", - "requires": { - "p-try": "1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "requires": { - "p-limit": "1.2.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" - }, "package-json": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", @@ -6784,24 +6837,6 @@ "pinkie": "2.0.4" } }, - "pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", - "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=", - "requires": { - "find-up": "2.1.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "requires": { - "locate-path": "2.0.0" - } - } - } - }, "plugin-error": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", @@ -6839,6 +6874,35 @@ "integrity": "sha1-uRepB5smF42aJK9aXNjLSpkdEbk=", "dev": true }, + "prebuild-install": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-2.5.3.tgz", + "integrity": "sha512-/rI36cN2g7vDQnKWN8Uzupi++KjyqS9iS+/fpwG4Ea8d0Pip0PQ5bshUNzVwt+/D2MRfhVAplYMMvWLqWrCF/g==", + "requires": { + "detect-libc": "1.0.3", + "expand-template": "1.1.1", + "github-from-package": "0.0.0", + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "node-abi": "2.4.3", + "noop-logger": "0.1.1", + "npmlog": "4.1.2", + "os-homedir": "1.0.2", + "pump": "2.0.1", + "rc": "1.2.6", + "simple-get": "2.8.1", + "tar-fs": "1.16.3", + "tunnel-agent": "0.6.0", + "which-pm-runs": "1.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -6876,8 +6940,7 @@ "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "progress-stream": { "version": "1.2.0", @@ -6977,6 +7040,30 @@ "randombytes": "2.0.6" } }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "requires": { + "end-of-stream": "1.4.1", + "once": "1.4.0" + }, + "dependencies": { + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } + }, "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", @@ -7121,7 +7208,6 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.6.tgz", "integrity": "sha1-6xiYnG1PTxYsOZ953dKfODVWgJI=", - "dev": true, "requires": { "deep-extend": "0.4.2", "ini": "1.3.5", @@ -7132,8 +7218,7 @@ "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" } } }, @@ -7162,7 +7247,6 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, "requires": { "core-util-is": "1.0.2", "inherits": "2.0.3", @@ -7176,8 +7260,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" } } }, @@ -7382,9 +7465,9 @@ } }, "request": { - "version": "2.85.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", - "integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", "dev": true, "requires": { "aws-sign2": "0.7.0", @@ -7395,7 +7478,6 @@ "forever-agent": "0.6.1", "form-data": "2.3.2", "har-validator": "5.0.3", - "hawk": "6.0.2", "http-signature": "1.2.0", "is-typedarray": "1.0.0", "isstream": "0.1.2", @@ -7405,10 +7487,9 @@ "performance-now": "2.1.0", "qs": "6.5.1", "safe-buffer": "5.1.1", - "stringstream": "0.0.5", "tough-cookie": "2.3.4", "tunnel-agent": "0.6.0", - "uuid": "3.2.1" + "uuid": "3.3.2" } }, "requires-port": { @@ -7492,8 +7573,7 @@ "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, "safe-regex": { "version": "1.1.0", @@ -7504,6 +7584,12 @@ "ret": "0.1.15" } }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -7532,6 +7618,11 @@ } } }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, "set-immediate-shim": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", @@ -7611,6 +7702,36 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, + "simple-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", + "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" + }, + "simple-get": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.1.tgz", + "integrity": "sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==", + "requires": { + "decompress-response": "3.3.0", + "once": "1.4.0", + "simple-concat": "1.0.0" + }, + "dependencies": { + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } + }, "single-line-log": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-1.1.2.tgz", @@ -7756,15 +7877,6 @@ "kind-of": "3.2.2" } }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", - "dev": true, - "requires": { - "hoek": "4.2.1" - } - }, "socket.io": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.7.3.tgz", @@ -7924,11 +8036,11 @@ } }, "source-map-support": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.5.tgz", - "integrity": "sha512-mR7/Nd5l1z6g99010shcXJiNEaf3fEtmLhRB/sBcQVJGodcHCULPp2y4Sfa43Kv2zq7T+Izmfp/WHCR6dYkQCA==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz", + "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==", "requires": { - "buffer-from": "1.0.0", + "buffer-from": "1.1.0", "source-map": "0.6.1" }, "dependencies": { @@ -8034,18 +8146,19 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "sshpk": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", - "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", "dev": true, "requires": { "asn1": "0.2.3", "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", + "bcrypt-pbkdf": "1.0.2", "dashdash": "1.14.1", "ecc-jsbn": "0.1.1", "getpass": "0.1.7", "jsbn": "0.1.1", + "safer-buffer": "2.1.2", "tweetnacl": "0.14.5" } }, @@ -8192,7 +8305,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, "requires": { "is-fullwidth-code-point": "2.0.0", "strip-ansi": "4.0.0" @@ -8201,14 +8313,12 @@ "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, "requires": { "ansi-regex": "3.0.0" } @@ -8219,22 +8329,14 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "5.1.1" } }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", - "dev": true - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, "requires": { "ansi-regex": "2.1.1" } @@ -8266,8 +8368,7 @@ "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "sumchecker": { "version": "1.3.1", @@ -8290,6 +8391,55 @@ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" }, + "tar-fs": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", + "integrity": "sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==", + "requires": { + "chownr": "1.0.1", + "mkdirp": "0.5.1", + "pump": "1.0.3", + "tar-stream": "1.6.1" + }, + "dependencies": { + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "pump": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", + "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", + "requires": { + "end-of-stream": "1.4.1", + "once": "1.4.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } + }, + "tar-stream": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.1.tgz", + "integrity": "sha512-IFLM5wp3QrJODQFPm6/to3LJZrONdBY/otxcvDIQzu217zKye6yVR3hhi9lAjrC2Z+m/j5oDxMPb1qcd8cIvpA==", + "requires": { + "bl": "1.2.2", + "buffer-alloc": "1.2.0", + "end-of-stream": "1.4.1", + "fs-constants": "1.0.0", + "readable-stream": "2.3.6", + "to-buffer": "1.1.1", + "xtend": "4.0.1" + } + }, "term-size": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", @@ -8391,6 +8541,11 @@ "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", "dev": true }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" + }, "to-fast-properties": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", @@ -8818,7 +8973,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, "requires": { "safe-buffer": "5.1.1" } @@ -8966,9 +9120,9 @@ } }, "universalify": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", - "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" }, "unpipe": { "version": "1.0.0", @@ -9169,8 +9323,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "utils-merge": { "version": "1.0.1", @@ -9179,9 +9332,9 @@ "dev": true }, "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", "dev": true }, "validate-npm-package-license": { @@ -9238,6 +9391,19 @@ "isexe": "2.0.0" } }, + "which-pm-runs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", + "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=" + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "2.1.1" + } + }, "widest-line": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.0.tgz", @@ -9269,6 +9435,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", + "dev": true, "requires": { "graceful-fs": "4.1.11", "imurmurhash": "0.1.4", @@ -9306,8 +9473,7 @@ "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" }, "yallist": { "version": "2.1.2", diff --git a/package.json b/package.json index fbdc1ccbed..c34ba06efc 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "devDependencies": { "@types/form-data": "^2.2.1", "@types/jasmine": "^2.8.2", - "@types/keytar": "^4.0.1", "@types/lowdb": "^1.0.1", "@types/lunr": "2.1.5", "@types/node": "8.0.19", @@ -35,7 +34,7 @@ "@types/papaparse": "4.1.31", "@types/webcrypto": "0.0.28", "concurrently": "3.5.1", - "electron": "2.0.2", + "electron": "2.0.5", "jasmine": "^3.1.0", "jasmine-core": "^2.8.0", "jasmine-spec-reporter": "^4.2.1", @@ -72,10 +71,9 @@ "angulartics2": "5.0.1", "core-js": "2.4.1", "electron-log": "2.2.14", - "electron-store": "1.3.0", - "electron-updater": "2.21.4", + "electron-updater": "3.0.3", "form-data": "2.3.2", - "keytar": "4.1.0", + "keytar": "4.2.1", "lowdb": "1.0.0", "lunr": "2.1.6", "node-fetch": "2.1.2", From 9df96a3288510a5b92837d93513d9981336d0229 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 25 Jul 2018 11:01:03 -0400 Subject: [PATCH 0454/1626] default disable on FirefoxExtension --- src/misc/analytics.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/misc/analytics.ts b/src/misc/analytics.ts index 5812821b2e..687d6fc4c9 100644 --- a/src/misc/analytics.ts +++ b/src/misc/analytics.ts @@ -42,7 +42,8 @@ export class Analytics { } this.appVersion = this.platformUtilsService.getApplicationVersion(); - this.defaultDisabled = this.platformUtilsService.isFirefox() || this.platformUtilsService.isMacAppStore(); + this.defaultDisabled = this.platformUtilsService.getDevice() === DeviceType.FirefoxExtension || + this.platformUtilsService.isMacAppStore(); this.gaTrackingId = this.platformUtilsService.analyticsId(); (win as any).GoogleAnalyticsObject = GaObj; From dab995488774032006bc31a1027b742566bd6696 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 27 Jul 2018 16:44:20 -0400 Subject: [PATCH 0455/1626] send password history to server --- .../passwordGeneration.service.ts | 4 +- .../password-generator-history.component.ts | 4 +- src/models/data/cipherData.ts | 9 +++ src/models/data/passwordHistoryData.ts | 15 ++++ src/models/domain/cipher.ts | 30 ++++++++ ...History.ts => generatedPasswordHistory.ts} | 2 +- src/models/domain/index.ts | 2 +- src/models/domain/password.ts | 39 +++++++++++ src/models/request/cipherRequest.ts | 13 ++++ src/models/request/passwordHistoryRequest.ts | 9 +++ src/models/response/cipherResponse.ts | 9 +++ .../response/passwordHistoryResponse.ts | 9 +++ src/models/view/cipherView.ts | 6 ++ src/models/view/passwordHistoryView.ts | 16 +++++ src/services/cipher.service.ts | 70 +++++++++++++++++++ src/services/passwordGeneration.service.ts | 24 +++---- 16 files changed, 243 insertions(+), 18 deletions(-) create mode 100644 src/models/data/passwordHistoryData.ts rename src/models/domain/{passwordHistory.ts => generatedPasswordHistory.ts} (79%) create mode 100644 src/models/domain/password.ts create mode 100644 src/models/request/passwordHistoryRequest.ts create mode 100644 src/models/response/passwordHistoryResponse.ts create mode 100644 src/models/view/passwordHistoryView.ts diff --git a/src/abstractions/passwordGeneration.service.ts b/src/abstractions/passwordGeneration.service.ts index 44f7cee32d..414e29e710 100644 --- a/src/abstractions/passwordGeneration.service.ts +++ b/src/abstractions/passwordGeneration.service.ts @@ -1,10 +1,10 @@ -import { PasswordHistory } from '../models/domain/passwordHistory'; +import { GeneratedPasswordHistory } from '../models/domain/generatedPasswordHistory'; export abstract class PasswordGenerationService { generatePassword: (options: any) => Promise; getOptions: () => any; saveOptions: (options: any) => Promise; - getHistory: () => Promise; + getHistory: () => Promise; addHistory: (password: string) => Promise; clear: () => Promise; } diff --git a/src/angular/components/password-generator-history.component.ts b/src/angular/components/password-generator-history.component.ts index a94f780a06..015d64fcc0 100644 --- a/src/angular/components/password-generator-history.component.ts +++ b/src/angular/components/password-generator-history.component.ts @@ -7,10 +7,10 @@ import { I18nService } from '../../abstractions/i18n.service'; import { PasswordGenerationService } from '../../abstractions/passwordGeneration.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; -import { PasswordHistory } from '../../models/domain/passwordHistory'; +import { GeneratedPasswordHistory } from '../../models/domain/generatedPasswordHistory'; export class PasswordGeneratorHistoryComponent implements OnInit { - history: PasswordHistory[] = []; + history: GeneratedPasswordHistory[] = []; constructor(protected passwordGenerationService: PasswordGenerationService, protected analytics: Angulartics2, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, diff --git a/src/models/data/cipherData.ts b/src/models/data/cipherData.ts index 59a52fe53d..e3b9944de7 100644 --- a/src/models/data/cipherData.ts +++ b/src/models/data/cipherData.ts @@ -5,6 +5,7 @@ import { CardData } from './cardData'; import { FieldData } from './fieldData'; import { IdentityData } from './identityData'; import { LoginData } from './loginData'; +import { PasswordHistoryData } from './passwordHistoryData'; import { SecureNoteData } from './secureNoteData'; import { CipherResponse } from '../response/cipherResponse'; @@ -28,6 +29,7 @@ export class CipherData { identity?: IdentityData; fields?: FieldData[]; attachments?: AttachmentData[]; + passwordHistory?: PasswordHistoryData[]; collectionIds?: string[]; constructor(response?: CipherResponse, userId?: string, collectionIds?: string[]) { @@ -83,5 +85,12 @@ export class CipherData { this.attachments.push(new AttachmentData(attachment)); }); } + + if (response.passwordHistory != null) { + this.passwordHistory = []; + response.passwordHistory.forEach((ph) => { + this.passwordHistory.push(new PasswordHistoryData(ph)); + }); + } } } diff --git a/src/models/data/passwordHistoryData.ts b/src/models/data/passwordHistoryData.ts new file mode 100644 index 0000000000..1ef1c87a48 --- /dev/null +++ b/src/models/data/passwordHistoryData.ts @@ -0,0 +1,15 @@ +import { PasswordHistoryResponse } from '../response/passwordHistoryResponse'; + +export class PasswordHistoryData { + password: string; + lastUsedDate: Date; + + constructor(response?: PasswordHistoryResponse) { + if (response == null) { + return; + } + + this.password = response.password; + this.lastUsedDate = response.lastUsedDate; + } +} diff --git a/src/models/domain/cipher.ts b/src/models/domain/cipher.ts index ad5cd75587..d66ee67cc2 100644 --- a/src/models/domain/cipher.ts +++ b/src/models/domain/cipher.ts @@ -11,6 +11,7 @@ import Domain from './domain'; import { Field } from './field'; import { Identity } from './identity'; import { Login } from './login'; +import { Password } from './password'; import { SecureNote } from './secureNote'; export class Cipher extends Domain { @@ -31,6 +32,7 @@ export class Cipher extends Domain { secureNote: SecureNote; attachments: Attachment[]; fields: Field[]; + passwordHistory: Password[]; collectionIds: string[]; constructor(obj?: CipherData, alreadyEncrypted: boolean = false, localData: any = null) { @@ -90,6 +92,15 @@ export class Cipher extends Domain { } else { this.fields = null; } + + if (obj.passwordHistory != null) { + this.passwordHistory = []; + obj.passwordHistory.forEach((ph) => { + this.passwordHistory.push(new Password(ph, alreadyEncrypted)); + }); + } else { + this.passwordHistory = null; + } } async decrypt(): Promise { @@ -143,6 +154,18 @@ export class Cipher extends Domain { model.fields = fields; } + if (this.passwordHistory != null && this.passwordHistory.length > 0) { + const passwordHistory: any[] = []; + await this.passwordHistory.reduce((promise, ph) => { + return promise.then(() => { + return ph.decrypt(orgId); + }).then((decPh) => { + passwordHistory.push(decPh); + }); + }, Promise.resolve()); + model.passwordHistory = passwordHistory; + } + return model; } @@ -194,6 +217,13 @@ export class Cipher extends Domain { c.attachments.push(attachment.toAttachmentData()); }); } + + if (this.passwordHistory != null) { + c.passwordHistory = []; + this.passwordHistory.forEach((ph) => { + c.passwordHistory.push(ph.toPasswordHistoryData()); + }); + } return c; } } diff --git a/src/models/domain/passwordHistory.ts b/src/models/domain/generatedPasswordHistory.ts similarity index 79% rename from src/models/domain/passwordHistory.ts rename to src/models/domain/generatedPasswordHistory.ts index 35e7892dcd..1b08256548 100644 --- a/src/models/domain/passwordHistory.ts +++ b/src/models/domain/generatedPasswordHistory.ts @@ -1,4 +1,4 @@ -export class PasswordHistory { +export class GeneratedPasswordHistory { password: string; date: number; diff --git a/src/models/domain/index.ts b/src/models/domain/index.ts index a25fe24735..64a1d7f4e1 100644 --- a/src/models/domain/index.ts +++ b/src/models/domain/index.ts @@ -11,6 +11,6 @@ export { Folder } from './folder'; export { Identity } from './identity'; export { Login } from './login'; export { LoginUri } from './loginUri'; -export { PasswordHistory } from './passwordHistory'; +export { GeneratedPasswordHistory } from './generatedPasswordHistory'; export { SecureNote } from './secureNote'; export { SymmetricCryptoKey } from './symmetricCryptoKey'; diff --git a/src/models/domain/password.ts b/src/models/domain/password.ts new file mode 100644 index 0000000000..c60d0937fb --- /dev/null +++ b/src/models/domain/password.ts @@ -0,0 +1,39 @@ +import { PasswordHistoryData } from '../data/passwordHistoryData'; + +import { CipherString } from './cipherString'; +import Domain from './domain'; + +import { PasswordHistoryView } from '../view/passwordHistoryView'; + +export class Password extends Domain { + password: CipherString; + lastUsedDate: Date; + + constructor(obj?: PasswordHistoryData, alreadyEncrypted: boolean = false) { + super(); + if (obj == null) { + return; + } + + this.buildDomainModel(this, obj, { + password: null, + lastUsedDate: null, + }, alreadyEncrypted, ['lastUsedDate']); + } + + async decrypt(orgId: string): Promise { + const view = await this.decryptObj(new PasswordHistoryView(this), { + password: null, + }, orgId); + return view; + } + + toPasswordHistoryData(): PasswordHistoryData { + const ph = new PasswordHistoryData(); + ph.lastUsedDate = this.lastUsedDate; + this.buildDataModel(this, ph, { + password: null, + }); + return ph; + } +} diff --git a/src/models/request/cipherRequest.ts b/src/models/request/cipherRequest.ts index 5d876ebaba..01a4a9add9 100644 --- a/src/models/request/cipherRequest.ts +++ b/src/models/request/cipherRequest.ts @@ -8,6 +8,8 @@ import { IdentityApi } from '../api/identityApi'; import { LoginApi } from '../api/loginApi'; import { SecureNoteApi } from '../api/secureNoteApi'; +import { PasswordHistoryRequest } from './passwordHistoryRequest'; + export class CipherRequest { type: CipherType; folderId: string; @@ -20,6 +22,7 @@ export class CipherRequest { card: CardApi; identity: IdentityApi; fields: FieldApi[]; + passwordHistory: PasswordHistoryRequest[]; attachments: { [id: string]: string; }; constructor(cipher: Cipher) { @@ -102,6 +105,16 @@ export class CipherRequest { }); } + if (cipher.passwordHistory) { + this.passwordHistory = []; + cipher.passwordHistory.forEach((ph) => { + this.passwordHistory.push({ + lastUsedDate: ph.lastUsedDate, + password: ph.password ? ph.password.encryptedString : null, + }); + }); + } + if (cipher.attachments) { this.attachments = {}; cipher.attachments.forEach((attachment) => { diff --git a/src/models/request/passwordHistoryRequest.ts b/src/models/request/passwordHistoryRequest.ts new file mode 100644 index 0000000000..c917de1d10 --- /dev/null +++ b/src/models/request/passwordHistoryRequest.ts @@ -0,0 +1,9 @@ +export class PasswordHistoryRequest { + password: string; + lastUsedDate: Date; + + constructor(response: any) { + this.password = response.Password; + this.lastUsedDate = response.LastUsedDate; + } +} diff --git a/src/models/response/cipherResponse.ts b/src/models/response/cipherResponse.ts index 452a88d1b5..c2c9a67242 100644 --- a/src/models/response/cipherResponse.ts +++ b/src/models/response/cipherResponse.ts @@ -1,4 +1,5 @@ import { AttachmentResponse } from './attachmentResponse'; +import { PasswordHistoryResponse } from './passwordHistoryResponse'; import { CardApi } from '../api/cardApi'; import { FieldApi } from '../api/fieldApi'; @@ -23,6 +24,7 @@ export class CipherResponse { organizationUseTotp: boolean; revisionDate: Date; attachments: AttachmentResponse[]; + passwordHistory: PasswordHistoryResponse[]; collectionIds: string[]; constructor(response: any) { @@ -67,6 +69,13 @@ export class CipherResponse { }); } + if (response.PasswordHistory != null) { + this.passwordHistory = []; + response.PasswordHistory.forEach((ph: any) => { + this.passwordHistory.push(new PasswordHistoryResponse(ph)); + }); + } + if (response.CollectionIds) { this.collectionIds = []; response.CollectionIds.forEach((id: string) => { diff --git a/src/models/response/passwordHistoryResponse.ts b/src/models/response/passwordHistoryResponse.ts new file mode 100644 index 0000000000..8630da8f03 --- /dev/null +++ b/src/models/response/passwordHistoryResponse.ts @@ -0,0 +1,9 @@ +export class PasswordHistoryResponse { + password: string; + lastUsedDate: Date; + + constructor(response: any) { + this.password = response.Password; + this.lastUsedDate = response.LastUsedDate; + } +} diff --git a/src/models/view/cipherView.ts b/src/models/view/cipherView.ts index 72e68eaced..73a8d062e4 100644 --- a/src/models/view/cipherView.ts +++ b/src/models/view/cipherView.ts @@ -7,6 +7,7 @@ import { CardView } from './cardView'; import { FieldView } from './fieldView'; import { IdentityView } from './identityView'; import { LoginView } from './loginView'; +import { PasswordHistoryView } from './passwordHistoryView'; import { SecureNoteView } from './secureNoteView'; import { View } from './view'; @@ -27,6 +28,7 @@ export class CipherView implements View { secureNote: SecureNoteView; attachments: AttachmentView[]; fields: FieldView[]; + passwordHistory: PasswordHistoryView[]; collectionIds: string[]; constructor(c?: Cipher) { @@ -62,6 +64,10 @@ export class CipherView implements View { return null; } + get hasPasswordHistory(): boolean { + return this.passwordHistory && this.passwordHistory.length > 0; + } + get hasAttachments(): boolean { return this.attachments && this.attachments.length > 0; } diff --git a/src/models/view/passwordHistoryView.ts b/src/models/view/passwordHistoryView.ts new file mode 100644 index 0000000000..946424ffa9 --- /dev/null +++ b/src/models/view/passwordHistoryView.ts @@ -0,0 +1,16 @@ +import { View } from './view'; + +import { Password } from '../domain/password'; + +export class PasswordHistoryView implements View { + password: string; + lastUsedDate: Date; + + constructor(ph?: Password) { + if (!ph) { + return; + } + + this.lastUsedDate = ph.lastUsedDate; + } +} diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index b7b82eece9..7db6bf6fbf 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -1,4 +1,5 @@ import { CipherType } from '../enums/cipherType'; +import { FieldType } from '../enums/fieldType'; import { UriMatchType } from '../enums/uriMatchType'; import { CipherData } from '../models/data/cipherData'; @@ -12,6 +13,7 @@ import { Field } from '../models/domain/field'; import { Identity } from '../models/domain/identity'; import { Login } from '../models/domain/login'; import { LoginUri } from '../models/domain/loginUri'; +import { Password } from '../models/domain/password'; import { SecureNote } from '../models/domain/secureNote'; import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; @@ -28,6 +30,7 @@ import { ErrorResponse } from '../models/response/errorResponse'; import { AttachmentView } from '../models/view/attachmentView'; import { CipherView } from '../models/view/cipherView'; import { FieldView } from '../models/view/fieldView'; +import { PasswordHistoryView } from '../models/view/passwordHistoryView'; import { View } from '../models/view/view'; import { ApiService } from '../abstractions/api.service'; @@ -61,6 +64,41 @@ export class CipherService implements CipherServiceAbstraction { } async encrypt(model: CipherView, key?: SymmetricCryptoKey): Promise { + // Adjust password history + if (model.id != null) { + const existingCipher = await (await this.get(model.id)).decrypt(); + if (existingCipher != null) { + model.passwordHistory = existingCipher.passwordHistory || []; + if (model.type === CipherType.Login && existingCipher.type === CipherType.Login && + existingCipher.login.password !== model.login.password) { + const ph = new PasswordHistoryView(null); + ph.password = existingCipher.login.password; + ph.lastUsedDate = new Date(); + model.passwordHistory.splice(0, 0, ph); + } + if (existingCipher.hasFields) { + const existingHiddenFields = existingCipher.fields.filter((f) => f.type === FieldType.Hidden); + const hiddenFields = model.fields == null ? [] : + model.fields.filter((f) => f.type === FieldType.Hidden); + existingHiddenFields.forEach((ef) => { + const matchedField = hiddenFields.filter((f) => f.name === ef.name); + if (matchedField.length === 0 || matchedField[0].value !== ef.value) { + const ph = new PasswordHistoryView(null); + ph.password = ef.name + ': ' + ef.value; + ph.lastUsedDate = new Date(); + model.passwordHistory.splice(0, 0, ph); + } + }); + } + } + if (model.passwordHistory != null && model.passwordHistory.length === 0) { + model.passwordHistory = null; + } else if (model.passwordHistory != null && model.passwordHistory.length > 5) { + // only save last 5 history + model.passwordHistory = model.passwordHistory.slice(0, 4); + } + } + const cipher = new Cipher(); cipher.id = model.id; cipher.folderId = model.folderId; @@ -81,6 +119,9 @@ export class CipherService implements CipherServiceAbstraction { this.encryptFields(model.fields, key).then((fields) => { cipher.fields = fields; }), + this.encryptPasswordHistories(model.passwordHistory, key).then((ph) => { + cipher.passwordHistory = ph; + }), this.encryptAttachments(model.attachments, key).then((attachments) => { cipher.attachments = attachments; }), @@ -144,6 +185,35 @@ export class CipherService implements CipherServiceAbstraction { return field; } + async encryptPasswordHistories(phModels: PasswordHistoryView[], key: SymmetricCryptoKey): Promise { + if (!phModels || !phModels.length) { + return null; + } + + const self = this; + const encPhs: Password[] = []; + await phModels.reduce((promise, ph) => { + return promise.then(() => { + return self.encryptPasswordHistory(ph, key); + }).then((encPh: Password) => { + encPhs.push(encPh); + }); + }, Promise.resolve()); + + return encPhs; + } + + async encryptPasswordHistory(phModel: PasswordHistoryView, key: SymmetricCryptoKey): Promise { + const ph = new Password(); + ph.lastUsedDate = phModel.lastUsedDate; + + await this.encryptObjProperty(phModel, ph, { + password: null, + }, key); + + return ph; + } + async get(id: string): Promise { const userId = await this.userService.getUserId(); const localData = await this.storageService.get(Keys.localData); diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 0e68c0c8f8..cc5ebe4f34 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -1,5 +1,5 @@ import { CipherString } from '../models/domain/cipherString'; -import { PasswordHistory } from '../models/domain/passwordHistory'; +import { GeneratedPasswordHistory } from '../models/domain/generatedPasswordHistory'; import { CryptoService } from '../abstractions/crypto.service'; import { @@ -29,7 +29,7 @@ const MaxPasswordsInHistory = 100; export class PasswordGenerationService implements PasswordGenerationServiceAbstraction { private optionsCache: any; - private history: PasswordHistory[]; + private history: GeneratedPasswordHistory[]; constructor(private cryptoService: CryptoService, private storageService: StorageService) { } @@ -168,18 +168,18 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr this.optionsCache = options; } - async getHistory(): Promise { + async getHistory(): Promise { const hasKey = await this.cryptoService.hasKey(); if (!hasKey) { - return new Array(); + return new Array(); } if (!this.history) { - const encrypted = await this.storageService.get(Keys.history); + const encrypted = await this.storageService.get(Keys.history); this.history = await this.decryptHistory(encrypted); } - return this.history || new Array(); + return this.history || new Array(); } async addHistory(password: string): Promise { @@ -196,7 +196,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return; } - currentHistory.unshift(new PasswordHistory(password, Date.now())); + currentHistory.unshift(new GeneratedPasswordHistory(password, Date.now())); // Remove old items. if (currentHistory.length > MaxPasswordsInHistory) { @@ -212,33 +212,33 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return await this.storageService.remove(Keys.history); } - private async encryptHistory(history: PasswordHistory[]): Promise { + private async encryptHistory(history: GeneratedPasswordHistory[]): Promise { if (history == null || history.length === 0) { return Promise.resolve([]); } const promises = history.map(async (item) => { const encrypted = await this.cryptoService.encrypt(item.password); - return new PasswordHistory(encrypted.encryptedString, item.date); + return new GeneratedPasswordHistory(encrypted.encryptedString, item.date); }); return await Promise.all(promises); } - private async decryptHistory(history: PasswordHistory[]): Promise { + private async decryptHistory(history: GeneratedPasswordHistory[]): Promise { if (history == null || history.length === 0) { return Promise.resolve([]); } const promises = history.map(async (item) => { const decrypted = await this.cryptoService.decryptToUtf8(new CipherString(item.password)); - return new PasswordHistory(decrypted, item.date); + return new GeneratedPasswordHistory(decrypted, item.date); }); return await Promise.all(promises); } - private matchesPrevious(password: string, history: PasswordHistory[]): boolean { + private matchesPrevious(password: string, history: GeneratedPasswordHistory[]): boolean { if (history == null || history.length === 0) { return false; } From 6d431f7832721768cd5458de472650379f87b9b0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 27 Jul 2018 17:30:51 -0400 Subject: [PATCH 0456/1626] keep last 5, not 4 --- src/services/cipher.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 7db6bf6fbf..fdb1f5ceb5 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -95,7 +95,7 @@ export class CipherService implements CipherServiceAbstraction { model.passwordHistory = null; } else if (model.passwordHistory != null && model.passwordHistory.length > 5) { // only save last 5 history - model.passwordHistory = model.passwordHistory.slice(0, 4); + model.passwordHistory = model.passwordHistory.slice(0, 5); } } From 6a8d2c305e9a548c9d6cdcc04569ca3f32ab8370 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 27 Jul 2018 17:48:18 -0400 Subject: [PATCH 0457/1626] support for password revision date on logins --- src/models/api/loginApi.ts | 2 ++ src/models/data/loginData.ts | 3 ++- src/models/domain/login.ts | 4 +++- src/models/request/cipherRequest.ts | 1 + src/models/view/loginView.ts | 9 ++++++--- src/services/cipher.service.ts | 18 +++++++++++------- 6 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/models/api/loginApi.ts b/src/models/api/loginApi.ts index ba9a9da11d..7f5fac7b6b 100644 --- a/src/models/api/loginApi.ts +++ b/src/models/api/loginApi.ts @@ -4,11 +4,13 @@ export class LoginApi { uris: LoginUriApi[]; username: string; password: string; + passwordRevisionDate?: Date; totp: string; constructor(data: any) { this.username = data.Username; this.password = data.Password; + this.passwordRevisionDate = data.PasswordRevisionDate; this.totp = data.Totp; if (data.Uris) { diff --git a/src/models/data/loginData.ts b/src/models/data/loginData.ts index a7d504e052..31a4d6bcce 100644 --- a/src/models/data/loginData.ts +++ b/src/models/data/loginData.ts @@ -1,5 +1,4 @@ import { LoginApi } from '../api/loginApi'; -import { LoginUriApi } from '../api/loginUriApi'; import { LoginUriData } from './loginUriData'; @@ -7,6 +6,7 @@ export class LoginData { uris: LoginUriData[]; username: string; password: string; + passwordRevisionDate?: Date; totp: string; constructor(data?: LoginApi) { @@ -16,6 +16,7 @@ export class LoginData { this.username = data.username; this.password = data.password; + this.passwordRevisionDate = data.passwordRevisionDate; this.totp = data.totp; if (data.uris) { diff --git a/src/models/domain/login.ts b/src/models/domain/login.ts index 3d81b0eb09..23fc207335 100644 --- a/src/models/domain/login.ts +++ b/src/models/domain/login.ts @@ -2,7 +2,6 @@ import { LoginUri } from './loginUri'; import { LoginData } from '../data/loginData'; -import { LoginUriView } from '../view/loginUriView'; import { LoginView } from '../view/loginView'; import { CipherString } from './cipherString'; @@ -12,6 +11,7 @@ export class Login extends Domain { uris: LoginUri[]; username: CipherString; password: CipherString; + passwordRevisionDate?: Date; totp: CipherString; constructor(obj?: LoginData, alreadyEncrypted: boolean = false) { @@ -20,6 +20,7 @@ export class Login extends Domain { return; } + this.passwordRevisionDate = obj.passwordRevisionDate; this.buildDomainModel(this, obj, { username: null, password: null, @@ -54,6 +55,7 @@ export class Login extends Domain { toLoginData(): LoginData { const l = new LoginData(); + l.passwordRevisionDate = this.passwordRevisionDate; this.buildDataModel(this, l, { username: null, password: null, diff --git a/src/models/request/cipherRequest.ts b/src/models/request/cipherRequest.ts index 01a4a9add9..fa2caa326d 100644 --- a/src/models/request/cipherRequest.ts +++ b/src/models/request/cipherRequest.ts @@ -39,6 +39,7 @@ export class CipherRequest { uris: null, username: cipher.login.username ? cipher.login.username.encryptedString : null, password: cipher.login.password ? cipher.login.password.encryptedString : null, + passwordRevisionDate: cipher.login.passwordRevisionDate, totp: cipher.login.totp ? cipher.login.totp.encryptedString : null, }; diff --git a/src/models/view/loginView.ts b/src/models/view/loginView.ts index c36540fec6..e0c7b06bf0 100644 --- a/src/models/view/loginView.ts +++ b/src/models/view/loginView.ts @@ -3,16 +3,19 @@ import { View } from './view'; import { Login } from '../domain/login'; -import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; - export class LoginView implements View { username: string; password: string; + passwordRevisionDate?: Date; totp: string; uris: LoginUriView[]; constructor(l?: Login) { - // ctor + if (!l) { + return; + } + + this.passwordRevisionDate = l.passwordRevisionDate; } get uri(): string { diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index fdb1f5ceb5..063cebf2c9 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -69,12 +69,15 @@ export class CipherService implements CipherServiceAbstraction { const existingCipher = await (await this.get(model.id)).decrypt(); if (existingCipher != null) { model.passwordHistory = existingCipher.passwordHistory || []; - if (model.type === CipherType.Login && existingCipher.type === CipherType.Login && - existingCipher.login.password !== model.login.password) { - const ph = new PasswordHistoryView(null); - ph.password = existingCipher.login.password; - ph.lastUsedDate = new Date(); - model.passwordHistory.splice(0, 0, ph); + if (model.type === CipherType.Login && existingCipher.type === CipherType.Login) { + if (existingCipher.login.password !== model.login.password) { + const ph = new PasswordHistoryView(); + ph.password = existingCipher.login.password; + ph.lastUsedDate = model.login.passwordRevisionDate = new Date(); + model.passwordHistory.splice(0, 0, ph); + } else { + model.login.passwordRevisionDate = existingCipher.login.passwordRevisionDate; + } } if (existingCipher.hasFields) { const existingHiddenFields = existingCipher.fields.filter((f) => f.type === FieldType.Hidden); @@ -83,7 +86,7 @@ export class CipherService implements CipherServiceAbstraction { existingHiddenFields.forEach((ef) => { const matchedField = hiddenFields.filter((f) => f.name === ef.name); if (matchedField.length === 0 || matchedField[0].value !== ef.value) { - const ph = new PasswordHistoryView(null); + const ph = new PasswordHistoryView(); ph.password = ef.name + ': ' + ef.value; ph.lastUsedDate = new Date(); model.passwordHistory.splice(0, 0, ph); @@ -767,6 +770,7 @@ export class CipherService implements CipherServiceAbstraction { switch (cipher.type) { case CipherType.Login: cipher.login = new Login(); + cipher.login.passwordRevisionDate = model.login.passwordRevisionDate; await this.encryptObjProperty(model.login, cipher.login, { username: null, password: null, From 2fcc3c51b8cad9170445d5fcaba3b239af9de9a6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 27 Jul 2018 21:52:09 -0400 Subject: [PATCH 0458/1626] dont need to check storage for lock options on get --- src/services/crypto.service.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 9cf4d673d8..f70359a4e6 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -83,11 +83,6 @@ export class CryptoService implements CryptoServiceAbstraction { 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 != null) { this.key = new SymmetricCryptoKey(Utils.fromB64ToArray(key).buffer); From 0b29dc10bf70718a9e1cd6d08674068cd99ad5ee Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 27 Jul 2018 23:37:36 -0400 Subject: [PATCH 0459/1626] cipher view revision dates --- src/models/view/cipherView.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/models/view/cipherView.ts b/src/models/view/cipherView.ts index 73a8d062e4..6290993f59 100644 --- a/src/models/view/cipherView.ts +++ b/src/models/view/cipherView.ts @@ -30,6 +30,7 @@ export class CipherView implements View { fields: FieldView[]; passwordHistory: PasswordHistoryView[]; collectionIds: string[]; + revisionDate: Date; constructor(c?: Cipher) { if (!c) { @@ -45,6 +46,7 @@ export class CipherView implements View { this.type = c.type; this.localData = c.localData; this.collectionIds = c.collectionIds; + this.revisionDate = c.revisionDate; } get subTitle(): string { @@ -79,4 +81,13 @@ export class CipherView implements View { get login_username(): string { return this.login != null ? this.login.username : null; } + + get passwordRevisionDisplayDate(): Date { + if (this.login == null) { + return null; + } else if (this.login.password == null || this.login.password === '') { + return null; + } + return this.login.passwordRevisionDate != null ? this.login.passwordRevisionDate : this.revisionDate; + } } From c0f6fa2db165d0c33752c10d6649323a9b4268b4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 30 Jul 2018 10:04:20 -0400 Subject: [PATCH 0460/1626] password history component --- .../components/password-history.component.ts | 32 +++++++++++++++++++ src/angular/components/view.component.ts | 6 ++++ 2 files changed, 38 insertions(+) create mode 100644 src/angular/components/password-history.component.ts diff --git a/src/angular/components/password-history.component.ts b/src/angular/components/password-history.component.ts new file mode 100644 index 0000000000..350dc6ed78 --- /dev/null +++ b/src/angular/components/password-history.component.ts @@ -0,0 +1,32 @@ +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { OnInit } from '@angular/core'; + +import { CipherService } from '../../abstractions/cipher.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +import { PasswordHistoryView } from '../../models/view/passwordHistoryView'; + +export class PasswordHistoryComponent implements OnInit { + cipherId: string; + history: PasswordHistoryView[] = []; + + constructor(protected cipherService: CipherService, protected analytics: Angulartics2, + protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, + protected toasterService: ToasterService, private win: Window) { } + + async ngOnInit() { + const cipher = await this.cipherService.get(this.cipherId); + const decCipher = await cipher.decrypt(); + this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory; + } + + copy(password: string) { + this.analytics.eventTrack.next({ action: 'Copied Password History' }); + const copyOptions = this.win != null ? { doc: this.win.document } : null; + this.platformUtilsService.copyToClipboard(password, copyOptions); + this.toasterService.popAsync('info', null, this.i18nService.t('valueCopied', this.i18nService.t('password'))); + } +} diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index a7325893dc..15a773b23c 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -27,6 +27,7 @@ import { LoginUriView } from '../../models/view/loginUriView'; export class ViewComponent implements OnDestroy { @Input() cipherId: string; @Output() onEditCipher = new EventEmitter(); + @Output() onViewCipherPasswordHistory = new EventEmitter(); cipher: CipherView; showPassword: boolean; @@ -159,6 +160,11 @@ export class ViewComponent implements OnDestroy { a.downloading = false; } + viewHistory() { + this.analytics.eventTrack.next({ action: 'View Password History' }); + this.onViewCipherPasswordHistory.emit(this.cipher); + } + private cleanUp() { this.totpCode = null; this.cipher = null; From b21cb789da2f93a36908cdfb0dba6e701102b87d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 30 Jul 2018 10:50:22 -0400 Subject: [PATCH 0461/1626] dont make date obj from revision date --- src/models/response/cipherResponse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/response/cipherResponse.ts b/src/models/response/cipherResponse.ts index c2c9a67242..3d2770fb82 100644 --- a/src/models/response/cipherResponse.ts +++ b/src/models/response/cipherResponse.ts @@ -37,7 +37,7 @@ export class CipherResponse { this.favorite = response.Favorite || false; this.edit = response.Edit || true; this.organizationUseTotp = response.OrganizationUseTotp; - this.revisionDate = new Date(response.RevisionDate); + this.revisionDate = response.RevisionDate; if (response.Login != null) { this.login = new LoginApi(response.Login); From a5d1bb88a706835ed1aa57b72623e8fcf7ed396c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 30 Jul 2018 10:56:33 -0400 Subject: [PATCH 0462/1626] move view history method to just desktop --- src/angular/components/view.component.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index 15a773b23c..20080d9d28 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -160,11 +160,6 @@ export class ViewComponent implements OnDestroy { a.downloading = false; } - viewHistory() { - this.analytics.eventTrack.next({ action: 'View Password History' }); - this.onViewCipherPasswordHistory.emit(this.cipher); - } - private cleanUp() { this.totpCode = null; this.cipher = null; From 557b2fc3f093ee1e2eb4515c2e33c613f083c327 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 30 Jul 2018 10:58:47 -0400 Subject: [PATCH 0463/1626] move onViewCipherPasswordHistory to desktop --- src/angular/components/view.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index 20080d9d28..a7325893dc 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -27,7 +27,6 @@ import { LoginUriView } from '../../models/view/loginUriView'; export class ViewComponent implements OnDestroy { @Input() cipherId: string; @Output() onEditCipher = new EventEmitter(); - @Output() onViewCipherPasswordHistory = new EventEmitter(); cipher: CipherView; showPassword: boolean; From cfa4664b3182840ec3f8dac6da8e8562d11468d3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 30 Jul 2018 16:40:16 -0400 Subject: [PATCH 0464/1626] null or empty on password changed checks --- src/services/cipher.service.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 063cebf2c9..70351dfef1 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -70,7 +70,8 @@ export class CipherService implements CipherServiceAbstraction { if (existingCipher != null) { model.passwordHistory = existingCipher.passwordHistory || []; if (model.type === CipherType.Login && existingCipher.type === CipherType.Login) { - if (existingCipher.login.password !== model.login.password) { + if (existingCipher.login.password != null && existingCipher.login.password !== '' && + existingCipher.login.password !== model.login.password) { const ph = new PasswordHistoryView(); ph.password = existingCipher.login.password; ph.lastUsedDate = model.login.passwordRevisionDate = new Date(); @@ -80,9 +81,10 @@ export class CipherService implements CipherServiceAbstraction { } } if (existingCipher.hasFields) { - const existingHiddenFields = existingCipher.fields.filter((f) => f.type === FieldType.Hidden); + const existingHiddenFields = existingCipher.fields.filter((f) => f.type === FieldType.Hidden && + f.name != null && f.name !== '' && f.value != null && f.value !== ''); const hiddenFields = model.fields == null ? [] : - model.fields.filter((f) => f.type === FieldType.Hidden); + model.fields.filter((f) => f.type === FieldType.Hidden && f.name != null && f.name !== ''); existingHiddenFields.forEach((ef) => { const matchedField = hiddenFields.filter((f) => f.name === ef.name); if (matchedField.length === 0 || matchedField[0].value !== ef.value) { From 13769a7fcba0d6ba67519b78e590019573776993 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 30 Jul 2018 22:01:21 -0400 Subject: [PATCH 0465/1626] dont use cipher revisionDate --- src/models/view/cipherView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/view/cipherView.ts b/src/models/view/cipherView.ts index 6290993f59..c9ef130048 100644 --- a/src/models/view/cipherView.ts +++ b/src/models/view/cipherView.ts @@ -88,6 +88,6 @@ export class CipherView implements View { } else if (this.login.password == null || this.login.password === '') { return null; } - return this.login.passwordRevisionDate != null ? this.login.passwordRevisionDate : this.revisionDate; + return this.login.passwordRevisionDate; } } From 2045e7047a66599b2c8a92b88cd0d1b8bfc5186f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 30 Jul 2018 23:29:30 -0400 Subject: [PATCH 0466/1626] add support for md5 hash crypto function --- spec/node/services/nodeCryptoFunction.service.spec.ts | 8 +++++++- spec/web/services/webCryptoFunction.service.spec.ts | 7 ++++++- src/abstractions/cryptoFunction.service.ts | 2 +- src/services/nodeCryptoFunction.service.ts | 2 +- src/services/webCryptoFunction.service.ts | 11 +++++++---- 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/spec/node/services/nodeCryptoFunction.service.spec.ts b/spec/node/services/nodeCryptoFunction.service.spec.ts index 5f79750d5f..8b58312da6 100644 --- a/spec/node/services/nodeCryptoFunction.service.spec.ts +++ b/spec/node/services/nodeCryptoFunction.service.spec.ts @@ -65,9 +65,14 @@ describe('NodeCrypto Function Service', () => { const unicode512Hash = '2b16a5561af8ad6fe414cc103fc8036492e1fc6d9aabe1b655497054f760fe0e34c5d100ac773d' + '9f3030438284f22dbfa20cb2e9b019f2c98dfe38ce1ef41bae'; + const regularMd5 = '5eceffa53a5fd58c44134211e2c5f522'; + const utf8Md5 = '3abc9433c09551b939c80aa0aa3174e1'; + const unicodeMd5 = '85ae134072c8d81257933f7045ba17ca'; + testHash('sha1', regular1Hash, utf81Hash, unicode1Hash); testHash('sha256', regular256Hash, utf8256Hash, unicode256Hash); testHash('sha512', regular512Hash, utf8512Hash, unicode512Hash); + testHash('md5', regularMd5, utf8Md5, unicodeMd5); }); describe('hmac', () => { @@ -229,7 +234,8 @@ function testPbkdf2(algorithm: 'sha256' | 'sha512', regularKey: string, utf8Key: }); } -function testHash(algorithm: 'sha1' | 'sha256' | 'sha512', regularHash: string, utf8Hash: string, unicodeHash: string) { +function testHash(algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5', regularHash: string, + utf8Hash: string, unicodeHash: string) { const regularValue = 'HashMe!!'; const utf8Value = 'HǻshMe!!'; const unicodeValue = '😀HashMe!!!🙏'; diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index 7be59e73db..f3ad003bf8 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -71,9 +71,14 @@ describe('WebCrypto Function Service', () => { const unicode512Hash = '2b16a5561af8ad6fe414cc103fc8036492e1fc6d9aabe1b655497054f760fe0e34c5d100ac773d' + '9f3030438284f22dbfa20cb2e9b019f2c98dfe38ce1ef41bae'; + const regularMd5 = '5eceffa53a5fd58c44134211e2c5f522'; + const utf8Md5 = '3abc9433c09551b939c80aa0aa3174e1'; + const unicodeMd5 = '85ae134072c8d81257933f7045ba17ca'; + testHash('sha1', regular1Hash, utf81Hash, unicode1Hash); testHash('sha256', regular256Hash, utf8256Hash, unicode256Hash); testHash('sha512', regular512Hash, utf8512Hash, unicode512Hash); + testHash('md5', regularMd5, utf8Md5, unicodeMd5); }); describe('hmac', () => { @@ -315,7 +320,7 @@ function testPbkdf2(algorithm: 'sha256' | 'sha512', regularKey: string, }); } -function testHash(algorithm: 'sha1' | 'sha256' | 'sha512', regularHash: string, +function testHash(algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5', regularHash: string, utf8Hash: string, unicodeHash: string) { const regularValue = 'HashMe!!'; const utf8Value = 'HǻshMe!!'; diff --git a/src/abstractions/cryptoFunction.service.ts b/src/abstractions/cryptoFunction.service.ts index 72b64fac57..45b1014a29 100644 --- a/src/abstractions/cryptoFunction.service.ts +++ b/src/abstractions/cryptoFunction.service.ts @@ -4,7 +4,7 @@ import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; export abstract class CryptoFunctionService { pbkdf2: (password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', iterations: number) => Promise; - hash: (value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise; + hash: (value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5') => Promise; hmac: (value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise; compare: (a: ArrayBuffer, b: ArrayBuffer) => Promise; hmacFast: (value: ArrayBuffer | string, key: ArrayBuffer | string, algorithm: 'sha1' | 'sha256' | 'sha512') => diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index eedacf5374..d88be72d99 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -26,7 +26,7 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { }); } - hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { + hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5'): Promise { const nodeValue = this.toNodeValue(value); const hash = crypto.createHash(algorithm); hash.update(nodeValue); diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index 0c6c23d4e9..aa8d3f47ae 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -47,9 +47,9 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return await this.subtle.deriveBits(pbkdf2Params, impKey, wcLen); } - async hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { - if ((this.isEdge || this.isIE) && algorithm === 'sha1') { - const md = forge.md.sha1.create(); + async hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5'): Promise { + if (((this.isEdge || this.isIE) && algorithm === 'sha1') || algorithm === 'md5') { + const md = algorithm === 'md5' ? forge.md.md5.create() : forge.md.sha1.create(); const valueBytes = this.toByteString(value); md.update(valueBytes, 'raw'); return Utils.fromByteStringToArray(md.digest().data).buffer; @@ -263,7 +263,10 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return bytes; } - private toWebCryptoAlgorithm(algorithm: 'sha1' | 'sha256' | 'sha512'): string { + private toWebCryptoAlgorithm(algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5'): string { + if (algorithm === 'md5') { + throw new Error('MD5 is not supported in WebCrypto.'); + } return algorithm === 'sha1' ? 'SHA-1' : algorithm === 'sha256' ? 'SHA-256' : 'SHA-512'; } } From 41ab22a82fb5fafcce2e8d1abe86789e152ef1fa Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 31 Jul 2018 11:25:50 -0400 Subject: [PATCH 0467/1626] support for otpauth:// urls for totp codes --- src/abstractions/totp.service.ts | 3 +- src/angular/components/view.component.ts | 20 +++--- src/misc/utils.ts | 46 +++++++++----- src/services/totp.service.ts | 78 ++++++++++++++++++------ 4 files changed, 108 insertions(+), 39 deletions(-) diff --git a/src/abstractions/totp.service.ts b/src/abstractions/totp.service.ts index 1155d3c2f7..608c7d1b3d 100644 --- a/src/abstractions/totp.service.ts +++ b/src/abstractions/totp.service.ts @@ -1,4 +1,5 @@ export abstract class TotpService { - getCode: (keyb32: string) => Promise; + getCode: (key: string) => Promise; + getTimeInterval: (key: string) => number; isAutoCopyEnabled: () => Promise; } diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index a7325893dc..b05fd6a76d 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -63,10 +63,11 @@ export class ViewComponent implements OnDestroy { if (this.cipher.type === CipherType.Login && this.cipher.login.totp && (cipher.organizationUseTotp || this.isPremium)) { await this.totpUpdateCode(); - await this.totpTick(); + const interval = this.totpService.getTimeInterval(this.cipher.login.totp); + await this.totpTick(interval); this.totpInterval = setInterval(async () => { - await this.totpTick(); + await this.totpTick(interval); }, 1000); } } @@ -178,7 +179,12 @@ export class ViewComponent implements OnDestroy { this.totpCode = await this.totpService.getCode(this.cipher.login.totp); if (this.totpCode != null) { - this.totpCodeFormatted = this.totpCode.substring(0, 3) + ' ' + this.totpCode.substring(3); + if (this.totpCode.length > 4) { + const half = Math.floor(this.totpCode.length / 2); + this.totpCodeFormatted = this.totpCode.substring(0, half) + ' ' + this.totpCode.substring(half); + } else { + this.totpCodeFormatted = this.totpCode; + } } else { this.totpCodeFormatted = null; if (this.totpInterval) { @@ -187,12 +193,12 @@ export class ViewComponent implements OnDestroy { } } - private async totpTick() { + private async totpTick(intervalSeconds: number) { const epoch = Math.round(new Date().getTime() / 1000.0); - const mod = epoch % 30; + const mod = epoch % intervalSeconds; - this.totpSec = 30 - mod; - this.totpDash = +(Math.round(((2.62 * mod) + 'e+2') as any) + 'e-2'); + this.totpSec = intervalSeconds - mod; + this.totpDash = +(Math.round((((78.6 / intervalSeconds) * mod) + 'e+2') as any) + 'e-2'); this.totpLow = this.totpSec <= 7; if (mod === 0) { await this.totpUpdateCode(); diff --git a/src/misc/utils.ts b/src/misc/utils.ts index b507f2aba1..67627cb4b0 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -151,6 +151,23 @@ export class Utils { return url != null ? url.host : null; } + static getQueryParams(uriString: string): Map { + const url = Utils.getUrl(uriString); + if (url == null || url.search == null || url.search === '') { + return null; + } + const map = new Map(); + const pairs = (url.search[0] === '?' ? url.search.substr(1) : url.search).split('&'); + pairs.forEach((pair) => { + const parts = pair.split('='); + if (parts.length < 1) { + return; + } + map.set(decodeURIComponent(parts[0]).toLowerCase(), parts[1] == null ? '' : decodeURIComponent(parts[1])); + }); + return map; + } + static getSortFunction(i18nService: I18nService, prop: string) { return (a: any, b: any) => { if (a[prop] == null && b[prop] != null) { @@ -178,23 +195,24 @@ export class Utils { return null; } - if (uriString.indexOf('://') === -1 && uriString.indexOf('.') > -1) { + const hasProtocol = uriString.indexOf('://') > -1; + if (!hasProtocol && uriString.indexOf('.') > -1) { uriString = 'http://' + uriString; + } else if (!hasProtocol) { + return null; } - if (uriString.startsWith('http://') || uriString.startsWith('https://')) { - try { - if (nodeURL != null) { - return new nodeURL(uriString); - } else if (typeof URL === 'function') { - return new URL(uriString); - } else if (window != null) { - const anchor = window.document.createElement('a'); - anchor.href = uriString; - return anchor as any; - } - } catch (e) { } - } + try { + if (nodeURL != null) { + return new nodeURL(uriString); + } else if (typeof URL === 'function') { + return new URL(uriString); + } else if (window != null) { + const anchor = window.document.createElement('a'); + anchor.href = uriString; + return anchor as any; + } + } catch (e) { } return null; } diff --git a/src/services/totp.service.ts b/src/services/totp.service.ts index 3a8fb3b896..233ddde406 100644 --- a/src/services/totp.service.ts +++ b/src/services/totp.service.ts @@ -9,30 +9,78 @@ import { Utils } from '../misc/utils'; const b32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; export class TotpService implements TotpServiceAbstraction { - constructor(private storageService: StorageService, private cryptoFunctionService: CryptoFunctionService) {} + constructor(private storageService: StorageService, private cryptoFunctionService: CryptoFunctionService) { } + + async getCode(key: string): Promise { + let period = 30; + let alg: 'sha1' | 'sha256' | 'sha512' = 'sha1'; + let digits = 6; + let keyB32 = key; + if (key.indexOf('otpauth://') === 0) { + const params = Utils.getQueryParams(key); + if (params.has('digits') && params.get('digits') != null) { + try { + const digitParams = parseInt(params.get('digits').trim(), null); + if (digitParams > 10) { + digits = 10; + } else if (digitParams > 0) { + digits = digitParams; + } + } catch { } + } + if (params.has('period') && params.get('period') != null) { + try { + period = parseInt(params.get('period').trim(), null); + } catch { } + } + if (params.has('secret') && params.get('secret') != null) { + keyB32 = params.get('secret'); + } + if (params.has('algorithm') && params.get('algorithm') != null) { + const algParam = params.get('algorithm').toLowerCase(); + if (algParam === 'sha1' || algParam === 'sha256' || algParam === 'sha512') { + alg = algParam; + } + } + } - async getCode(keyb32: string): Promise { const epoch = Math.round(new Date().getTime() / 1000.0); - const timeHex = this.leftpad(this.dec2hex(Math.floor(epoch / 30)), 16, '0'); + const timeHex = this.leftpad(this.dec2hex(Math.floor(epoch / period)), 16, '0'); const timeBytes = Utils.fromHexToArray(timeHex); - const keyBytes = this.b32tobytes(keyb32); + const keyBytes = this.b32tobytes(keyB32); if (!keyBytes.length || !timeBytes.length) { return null; } - const hashHex = await this.sign(keyBytes, timeBytes); - if (!hashHex) { + const hash = await this.sign(keyBytes, timeBytes, alg); + if (hash.length === 0) { return null; } - const offset = this.hex2dec(hashHex.substring(hashHex.length - 1)); - // tslint:disable-next-line - let otp = (this.hex2dec(hashHex.substr(offset * 2, 8)) & this.hex2dec('7fffffff')) + ''; - otp = (otp).substr(otp.length - 6, 6); + /* tslint:disable */ + const offset = (hash[hash.length - 1] & 0xf); + const binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) | + ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff); + /* tslint:enable */ + let otp = (binary % Math.pow(10, digits)).toString(); + otp = this.leftpad(otp, digits, '0'); return otp; } + getTimeInterval(key: string): number { + let period = 30; + if (key.indexOf('otpauth://') === 0) { + const params = Utils.getQueryParams(key); + if (params.has('period') && params.get('period') != null) { + try { + period = parseInt(params.get('period').trim(), null); + } catch { } + } + } + return period; + } + async isAutoCopyEnabled(): Promise { return !(await this.storageService.get(ConstantsService.disableAutoTotpCopyKey)); } @@ -50,10 +98,6 @@ export class TotpService implements TotpServiceAbstraction { return (d < 15.5 ? '0' : '') + Math.round(d).toString(16); } - private hex2dec(s: string): number { - return parseInt(s, 16); - } - private b32tohex(s: string): string { s = s.toUpperCase(); let cleanedInput = ''; @@ -87,8 +131,8 @@ export class TotpService implements TotpServiceAbstraction { return Utils.fromHexToArray(this.b32tohex(s)); } - private async sign(keyBytes: Uint8Array, timeBytes: Uint8Array) { - const signature = await this.cryptoFunctionService.hmac(timeBytes.buffer, keyBytes.buffer, 'sha1'); - return Utils.fromBufferToHex(signature); + private async sign(keyBytes: Uint8Array, timeBytes: Uint8Array, alg: 'sha1' | 'sha256' | 'sha512') { + const signature = await this.cryptoFunctionService.hmac(timeBytes.buffer, keyBytes.buffer, alg); + return new Uint8Array(signature); } } From ee13a562f97496e59efc8f2a2e382cfcb7ba7184 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 31 Jul 2018 11:35:04 -0400 Subject: [PATCH 0468/1626] check that period is positive value --- src/services/totp.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/services/totp.service.ts b/src/services/totp.service.ts index 233ddde406..9b4437544d 100644 --- a/src/services/totp.service.ts +++ b/src/services/totp.service.ts @@ -30,7 +30,10 @@ export class TotpService implements TotpServiceAbstraction { } if (params.has('period') && params.get('period') != null) { try { - period = parseInt(params.get('period').trim(), null); + const periodParam = parseInt(params.get('period').trim(), null); + if (periodParam > 0) { + period = periodParam; + } } catch { } } if (params.has('secret') && params.get('secret') != null) { From a1112988c4624219461acf8269058353203969c2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 31 Jul 2018 14:37:39 -0400 Subject: [PATCH 0469/1626] null key checks --- src/services/totp.service.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/services/totp.service.ts b/src/services/totp.service.ts index 9b4437544d..293810112b 100644 --- a/src/services/totp.service.ts +++ b/src/services/totp.service.ts @@ -12,11 +12,14 @@ export class TotpService implements TotpServiceAbstraction { constructor(private storageService: StorageService, private cryptoFunctionService: CryptoFunctionService) { } async getCode(key: string): Promise { + if (key == null) { + return null; + } let period = 30; let alg: 'sha1' | 'sha256' | 'sha512' = 'sha1'; let digits = 6; let keyB32 = key; - if (key.indexOf('otpauth://') === 0) { + if (key.toLowerCase().indexOf('otpauth://') === 0) { const params = Utils.getQueryParams(key); if (params.has('digits') && params.get('digits') != null) { try { @@ -73,7 +76,7 @@ export class TotpService implements TotpServiceAbstraction { getTimeInterval(key: string): number { let period = 30; - if (key.indexOf('otpauth://') === 0) { + if (key != null && key.toLowerCase().indexOf('otpauth://') === 0) { const params = Utils.getQueryParams(key); if (params.has('period') && params.get('period') != null) { try { From 76c89f01ef993e572d8c2cbb057cf11f1ea21742 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 31 Jul 2018 23:49:15 -0400 Subject: [PATCH 0470/1626] new constant --- src/services/constants.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index efc966759b..987f53ce48 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -2,6 +2,7 @@ export class ConstantsService { static readonly environmentUrlsKey: string = 'environmentUrls'; static readonly disableGaKey: string = 'disableGa'; static readonly disableAddLoginNotificationKey: string = 'disableAddLoginNotification'; + static readonly disableChangedPasswordNotificationKey: string = 'disableChangedPasswordNotification'; static readonly disableContextMenuItemKey: string = 'disableContextMenuItem'; static readonly disableFaviconKey: string = 'disableFavicon'; static readonly disableAutoTotpCopyKey: string = 'disableAutoTotpCopy'; From a26527b500c01fa11931f68d76dfbcf32c442d4e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 1 Aug 2018 09:13:49 -0400 Subject: [PATCH 0471/1626] is mobile browser checks on autofocus --- src/angular/directives/autofocus.directive.ts | 4 +++- src/misc/utils.ts | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/angular/directives/autofocus.directive.ts b/src/angular/directives/autofocus.directive.ts index dd708e5ae4..a999d86ed5 100644 --- a/src/angular/directives/autofocus.directive.ts +++ b/src/angular/directives/autofocus.directive.ts @@ -4,6 +4,8 @@ import { Input, } from '@angular/core'; +import { Utils } from '../../misc/utils'; + @Directive({ selector: '[appAutofocus]', }) @@ -17,7 +19,7 @@ export class AutofocusDirective { constructor(private el: ElementRef) { } ngOnInit() { - if (this.autofocus) { + if (!Utils.isMobileBrowser && this.autofocus) { this.el.nativeElement.focus(); } } diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 67627cb4b0..d2ccc006a4 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -7,6 +7,7 @@ export class Utils { static inited = false; static isNode = false; static isBrowser = true; + static isMobileBrowser = false; static global: NodeJS.Global | Window = null; static init() { @@ -18,6 +19,7 @@ export class Utils { Utils.isNode = typeof process !== 'undefined' && (process as any).release != null && (process as any).release.name === 'node'; Utils.isBrowser = typeof window !== 'undefined'; + Utils.isMobileBrowser = Utils.isBrowser && this.isMobile(window); Utils.global = Utils.isNode && !Utils.isBrowser ? global : window; } @@ -185,6 +187,17 @@ export class Utils { }; } + private static isMobile(win: Window) { + let mobile = false; + ((a) => { + // tslint:disable-next-line + if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) { + mobile = true; + } + })(win.navigator.userAgent || win.navigator.vendor || (win as any).opera); + return mobile || win.navigator.userAgent.match(/iPad/i) != null; + } + private static getUrl(uriString: string): URL { if (uriString == null) { return null; From 370952971ae5421802be97aa466492217620841d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 1 Aug 2018 16:50:34 -0400 Subject: [PATCH 0472/1626] make billing expiration a date object --- src/models/response/organizationBillingResponse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/response/organizationBillingResponse.ts b/src/models/response/organizationBillingResponse.ts index db5e5ca2bb..bfc288635b 100644 --- a/src/models/response/organizationBillingResponse.ts +++ b/src/models/response/organizationBillingResponse.ts @@ -27,6 +27,6 @@ export class OrganizationBillingResponse extends OrganizationResponse { if (response.Charges != null) { this.charges = response.Charges.map((c: any) => new BillingChargeResponse(c)); } - this.expiration = response.Expiration; + this.expiration = new Date(response.Expiration); } } From 6f64c5cb5a90354d1a76b5270378030dc99d325b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 1 Aug 2018 16:53:48 -0400 Subject: [PATCH 0473/1626] null check --- src/models/response/organizationBillingResponse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/response/organizationBillingResponse.ts b/src/models/response/organizationBillingResponse.ts index bfc288635b..1cd35e8dc6 100644 --- a/src/models/response/organizationBillingResponse.ts +++ b/src/models/response/organizationBillingResponse.ts @@ -27,6 +27,6 @@ export class OrganizationBillingResponse extends OrganizationResponse { if (response.Charges != null) { this.charges = response.Charges.map((c: any) => new BillingChargeResponse(c)); } - this.expiration = new Date(response.Expiration); + this.expiration = response.Expiration != null ? new Date(response.Expiration) : null; } } From 49d3f227042bc090dec48f83bbbf7da3029828a5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 2 Aug 2018 08:46:08 -0400 Subject: [PATCH 0474/1626] format multiple error messages when validating --- src/angular/services/validation.service.ts | 35 ++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/angular/services/validation.service.ts b/src/angular/services/validation.service.ts index ddcc37afff..dbef4e9a7e 100644 --- a/src/angular/services/validation.service.ts +++ b/src/angular/services/validation.service.ts @@ -1,12 +1,21 @@ -import { Injectable } from '@angular/core'; +import { + Injectable, + SecurityContext, +} from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; -import { ToasterService } from 'angular2-toaster'; +import { + BodyOutputType, + Toast, + ToasterService, +} from 'angular2-toaster'; import { I18nService } from '../../abstractions/i18n.service'; @Injectable() export class ValidationService { - constructor(private toasterService: ToasterService, private i18nService: I18nService) { } + constructor(private toasterService: ToasterService, private i18nService: I18nService, + private sanitizer: DomSanitizer) { } showError(data: any): string[] { const defaultErrorMessage = this.i18nService.t('unexpectedError'); @@ -25,13 +34,29 @@ export class ValidationService { } data.validationErrors[key].forEach((item: string) => { - errors.push(item); + let prefix = ''; + if (key.indexOf('[') > -1 && key.indexOf(']') > -1) { + const lastSep = key.lastIndexOf('.'); + prefix = key.substr(0, lastSep > -1 ? lastSep : key.length) + ': '; + } + errors.push(prefix + item); }); } } - if (errors.length > 0) { + if (errors.length === 1) { this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), errors[0]); + } else if (errors.length > 1) { + let errorMessage = ''; + errors.forEach((e) => errorMessage += ('

' + this.sanitizer.sanitize(SecurityContext.HTML, e) + '

')); + const toast: Toast = { + type: 'error', + title: this.i18nService.t('errorOccurred'), + body: errorMessage, + bodyOutputType: BodyOutputType.TrustedHtml, + timeout: 5000 * errors.length, + }; + this.toasterService.popAsync(toast); } return errors; From a5476f12aa156f8375d137ca3ac3ddce6d4ca7cc Mon Sep 17 00:00:00 2001 From: Brandon Davis Date: Mon, 6 Aug 2018 09:37:56 -0400 Subject: [PATCH 0475/1626] feature/cli-18-import (#8) * feature/cli-18-import * updates to jslib --- src/abstractions/import.service.ts | 7 + src/misc/utils.ts | 8 +- src/services/cipher.service.ts | 4 +- src/services/import.service.ts | 230 +++++++++++++++++++++++++++++ 4 files changed, 243 insertions(+), 6 deletions(-) create mode 100644 src/abstractions/import.service.ts create mode 100644 src/services/import.service.ts diff --git a/src/abstractions/import.service.ts b/src/abstractions/import.service.ts new file mode 100644 index 0000000000..b6ad65419f --- /dev/null +++ b/src/abstractions/import.service.ts @@ -0,0 +1,7 @@ +import { Importer } from '../importers/importer'; +export type ImportOptions = Array<{id: string, name: string}>; +export abstract class ImportService { + submit: (importer: Importer, fileContents: string) => Promise; + getOptions: () => ImportOptions; + getImporter: (format: string) => Importer; +} diff --git a/src/misc/utils.ts b/src/misc/utils.ts index d2ccc006a4..3e3f3c0b43 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -71,7 +71,7 @@ export class Utils { static fromBufferToB64(buffer: ArrayBuffer): string { if (Utils.isNode) { - return new Buffer(buffer).toString('base64'); + return Buffer.from(buffer).toString('base64'); } else { let binary = ''; const bytes = new Uint8Array(buffer); @@ -84,7 +84,7 @@ export class Utils { static fromBufferToUtf8(buffer: ArrayBuffer): string { if (Utils.isNode) { - return new Buffer(buffer).toString('utf8'); + return Buffer.from(buffer).toString('utf8'); } else { const bytes = new Uint8Array(buffer); const encodedString = String.fromCharCode.apply(null, bytes); @@ -99,7 +99,7 @@ export class Utils { // ref: https://stackoverflow.com/a/40031979/1090359 static fromBufferToHex(buffer: ArrayBuffer): string { if (Utils.isNode) { - return new Buffer(buffer).toString('hex'); + return Buffer.from(buffer).toString('hex'); } else { const bytes = new Uint8Array(buffer); return Array.prototype.map.call(bytes, (x: number) => ('00' + x.toString(16)).slice(-2)).join(''); @@ -126,7 +126,7 @@ export class Utils { static fromB64ToUtf8(b64Str: string): string { if (Utils.isNode) { - return new Buffer(b64Str, 'base64').toString('utf8'); + return Buffer.from(b64Str, 'base64').toString('utf8'); } else { return decodeURIComponent(escape(window.atob(b64Str))); } diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 70351dfef1..6fb6c3ab5e 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -477,7 +477,7 @@ export class CipherService implements CipherServiceAbstraction { fd.append('data', blob, encFileName.encryptedString); } catch (e) { if (Utils.isNode && !Utils.isBrowser) { - fd.append('data', new Buffer(encData) as any, { + fd.append('data', Buffer.from(encData) as any, { filepath: encFileName.encryptedString, contentType: 'application/octet-stream', } as any); @@ -526,7 +526,7 @@ export class CipherService implements CipherServiceAbstraction { fd.append('data', blob, encFileName.encryptedString); } catch (e) { if (Utils.isNode && !Utils.isBrowser) { - fd.append('data', new Buffer(encData) as any, { + fd.append('data', Buffer.from(encData) as any, { filepath: encFileName.encryptedString, contentType: 'application/octet-stream', } as any); diff --git a/src/services/import.service.ts b/src/services/import.service.ts new file mode 100644 index 0000000000..76a22e1b8c --- /dev/null +++ b/src/services/import.service.ts @@ -0,0 +1,230 @@ +import { ApiService } from '../abstractions/api.service'; +import { CipherService } from '../abstractions/cipher.service'; +import { FolderService } from '../abstractions/folder.service'; +import { I18nService } from '../abstractions/i18n.service'; +import { + ImportOptions, + ImportService as ImportServiceAbstraction, +} from '../abstractions/import.service'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CipherRequest } from '../models/request/cipherRequest'; +import { FolderRequest } from '../models/request/folderRequest'; +import { ImportCiphersRequest } from '../models/request/importCiphersRequest'; +import { KvpRequest } from '../models/request/kvpRequest'; + +import { CipherView } from '../models/view/cipherView'; + +import { AscendoCsvImporter } from '../importers/ascendoCsvImporter'; +import { AviraCsvImporter } from '../importers/aviraCsvImporter'; +import { BitwardenCsvImporter } from '../importers/bitwardenCsvImporter'; +import { BlurCsvImporter } from '../importers/blurCsvImporter'; +import { ChromeCsvImporter } from '../importers/chromeCsvImporter'; +import { ClipperzHtmlImporter } from '../importers/clipperzHtmlImporter'; +import { DashlaneCsvImporter } from '../importers/dashlaneCsvImporter'; +import { EnpassCsvImporter } from '../importers/enpassCsvImporter'; +import { FirefoxCsvImporter } from '../importers/firefoxCsvImporter'; +import { GnomeJsonImporter } from '../importers/gnomeJsonImporter'; +import { Importer } from '../importers/importer'; +import { KeePass2XmlImporter } from '../importers/keepass2XmlImporter'; +import { KeePassXCsvImporter } from '../importers/keepassxCsvImporter'; +import { KeeperCsvImporter } from '../importers/keeperCsvImporter'; +import { LastPassCsvImporter } from '../importers/lastpassCsvImporter'; +import { MeldiumCsvImporter } from '../importers/meldiumCsvImporter'; +import { MSecureCsvImporter } from '../importers/msecureCsvImporter'; +import { OnePassword1PifImporter } from '../importers/onepassword1PifImporter'; +import { OnePasswordWinCsvImporter } from '../importers/onepasswordWinCsvImporter'; +import { PadlockCsvImporter } from '../importers/padlockCsvImporter'; +import { PassKeepCsvImporter } from '../importers/passkeepCsvImporter'; +import { PasswordAgentCsvImporter } from '../importers/passwordAgentCsvImporter'; +import { PasswordBossJsonImporter } from '../importers/passwordBossJsonImporter'; +import { PasswordDragonXmlImporter } from '../importers/passwordDragonXmlImporter'; +import { PasswordSafeXmlImporter } from '../importers/passwordSafeXmlImporter'; +import { RoboFormCsvImporter } from '../importers/roboformCsvImporter'; +import { SafeInCloudXmlImporter } from '../importers/safeInCloudXmlImporter'; +import { SaferPassCsvImporter } from '../importers/saferpassCsvImport'; +import { SplashIdCsvImporter } from '../importers/splashIdCsvImporter'; +import { StickyPasswordXmlImporter } from '../importers/stickyPasswordXmlImporter'; +import { TrueKeyCsvImporter } from '../importers/truekeyCsvImporter'; +import { UpmCsvImporter } from '../importers/upmCsvImporter'; +import { ZohoVaultCsvImporter } from '../importers/zohoVaultCsvImporter'; + +export class ImportService implements ImportServiceAbstraction { + importOptions: ImportOptions = [ + { id: 'bitwardencsv', name: 'Bitwarden (csv)' }, + { id: 'lastpasscsv', name: 'LastPass (csv)' }, + { id: 'chromecsv', name: 'Chrome (csv)' }, + { id: 'firefoxcsv', name: 'Firefox (csv)' }, + { id: 'keepass2xml', name: 'KeePass 2 (xml)' }, + { id: '1password1pif', name: '1Password (1pif)' }, + { id: 'dashlanecsv', name: 'Dashlane (csv)' }, + { id: 'keepassxcsv', name: 'KeePassX (csv)' }, + { id: '1passwordwincsv', name: '1Password 6 and 7 Windows (csv)' }, + { id: 'roboformcsv', name: 'RoboForm (csv)' }, + { id: 'keepercsv', name: 'Keeper (csv)' }, + { id: 'enpasscsv', name: 'Enpass (csv)' }, + { id: 'safeincloudxml', name: 'SafeInCloud (xml)' }, + { id: 'pwsafexml', name: 'Password Safe (xml)' }, + { id: 'stickypasswordxml', name: 'Sticky Password (xml)' }, + { id: 'msecurecsv', name: 'mSecure (csv)' }, + { id: 'truekeycsv', name: 'True Key (csv)' }, + { id: 'passwordbossjson', name: 'Password Boss (json)' }, + { id: 'zohovaultcsv', name: 'Zoho Vault (csv)' }, + { id: 'splashidcsv', name: 'SplashID (csv)' }, + { id: 'passworddragonxml', name: 'Password Dragon (xml)' }, + { id: 'padlockcsv', name: 'Padlock (csv)' }, + { id: 'passboltcsv', name: 'Passbolt (csv)' }, + { id: 'clipperzhtml', name: 'Clipperz (html)' }, + { id: 'aviracsv', name: 'Avira (csv)' }, + { id: 'saferpasscsv', name: 'SaferPass (csv)' }, + { id: 'upmcsv', name: 'Universal Password Manager (csv)' }, + { id: 'ascendocsv', name: 'Ascendo DataVault (csv)' }, + { id: 'meldiumcsv', name: 'Meldium (csv)' }, + { id: 'passkeepcsv', name: 'PassKeep (csv)' }, + ]; + + protected successNavigate: any[] = ['vault']; + + constructor(protected cipherService: CipherService, protected folderService: FolderService, + protected apiService: ApiService, protected i18nService: I18nService) { } + + async submit(importer: Importer, fileContents: string): Promise { + if (importer === null) { + return new Error(this.i18nService.t('selectFormat')); + } + + if (fileContents == null || fileContents === '') { + return new Error(this.i18nService.t('selectFile')); + } + + const importResult = await importer.parse(fileContents); + if (importResult.success) { + if (importResult.folders.length === 0 && importResult.ciphers.length === 0) { + return new Error(this.i18nService.t('importNothingError')); + } else if (importResult.ciphers.length > 0) { + const halfway = Math.floor(importResult.ciphers.length / 2); + const last = importResult.ciphers.length - 1; + + if (this.badData(importResult.ciphers[0]) && + this.badData(importResult.ciphers[halfway]) && + this.badData(importResult.ciphers[last])) { + return new Error(this.i18nService.t('importFormatError')); + } + } + return this.postImport(importResult).then(() => { + return null; + }).catch((err) => { + return new Error(err); + }); + } else { + return new Error(this.i18nService.t('importFormatError')); + } + } + + getOptions(): ImportOptions { + return this.importOptions; + } + + getImporter(format: string): Importer { + if (format == null || format === '') { + return null; + } + + switch (format) { + case 'bitwardencsv': + return new BitwardenCsvImporter(); + case 'lastpasscsv': + case 'passboltcsv': + return new LastPassCsvImporter(); + case 'keepassxcsv': + return new KeePassXCsvImporter(); + case 'aviracsv': + return new AviraCsvImporter(); + case 'blurcsv': + return new BlurCsvImporter(); + case 'safeincloudxml': + return new SafeInCloudXmlImporter(); + case 'padlockcsv': + return new PadlockCsvImporter(); + case 'keepass2xml': + return new KeePass2XmlImporter(); + case 'chromecsv': + case 'operacsv': + case 'vivaldicsv': + return new ChromeCsvImporter(); + case 'firefoxcsv': + return new FirefoxCsvImporter(); + case 'upmcsv': + return new UpmCsvImporter(); + case 'saferpasscsv': + return new SaferPassCsvImporter(); + case 'meldiumcsv': + return new MeldiumCsvImporter(); + case '1password1pif': + return new OnePassword1PifImporter(); + case '1passwordwincsv': + return new OnePasswordWinCsvImporter(); + case 'keepercsv': + return new KeeperCsvImporter(); + case 'passworddragonxml': + return new PasswordDragonXmlImporter(); + case 'enpasscsv': + return new EnpassCsvImporter(); + case 'pwsafexml': + return new PasswordSafeXmlImporter(); + case 'dashlanecsv': + return new DashlaneCsvImporter(); + case 'msecurecsv': + return new MSecureCsvImporter(); + case 'stickypasswordxml': + return new StickyPasswordXmlImporter(); + case 'truekeycsv': + return new TrueKeyCsvImporter(); + case 'clipperzhtml': + return new ClipperzHtmlImporter(); + case 'roboformcsv': + return new RoboFormCsvImporter(); + case 'ascendocsv': + return new AscendoCsvImporter(); + case 'passwordbossjson': + return new PasswordBossJsonImporter(); + case 'zohovaultcsv': + return new ZohoVaultCsvImporter(); + case 'splashidcsv': + return new SplashIdCsvImporter(); + case 'passkeepcsv': + return new PassKeepCsvImporter(); + case 'gnomejson': + return new GnomeJsonImporter(); + case 'passwordagentcsv': + return new PasswordAgentCsvImporter(); + default: + return null; + } + } + + protected async postImport(importResult: ImportResult) { + const request = new ImportCiphersRequest(); + for (let i = 0; i < importResult.ciphers.length; i++) { + const c = await this.cipherService.encrypt(importResult.ciphers[i]); + request.ciphers.push(new CipherRequest(c)); + } + if (importResult.folders != null) { + for (let i = 0; i < importResult.folders.length; i++) { + const f = await this.folderService.encrypt(importResult.folders[i]); + request.folders.push(new FolderRequest(f)); + } + } + if (importResult.folderRelationships != null) { + importResult.folderRelationships.forEach((r) => + request.folderRelationships.push(new KvpRequest(r[0], r[1]))); + } + return await this.apiService.postImportCiphers(request); + } + + private badData(c: CipherView) { + return (c.name == null || c.name === '--') && + (c.login != null && (c.login.password == null || c.login.password === '')); + } +} From 8b26d90e742966bff3dc9e57ecb104b59e48371b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 6 Aug 2018 10:37:57 -0400 Subject: [PATCH 0476/1626] import service adjustments --- src/abstractions/import.service.ts | 10 ++++++--- src/services/import.service.ts | 34 +++++++----------------------- 2 files changed, 15 insertions(+), 29 deletions(-) diff --git a/src/abstractions/import.service.ts b/src/abstractions/import.service.ts index b6ad65419f..ff5c4a0f04 100644 --- a/src/abstractions/import.service.ts +++ b/src/abstractions/import.service.ts @@ -1,7 +1,11 @@ import { Importer } from '../importers/importer'; -export type ImportOptions = Array<{id: string, name: string}>; + +export interface ImportOption { + id: string; + name: string; +} export abstract class ImportService { - submit: (importer: Importer, fileContents: string) => Promise; - getOptions: () => ImportOptions; + importOptions: ImportOption[]; + import: (importer: Importer, fileContents: string) => Promise; getImporter: (format: string) => Importer; } diff --git a/src/services/import.service.ts b/src/services/import.service.ts index 76a22e1b8c..b45bc36986 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -3,7 +3,7 @@ import { CipherService } from '../abstractions/cipher.service'; import { FolderService } from '../abstractions/folder.service'; import { I18nService } from '../abstractions/i18n.service'; import { - ImportOptions, + ImportOption, ImportService as ImportServiceAbstraction, } from '../abstractions/import.service'; @@ -51,7 +51,7 @@ import { UpmCsvImporter } from '../importers/upmCsvImporter'; import { ZohoVaultCsvImporter } from '../importers/zohoVaultCsvImporter'; export class ImportService implements ImportServiceAbstraction { - importOptions: ImportOptions = [ + importOptions: ImportOption[] = [ { id: 'bitwardencsv', name: 'Bitwarden (csv)' }, { id: 'lastpasscsv', name: 'LastPass (csv)' }, { id: 'chromecsv', name: 'Chrome (csv)' }, @@ -84,24 +84,14 @@ export class ImportService implements ImportServiceAbstraction { { id: 'passkeepcsv', name: 'PassKeep (csv)' }, ]; - protected successNavigate: any[] = ['vault']; - constructor(protected cipherService: CipherService, protected folderService: FolderService, protected apiService: ApiService, protected i18nService: I18nService) { } - async submit(importer: Importer, fileContents: string): Promise { - if (importer === null) { - return new Error(this.i18nService.t('selectFormat')); - } - - if (fileContents == null || fileContents === '') { - return new Error(this.i18nService.t('selectFile')); - } - + async import(importer: Importer, fileContents: string): Promise { const importResult = await importer.parse(fileContents); if (importResult.success) { if (importResult.folders.length === 0 && importResult.ciphers.length === 0) { - return new Error(this.i18nService.t('importNothingError')); + throw new Error(this.i18nService.t('importNothingError')); } else if (importResult.ciphers.length > 0) { const halfway = Math.floor(importResult.ciphers.length / 2); const last = importResult.ciphers.length - 1; @@ -109,23 +99,15 @@ export class ImportService implements ImportServiceAbstraction { if (this.badData(importResult.ciphers[0]) && this.badData(importResult.ciphers[halfway]) && this.badData(importResult.ciphers[last])) { - return new Error(this.i18nService.t('importFormatError')); + throw new Error(this.i18nService.t('importFormatError')); } } - return this.postImport(importResult).then(() => { - return null; - }).catch((err) => { - return new Error(err); - }); + await this.postImport(importResult); } else { - return new Error(this.i18nService.t('importFormatError')); + throw new Error(this.i18nService.t('importFormatError')); } } - getOptions(): ImportOptions { - return this.importOptions; - } - getImporter(format: string): Importer { if (format == null || format === '') { return null; @@ -204,7 +186,7 @@ export class ImportService implements ImportServiceAbstraction { } } - protected async postImport(importResult: ImportResult) { + private async postImport(importResult: ImportResult) { const request = new ImportCiphersRequest(); for (let i = 0; i < importResult.ciphers.length; i++) { const c = await this.cipherService.encrypt(importResult.ciphers[i]); From 3429b57db42a3e1e9948b870bf24fcc02ebc8a99 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 6 Aug 2018 11:39:55 -0400 Subject: [PATCH 0477/1626] web vault and organizations support --- src/abstractions/import.service.ts | 8 ++- src/services/import.service.ts | 95 ++++++++++++++++++++++-------- 2 files changed, 76 insertions(+), 27 deletions(-) diff --git a/src/abstractions/import.service.ts b/src/abstractions/import.service.ts index ff5c4a0f04..c471ea6ef3 100644 --- a/src/abstractions/import.service.ts +++ b/src/abstractions/import.service.ts @@ -5,7 +5,9 @@ export interface ImportOption { name: string; } export abstract class ImportService { - importOptions: ImportOption[]; - import: (importer: Importer, fileContents: string) => Promise; - getImporter: (format: string) => Importer; + featuredImportOptions: ImportOption[]; + regularImportOptions: ImportOption[]; + getImportOptions: () => ImportOption[]; + import: (importer: Importer, fileContents: string, organizationId?: string) => Promise; + getImporter: (format: string, organization?: boolean) => Importer; } diff --git a/src/services/import.service.ts b/src/services/import.service.ts index b45bc36986..40f744231e 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -1,5 +1,6 @@ import { ApiService } from '../abstractions/api.service'; import { CipherService } from '../abstractions/cipher.service'; +import { CollectionService } from '../abstractions/collection.service'; import { FolderService } from '../abstractions/folder.service'; import { I18nService } from '../abstractions/i18n.service'; import { @@ -10,8 +11,10 @@ import { import { ImportResult } from '../models/domain/importResult'; import { CipherRequest } from '../models/request/cipherRequest'; +import { CollectionRequest } from '../models/request/collectionRequest'; import { FolderRequest } from '../models/request/folderRequest'; import { ImportCiphersRequest } from '../models/request/importCiphersRequest'; +import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest'; import { KvpRequest } from '../models/request/kvpRequest'; import { CipherView } from '../models/view/cipherView'; @@ -51,7 +54,7 @@ import { UpmCsvImporter } from '../importers/upmCsvImporter'; import { ZohoVaultCsvImporter } from '../importers/zohoVaultCsvImporter'; export class ImportService implements ImportServiceAbstraction { - importOptions: ImportOption[] = [ + featuredImportOptions = [ { id: 'bitwardencsv', name: 'Bitwarden (csv)' }, { id: 'lastpasscsv', name: 'LastPass (csv)' }, { id: 'chromecsv', name: 'Chrome (csv)' }, @@ -59,6 +62,9 @@ export class ImportService implements ImportServiceAbstraction { { id: 'keepass2xml', name: 'KeePass 2 (xml)' }, { id: '1password1pif', name: '1Password (1pif)' }, { id: 'dashlanecsv', name: 'Dashlane (csv)' }, + ]; + + regularImportOptions: ImportOption[] = [ { id: 'keepassxcsv', name: 'KeePassX (csv)' }, { id: '1passwordwincsv', name: '1Password 6 and 7 Windows (csv)' }, { id: 'roboformcsv', name: 'RoboForm (csv)' }, @@ -82,16 +88,26 @@ export class ImportService implements ImportServiceAbstraction { { id: 'ascendocsv', name: 'Ascendo DataVault (csv)' }, { id: 'meldiumcsv', name: 'Meldium (csv)' }, { id: 'passkeepcsv', name: 'PassKeep (csv)' }, + { id: 'operacsv', name: 'Opera (csv)' }, + { id: 'vivaldicsv', name: 'Vivaldi (csv)' }, + { id: 'gnomejson', name: 'GNOME Passwords and Keys/Seahorse (json)' }, + { id: 'blurcsv', name: 'Blur (csv)' }, + { id: 'passwordagentcsv', name: 'Password Agent (csv)' }, ]; - constructor(protected cipherService: CipherService, protected folderService: FolderService, - protected apiService: ApiService, protected i18nService: I18nService) { } + constructor(private cipherService: CipherService, private folderService: FolderService, + private apiService: ApiService, private i18nService: I18nService, + private collectionService: CollectionService) { } - async import(importer: Importer, fileContents: string): Promise { + getImportOptions(): ImportOption[] { + return this.featuredImportOptions.concat(this.regularImportOptions); + } + + async import(importer: Importer, fileContents: string, organizationId: string = null): Promise { const importResult = await importer.parse(fileContents); if (importResult.success) { if (importResult.folders.length === 0 && importResult.ciphers.length === 0) { - throw new Error(this.i18nService.t('importNothingError')); + return new Error(this.i18nService.t('importNothingError')); } else if (importResult.ciphers.length > 0) { const halfway = Math.floor(importResult.ciphers.length / 2); const last = importResult.ciphers.length - 1; @@ -99,16 +115,26 @@ export class ImportService implements ImportServiceAbstraction { if (this.badData(importResult.ciphers[0]) && this.badData(importResult.ciphers[halfway]) && this.badData(importResult.ciphers[last])) { - throw new Error(this.i18nService.t('importFormatError')); + return new Error(this.i18nService.t('importFormatError')); } } - await this.postImport(importResult); + await this.postImport(importResult, organizationId); + return null; } else { - throw new Error(this.i18nService.t('importFormatError')); + return new Error(this.i18nService.t('importFormatError')); } } - getImporter(format: string): Importer { + getImporter(format: string, organization = false): Importer { + const importer = this.getImporterInstance(format); + if (importer == null) { + return null; + } + importer.organization = organization; + return importer; + } + + private getImporterInstance(format: string) { if (format == null || format === '') { return null; } @@ -186,23 +212,44 @@ export class ImportService implements ImportServiceAbstraction { } } - private async postImport(importResult: ImportResult) { - const request = new ImportCiphersRequest(); - for (let i = 0; i < importResult.ciphers.length; i++) { - const c = await this.cipherService.encrypt(importResult.ciphers[i]); - request.ciphers.push(new CipherRequest(c)); - } - if (importResult.folders != null) { - for (let i = 0; i < importResult.folders.length; i++) { - const f = await this.folderService.encrypt(importResult.folders[i]); - request.folders.push(new FolderRequest(f)); + private async postImport(importResult: ImportResult, organizationId: string = null) { + if (organizationId == null) { + const request = new ImportCiphersRequest(); + for (let i = 0; i < importResult.ciphers.length; i++) { + const c = await this.cipherService.encrypt(importResult.ciphers[i]); + request.ciphers.push(new CipherRequest(c)); } + if (importResult.folders != null) { + for (let i = 0; i < importResult.folders.length; i++) { + const f = await this.folderService.encrypt(importResult.folders[i]); + request.folders.push(new FolderRequest(f)); + } + } + if (importResult.folderRelationships != null) { + importResult.folderRelationships.forEach((r) => + request.folderRelationships.push(new KvpRequest(r[0], r[1]))); + } + return await this.apiService.postImportCiphers(request); + } else { + const request = new ImportOrganizationCiphersRequest(); + for (let i = 0; i < importResult.ciphers.length; i++) { + importResult.ciphers[i].organizationId = organizationId; + const c = await this.cipherService.encrypt(importResult.ciphers[i]); + request.ciphers.push(new CipherRequest(c)); + } + if (importResult.collections != null) { + for (let i = 0; i < importResult.collections.length; i++) { + importResult.collections[i].organizationId = organizationId; + const c = await this.collectionService.encrypt(importResult.collections[i]); + request.collections.push(new CollectionRequest(c)); + } + } + if (importResult.collectionRelationships != null) { + importResult.collectionRelationships.forEach((r) => + request.collectionRelationships.push(new KvpRequest(r[0], r[1]))); + } + return await this.apiService.postImportOrganizationCiphers(organizationId, request); } - if (importResult.folderRelationships != null) { - importResult.folderRelationships.forEach((r) => - request.folderRelationships.push(new KvpRequest(r[0], r[1]))); - } - return await this.apiService.postImportCiphers(request); } private badData(c: CipherView) { From 4ca7a9709e9ccd0e67ce09309ae605f2057bf089 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 8 Aug 2018 12:10:36 -0400 Subject: [PATCH 0478/1626] update packages for node 10 --- package-lock.json | 5697 +++++++++++++++++---------------------------- 1 file changed, 2082 insertions(+), 3615 deletions(-) diff --git a/package-lock.json b/package-lock.json index b78ee02962..90c0ac7057 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-5.2.0.tgz", "integrity": "sha512-JLR42YHiJppO4ruAkFxgbzghUDtHkXHkKPM8udd2qyt16T7e1OX7EEOrrmldUu59CC56tZnJ/32p4SrYmxyBSA==", "requires": { - "tslib": "1.9.0" + "tslib": "^1.7.1" } }, "@angular/common": { @@ -17,7 +17,7 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-5.2.0.tgz", "integrity": "sha512-yMFn2isC7/XOs56/2Kzzbb1AASHiwipAPOVFtKe7TdZQClO8fJXwCnk326rzr615+CG0eSBNQWeiFGyWN2riBA==", "requires": { - "tslib": "1.9.0" + "tslib": "^1.7.1" } }, "@angular/compiler": { @@ -25,7 +25,7 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-5.2.0.tgz", "integrity": "sha512-RfYa4ESgjGX0T0ob/Xz00IF7nd2xZkoyRy6oKgL82q42uzB3xZUDMrFNgeGxAUs3H22IkL46/5SSPOMOTMZ0NA==", "requires": { - "tslib": "1.9.0" + "tslib": "^1.7.1" } }, "@angular/core": { @@ -33,7 +33,7 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-5.2.0.tgz", "integrity": "sha512-s2ne45DguNUubhC1YgybGECC4Tyx3G4EZCntUiRMDWWkmKXSK+6dgHMesyDo8R5Oat8VfN4Anf8l3JHS1He8kg==", "requires": { - "tslib": "1.9.0" + "tslib": "^1.7.1" } }, "@angular/forms": { @@ -41,7 +41,7 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-5.2.0.tgz", "integrity": "sha512-g1/SF9lY0ZwzJ0w4NXbFsTGGEuUdgtaZny8DmkaqtmA7idby3FW398X0tv25KQfVYKtL+p9Jp1Y8EI0CvrIsvw==", "requires": { - "tslib": "1.9.0" + "tslib": "^1.7.1" } }, "@angular/http": { @@ -49,7 +49,7 @@ "resolved": "https://registry.npmjs.org/@angular/http/-/http-5.2.0.tgz", "integrity": "sha512-V5Cl24dP3rCXTTQvDc0TIKoWqBRAa0DWAQbtr7iuDAt5a1vPGdKz5K1sEiiV6ziwX6gzjiwHjUvL+B+WbIUrQA==", "requires": { - "tslib": "1.9.0" + "tslib": "^1.7.1" } }, "@angular/platform-browser": { @@ -57,7 +57,7 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-5.2.0.tgz", "integrity": "sha512-c6cR15MfopPwGZ097HdRuAi9+R9BhA3bRRFpP2HmrSSB/BW4ZNovUYwB2QUMSYbd9s0lYTtnavqGm6DKcyF2QA==", "requires": { - "tslib": "1.9.0" + "tslib": "^1.7.1" } }, "@angular/platform-browser-dynamic": { @@ -65,7 +65,7 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-5.2.0.tgz", "integrity": "sha512-xG1eNoi8sm4Jcly2y98r5mqYVe3XV8sUJCtOhvGBYtvt4dKEQ5tOns6fWQ0nUbl6Vv3Y0xgGUS1JCtfut3DuaQ==", "requires": { - "tslib": "1.9.0" + "tslib": "^1.7.1" } }, "@angular/router": { @@ -73,7 +73,7 @@ "resolved": "https://registry.npmjs.org/@angular/router/-/router-5.2.0.tgz", "integrity": "sha512-VXDXtp2A1GQEUEhXg0ZzqHdTUERLgDSo3/Mmpzt+dgLMKlXDSCykcm4gINwE5VQLGD1zQvDFCCRv3seGRNfrqA==", "requires": { - "tslib": "1.9.0" + "tslib": "^1.7.1" } }, "@angular/upgrade": { @@ -81,7 +81,7 @@ "resolved": "https://registry.npmjs.org/@angular/upgrade/-/upgrade-5.2.0.tgz", "integrity": "sha512-ezWfhBCiP7RX+59scxfYfjDMRw+qq0BVbm/EfOXdYFU0NHWo7lXJ3v+cUi18G+5GVjzwRiJDIKWhw1QEyq2nug==", "requires": { - "tslib": "1.9.0" + "tslib": "^1.7.1" } }, "@types/form-data": { @@ -90,28 +90,28 @@ "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", "dev": true, "requires": { - "@types/node": "8.0.19" + "@types/node": "*" } }, "@types/jasmine": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.6.tgz", - "integrity": "sha512-clg9raJTY0EOo5pVZKX3ZlMjlYzVU73L71q5OV1jhE2Uezb7oF94jh4CvwrW6wInquQAdhOxJz5VDF2TLUGmmA==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.8.tgz", + "integrity": "sha512-OJSUxLaxXsjjhob2DBzqzgrkLmukM3+JMpRp0r0E4HTdT1nwDCWhaswjYxazPij6uOdzHCJfNbDjmQ1/rnNbCg==", "dev": true }, "@types/lodash": { - "version": "4.14.109", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.109.tgz", - "integrity": "sha512-hop8SdPUEzbcJm6aTsmuwjIYQo1tqLseKCM+s2bBqTU2gErwI4fE+aqUVOlscPSQbKHKgtMMPoC+h4AIGOJYvw==", + "version": "4.14.116", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.116.tgz", + "integrity": "sha512-lRnAtKnxMXcYYXqOiotTmJd74uawNWuPnsnPrrO7HiFuE3npE2iQhfABatbYDyxTNqZNuXzcKGhw37R7RjBFLg==", "dev": true }, "@types/lowdb": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/lowdb/-/lowdb-1.0.2.tgz", - "integrity": "sha512-0lS8jOba45tcXne01LXkw06x8uqpIKuh8LTwTOo2zmIXCVoXXmIxAemAGoLJvzNc8Q0qBG+fJT0xJMx7N0FLtA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/lowdb/-/lowdb-1.0.5.tgz", + "integrity": "sha512-UaaxeB1wUsDAXf9Iw5iulhR4hYnrbxEABVBze/Gkd8yhQzwA/Ay/FqyH2G8mo7T8tnZro6jP6ealoYrmirh1Yw==", "dev": true, "requires": { - "@types/lodash": "4.14.109" + "@types/lodash": "*" } }, "@types/lunr": { @@ -132,13 +132,13 @@ "integrity": "sha512-n2r6WLoY7+uuPT7pnEtKJCmPUGyJ+cbyBR8Avnu4+m1nzz7DwBVuyIvvlBzCZ/nrpC7rIgb3D6pNavL7rFEa9g==", "dev": true, "requires": { - "@types/node": "8.0.19" + "@types/node": "*" } }, "@types/node-forge": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.7.1.tgz", - "integrity": "sha1-XoS4q/QthOxenQg+jHVzsxVcYzQ=", + "integrity": "sha512-sXCLq42I8Evd/qnrSluSKwxuBc2ioPvNCvb5hl+VL3d2zlh45n26b3rPf8DuJiAuJSv5Z5cqcF1KL7X77tXG4Q==", "dev": true }, "@types/papaparse": { @@ -150,7 +150,7 @@ "@types/webcrypto": { "version": "0.0.28", "resolved": "https://registry.npmjs.org/@types/webcrypto/-/webcrypto-0.0.28.tgz", - "integrity": "sha1-sGFgOQzQAPsgH2LrAqw0Hr9+YP4=", + "integrity": "sha512-jzAoSUvqA+183nJO/Sc73CREQJsv+p77WJdn532GqA3YXQzlwRwHhClVa7U4O8iB2sJSR7G3v6f1mJFNkwA9YQ==", "dev": true }, "abbrev": { @@ -165,7 +165,7 @@ "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=", "dev": true, "requires": { - "mime-types": "2.1.18", + "mime-types": "~2.1.11", "negotiator": "0.6.1" } }, @@ -187,10 +187,10 @@ "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", "dev": true, "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.1.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" } }, "align-text": { @@ -199,9 +199,9 @@ "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", "dev": true, "requires": { - "kind-of": "3.2.2", - "longest": "1.0.1", - "repeat-string": "1.6.1" + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" } }, "amdefine": { @@ -220,7 +220,7 @@ "resolved": "https://registry.npmjs.org/angulartics2/-/angulartics2-5.0.1.tgz", "integrity": "sha512-QYBp7km7xTf/57zKKnYreM0OQ1Pq0kd4L9HJTC79vy7+RG1XqrkA944jTGKDERLWtjEAlQuSyZMS9J5IZZ56sw==", "requires": { - "tslib": "1.9.0" + "tslib": "^1.7.1" } }, "ansi-align": { @@ -229,7 +229,40 @@ "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", "dev": true, "requires": { - "string-width": "2.1.1" + "string-width": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } } }, "ansi-cyan": { @@ -256,9 +289,9 @@ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", "dev": true }, "ansi-wrap": { @@ -273,17 +306,17 @@ "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", "dev": true, "requires": { - "micromatch": "2.3.11", - "normalize-path": "2.1.1" + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" } }, "append-transform": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", - "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", "dev": true, "requires": { - "default-require-extensions": "1.0.0" + "default-require-extensions": "^2.0.0" } }, "aproba": { @@ -296,8 +329,8 @@ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.6" + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" } }, "argparse": { @@ -305,7 +338,7 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "requires": { - "sprintf-js": "1.0.3" + "sprintf-js": "~1.0.2" } }, "arr-diff": { @@ -314,7 +347,7 @@ "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", "dev": true, "requires": { - "arr-flatten": "1.1.0" + "arr-flatten": "^1.0.1" } }, "arr-flatten": { @@ -354,10 +387,13 @@ "dev": true }, "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", - "dev": true + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } }, "asn1.js": { "version": "4.10.1", @@ -365,17 +401,9 @@ "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", "dev": true, "requires": { - "bn.js": "4.11.8", - "inherits": "2.0.3", - "minimalistic-assert": "1.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" } }, "assert": { @@ -385,6 +413,23 @@ "dev": true, "requires": { "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } } }, "assert-plus": { @@ -400,10 +445,13 @@ "dev": true }, "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "dev": true, + "requires": { + "lodash": "^4.17.10" + } }, "async-each": { "version": "1.0.1", @@ -417,9 +465,9 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "atob": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.0.tgz", - "integrity": "sha512-SuiKH8vbsOyCALjA/+EINmt/Kdl+TQPrtFgW7XZZcwtryFu9e5kQoX3bjCW6mIvGH1fbeAZZuvwGR5IlBRznGw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", + "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=", "dev": true }, "aws-sign2": { @@ -429,9 +477,9 @@ "dev": true }, "aws4": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", - "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", "dev": true }, "babel-code-frame": { @@ -440,9 +488,45 @@ "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", "dev": true, "requires": { - "chalk": "1.1.3", - "esutils": "2.0.2", - "js-tokens": "3.0.2" + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } } }, "babel-generator": { @@ -451,14 +535,14 @@ "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", "dev": true, "requires": { - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "detect-indent": "4.0.0", - "jsesc": "1.3.0", - "lodash": "4.17.4", - "source-map": "0.5.7", - "trim-right": "1.0.1" + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" }, "dependencies": { "source-map": { @@ -475,7 +559,7 @@ "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", "dev": true, "requires": { - "babel-runtime": "6.26.0" + "babel-runtime": "^6.22.0" } }, "babel-runtime": { @@ -484,8 +568,8 @@ "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "dev": true, "requires": { - "core-js": "2.4.1", - "regenerator-runtime": "0.11.1" + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" } }, "babel-template": { @@ -494,11 +578,11 @@ "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", "dev": true, "requires": { - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "lodash": "4.17.4" + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" } }, "babel-traverse": { @@ -507,15 +591,26 @@ "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", "dev": true, "requires": { - "babel-code-frame": "6.26.0", - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "debug": "2.6.9", - "globals": "9.18.0", - "invariant": "2.2.4", - "lodash": "4.17.4" + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } } }, "babel-types": { @@ -524,10 +619,10 @@ "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", "dev": true, "requires": { - "babel-runtime": "6.26.0", - "esutils": "2.0.2", - "lodash": "4.17.4", - "to-fast-properties": "1.0.3" + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" } }, "babylon": { @@ -543,7 +638,8 @@ "dev": true }, "balanced-match": { - "version": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, @@ -553,13 +649,13 @@ "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", "dev": true, "requires": { - "cache-base": "1.0.1", - "class-utils": "0.3.6", - "component-emitter": "1.2.1", - "define-property": "1.0.0", - "isobject": "3.0.1", - "mixin-deep": "1.3.1", - "pascalcase": "0.1.1" + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" }, "dependencies": { "component-emitter": { @@ -574,7 +670,7 @@ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dev": true, "requires": { - "is-descriptor": "1.0.2" + "is-descriptor": "^1.0.0" } }, "is-accessor-descriptor": { @@ -583,7 +679,7 @@ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-data-descriptor": { @@ -592,7 +688,7 @@ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-descriptor": { @@ -601,9 +697,9 @@ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dev": true, "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" } }, "isobject": { @@ -627,9 +723,9 @@ "dev": true }, "base64-js": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.3.tgz", - "integrity": "sha512-MsAhsUW1GxCdgYSO6tAfZrNapmUKk7mWx/k5mFY/A1gBtkaCaNapTg+FExCw1r9yeaZhqx/xPg43xgTFH6KL5w==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==", "dev": true }, "base64id": { @@ -645,7 +741,7 @@ "dev": true, "optional": true, "requires": { - "tweetnacl": "0.14.5" + "tweetnacl": "^0.14.3" } }, "better-assert": { @@ -668,8 +764,8 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "requires": { - "readable-stream": "2.3.6", - "safe-buffer": "5.1.1" + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" } }, "blob": { @@ -688,7 +784,7 @@ "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.5.tgz", "integrity": "sha512-Ey0bDNys5qpYPhZ/oQ9vOEvD0TYQDTILMXWP2iGfvMg7rSDde+oV4aQQgqRH+CvBFNz2BSDQnPGMUl6LKBUUQA==", "requires": { - "bluebird": "3.5.1" + "bluebird": "^3.5.1" } }, "bn.js": { @@ -698,21 +794,32 @@ "dev": true }, "body-parser": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", - "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", "dev": true, "requires": { "bytes": "3.0.0", - "content-type": "1.0.4", + "content-type": "~1.0.4", "debug": "2.6.9", - "depd": "1.1.2", - "http-errors": "1.6.3", - "iconv-lite": "0.4.19", - "on-finished": "2.3.0", - "qs": "6.5.1", - "raw-body": "2.3.2", - "type-is": "1.6.16" + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } } }, "boxen": { @@ -721,22 +828,28 @@ "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", "dev": true, "requires": { - "ansi-align": "2.0.0", - "camelcase": "4.1.0", - "chalk": "2.4.0", - "cli-boxes": "1.0.0", - "string-width": "2.1.1", - "term-size": "1.2.0", - "widest-line": "2.0.0" + "ansi-align": "^2.0.0", + "camelcase": "^4.0.0", + "chalk": "^2.0.1", + "cli-boxes": "^1.0.0", + "string-width": "^2.0.0", + "term-size": "^1.2.0", + "widest-line": "^2.0.0" }, "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { - "color-convert": "1.9.1" + "color-convert": "^1.9.0" } }, "camelcase": { @@ -746,14 +859,14 @@ "dev": true }, "chalk": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.0.tgz", - "integrity": "sha512-Wr/w0f4o9LuE7K53cD0qmbAMM+2XNLzR29vFn5hqko4sxGlUsyy363NvmyGIyk5tpe9cjTr9SJYbysEyPkRnFw==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", "dev": true, "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.4.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" } }, "has-flag": { @@ -762,24 +875,50 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, "supports-color": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "dev": true, "requires": { - "has-flag": "3.0.0" + "has-flag": "^3.0.0" } } } }, "brace-expansion": { - "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", - "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "requires": { - "balanced-match": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "concat-map": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "braces": { @@ -788,9 +927,9 @@ "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", "dev": true, "requires": { - "expand-range": "1.8.2", - "preserve": "0.2.0", - "repeat-element": "1.1.2" + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" } }, "brorand": { @@ -800,9 +939,9 @@ "dev": true }, "browser-resolve": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz", - "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", "dev": true, "requires": { "resolve": "1.1.7" @@ -822,20 +961,12 @@ "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dev": true, "requires": { - "buffer-xor": "1.0.3", - "cipher-base": "1.0.4", - "create-hash": "1.2.0", - "evp_bytestokey": "1.0.3", - "inherits": "2.0.3", - "safe-buffer": "5.1.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" } }, "browserify-cipher": { @@ -844,28 +975,21 @@ "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", "dev": true, "requires": { - "browserify-aes": "1.2.0", - "browserify-des": "1.0.1", - "evp_bytestokey": "1.0.3" + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" } }, "browserify-des": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.1.tgz", - "integrity": "sha512-zy0Cobe3hhgpiOM32Tj7KQ3Vl91m0njwsjzZQK1L+JDf11dzP9qIvjreVinsvXrgfjhStXwUWAEpB9D7Gwmayw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", "dev": true, "requires": { - "cipher-base": "1.0.4", - "des.js": "1.0.0", - "inherits": "2.0.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" } }, "browserify-rsa": { @@ -874,8 +998,8 @@ "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "dev": true, "requires": { - "bn.js": "4.11.8", - "randombytes": "2.0.6" + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" } }, "browserify-sign": { @@ -884,21 +1008,13 @@ "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", "dev": true, "requires": { - "bn.js": "4.11.8", - "browserify-rsa": "4.0.1", - "create-hash": "1.2.0", - "create-hmac": "1.1.7", - "elliptic": "6.4.0", - "inherits": "2.0.3", - "parse-asn1": "5.1.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" } }, "browserify-zlib": { @@ -907,17 +1023,17 @@ "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", "dev": true, "requires": { - "pako": "1.0.6" + "pako": "~1.0.5" } }, "buffer": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.1.0.tgz", - "integrity": "sha512-YkIRgwsZwJWTnyQrsBTWefizHh+8GYj3kbL1BTiAQ/9pwpino0G7B2gp5tx/FUBqUlvtxV85KNR3mwfAtv15Yw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.0.tgz", + "integrity": "sha512-nUJyfChH7PMJy75eRDCCKtszSEFokUNXC1hNVSe+o+VdcgvDPLs20k3v8UXI8ruRYAJiYtyRea8mYyqPxoHWDw==", "dev": true, "requires": { - "base64-js": "1.2.3", - "ieee754": "1.1.11" + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" } }, "buffer-alloc": { @@ -925,8 +1041,8 @@ "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", "requires": { - "buffer-alloc-unsafe": "1.1.0", - "buffer-fill": "1.0.0" + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" } }, "buffer-alloc-unsafe": { @@ -940,9 +1056,9 @@ "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" }, "buffer-from": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", - "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, "buffer-xor": { "version": "1.0.3", @@ -955,20 +1071,10 @@ "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-4.4.1.tgz", "integrity": "sha512-8L2pbL6D3VdI1f8OMknlZJpw0c7KK15BRz3cY77AOUElc4XlCv2UhVV01jJM7+6Lx7henaQh80ALULp64eFYAQ==", "requires": { - "bluebird-lst": "1.0.5", - "debug": "3.1.0", - "fs-extra-p": "4.6.1", - "sax": "1.2.4" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } + "bluebird-lst": "^1.0.5", + "debug": "^3.1.0", + "fs-extra-p": "^4.6.1", + "sax": "^1.2.4" } }, "builtin-modules": { @@ -995,15 +1101,15 @@ "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", "dev": true, "requires": { - "collection-visit": "1.0.0", - "component-emitter": "1.2.1", - "get-value": "2.0.6", - "has-value": "1.0.0", - "isobject": "3.0.1", - "set-value": "2.0.0", - "to-object-path": "0.3.0", - "union-value": "1.0.0", - "unset-value": "1.0.0" + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" }, "dependencies": { "component-emitter": { @@ -1027,11 +1133,10 @@ "dev": true }, "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "dev": true, - "optional": true + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true }, "camelcase-keys": { "version": "2.1.0", @@ -1039,16 +1144,8 @@ "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "dev": true, "requires": { - "camelcase": "2.1.1", - "map-obj": "1.0.1" - }, - "dependencies": { - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true - } + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" } }, "capture-stack-trace": { @@ -1070,21 +1167,44 @@ "dev": true, "optional": true, "requires": { - "align-text": "0.1.4", - "lazy-cache": "1.0.4" + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" } }, "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", "dev": true, "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" + "ansi-styles": "^1.1.0", + "escape-string-regexp": "^1.0.0", + "has-ansi": "^0.1.0", + "strip-ansi": "^0.3.0", + "supports-color": "^0.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", + "dev": true + }, + "strip-ansi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.1" + } + }, + "supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "dev": true + } } }, "chokidar": { @@ -1093,29 +1213,15 @@ "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", "dev": true, "requires": { - "anymatch": "1.3.2", - "async-each": "1.0.1", - "fsevents": "1.1.3", - "glob-parent": "2.0.0", - "inherits": "2.0.3", - "is-binary-path": "1.0.1", - "is-glob": "2.0.1", - "path-is-absolute": "1.0.1", - "readdirp": "2.1.0" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - } + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "fsevents": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" } }, "chownr": { @@ -1135,16 +1241,8 @@ "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", "dev": true, "requires": { - "inherits": "2.0.3", - "safe-buffer": "5.1.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" } }, "circular-json": { @@ -1159,10 +1257,10 @@ "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", "dev": true, "requires": { - "arr-union": "3.1.0", - "define-property": "0.2.5", - "isobject": "3.0.1", - "static-extend": "0.1.2" + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" }, "dependencies": { "arr-union": { @@ -1177,7 +1275,7 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } }, "isobject": { @@ -1201,8 +1299,8 @@ "dev": true, "optional": true, "requires": { - "center-align": "0.1.3", - "right-align": "0.1.3", + "center-align": "^0.1.1", + "right-align": "^0.1.1", "wordwrap": "0.0.2" }, "dependencies": { @@ -1238,23 +1336,23 @@ "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", "dev": true, "requires": { - "map-visit": "1.0.0", - "object-visit": "1.0.1" + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" } }, "color-convert": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", - "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", + "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", "dev": true, "requires": { - "color-name": "1.1.3" + "color-name": "1.1.1" } }, "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", "dev": true }, "colors": { @@ -1269,7 +1367,7 @@ "integrity": "sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y=", "dev": true, "requires": { - "lodash": "4.17.4" + "lodash": "^4.5.0" } }, "combine-source-map": { @@ -1278,10 +1376,10 @@ "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=", "dev": true, "requires": { - "convert-source-map": "1.1.3", - "inline-source-map": "0.6.2", - "lodash.memoize": "3.0.4", - "source-map": "0.5.7" + "convert-source-map": "~1.1.0", + "inline-source-map": "~0.6.0", + "lodash.memoize": "~3.0.3", + "source-map": "~0.5.3" }, "dependencies": { "convert-source-map": { @@ -1303,7 +1401,7 @@ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", "requires": { - "delayed-stream": "1.0.0" + "delayed-stream": "~1.0.0" } }, "commander": { @@ -1313,9 +1411,9 @@ "dev": true }, "compare-versions": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.1.0.tgz", - "integrity": "sha512-4hAxDSBypT/yp2ySFD346So6Ragw5xmBn/e/agIGl3bZr6DLUqnoRZPusxKrXdYRZpgexO9daejmIenlq/wrIQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.3.0.tgz", + "integrity": "sha512-MAAAIOdi2s4Gl6rZ76PNcUa9IOYB+5ICdT41o5uMRf09aEu/F9RK+qhe8RjXNPwcTjGV7KU7h2P/fljThFVqyQ==", "dev": true }, "component-bind": { @@ -1337,7 +1435,8 @@ "dev": true }, "concat-map": { - "version": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, @@ -1347,18 +1446,10 @@ "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "dev": true, "requires": { - "buffer-from": "1.1.0", - "inherits": "2.0.3", - "readable-stream": "2.3.6", - "typedarray": "0.0.6" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" } }, "concurrently": { @@ -1369,74 +1460,12 @@ "requires": { "chalk": "0.5.1", "commander": "2.6.0", - "date-fns": "1.29.0", - "lodash": "4.17.4", + "date-fns": "^1.23.0", + "lodash": "^4.5.1", "rx": "2.3.24", - "spawn-command": "0.0.2-1", - "supports-color": "3.2.3", - "tree-kill": "1.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", - "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", - "dev": true - }, - "ansi-styles": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", - "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", - "dev": true - }, - "chalk": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", - "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", - "dev": true, - "requires": { - "ansi-styles": "1.1.0", - "escape-string-regexp": "1.0.5", - "has-ansi": "0.1.0", - "strip-ansi": "0.3.0", - "supports-color": "0.2.0" - }, - "dependencies": { - "supports-color": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", - "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", - "dev": true - } - } - }, - "has-ansi": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", - "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", - "dev": true, - "requires": { - "ansi-regex": "0.2.1" - } - }, - "strip-ansi": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", - "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", - "dev": true, - "requires": { - "ansi-regex": "0.2.1" - } - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "1.0.0" - } - } + "spawn-command": "^0.0.2-1", + "supports-color": "^3.2.3", + "tree-kill": "^1.1.0" } }, "configstore": { @@ -1445,12 +1474,12 @@ "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", "dev": true, "requires": { - "dot-prop": "4.2.0", - "graceful-fs": "4.1.11", - "make-dir": "1.2.0", - "unique-string": "1.0.0", - "write-file-atomic": "2.3.0", - "xdg-basedir": "3.0.0" + "dot-prop": "^4.1.0", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "unique-string": "^1.0.0", + "write-file-atomic": "^2.0.0", + "xdg-basedir": "^3.0.0" } }, "connect": { @@ -1461,8 +1490,19 @@ "requires": { "debug": "2.6.9", "finalhandler": "1.1.0", - "parseurl": "1.3.2", + "parseurl": "~1.3.2", "utils-merge": "1.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } } }, "console-browserify": { @@ -1471,7 +1511,7 @@ "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", "dev": true, "requires": { - "date-now": "0.1.4" + "date-now": "^0.1.4" } }, "console-control-strings": { @@ -1520,13 +1560,13 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "create-ecdh": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.1.tgz", - "integrity": "sha512-iZvCCg8XqHQZ1ioNBTzXS/cQSkqkqcPs8xSX4upNB+DAk9Ht3uzQf2J32uAHNCne8LDmKr29AgZrEs4oIrwLuQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", "dev": true, "requires": { - "bn.js": "4.11.8", - "elliptic": "6.4.0" + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" } }, "create-error-class": { @@ -1535,7 +1575,7 @@ "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", "dev": true, "requires": { - "capture-stack-trace": "1.0.0" + "capture-stack-trace": "^1.0.0" } }, "create-hash": { @@ -1544,19 +1584,11 @@ "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dev": true, "requires": { - "cipher-base": "1.0.4", - "inherits": "2.0.3", - "md5.js": "1.3.4", - "ripemd160": "2.0.1", - "sha.js": "2.4.11" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" } }, "create-hmac": { @@ -1565,20 +1597,12 @@ "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dev": true, "requires": { - "cipher-base": "1.0.4", - "create-hash": "1.2.0", - "inherits": "2.0.3", - "ripemd160": "2.0.1", - "safe-buffer": "5.1.1", - "sha.js": "2.4.11" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" } }, "cross-spawn": { @@ -1587,9 +1611,9 @@ "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", "dev": true, "requires": { - "lru-cache": "4.1.2", - "shebang-command": "1.2.0", - "which": "1.3.0" + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" } }, "crypto-browserify": { @@ -1598,25 +1622,17 @@ "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", "dev": true, "requires": { - "browserify-cipher": "1.0.1", - "browserify-sign": "4.0.4", - "create-ecdh": "4.0.1", - "create-hash": "1.2.0", - "create-hmac": "1.1.7", - "diffie-hellman": "5.0.3", - "inherits": "2.0.3", - "pbkdf2": "3.0.14", - "public-encrypt": "4.0.2", - "randombytes": "2.0.6", - "randomfill": "1.0.4" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" } }, "crypto-random-string": { @@ -1631,7 +1647,7 @@ "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", "dev": true, "requires": { - "array-find-index": "1.0.2" + "array-find-index": "^1.0.1" } }, "custom-event": { @@ -1646,7 +1662,7 @@ "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", "dev": true, "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" } }, "date-fns": { @@ -1673,15 +1689,14 @@ "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", "dev": true, "requires": { - "get-stdin": "4.0.1", - "meow": "3.7.0" + "get-stdin": "^4.0.1", + "meow": "^3.3.0" } }, "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "requires": { "ms": "2.0.0" } @@ -1703,13 +1718,13 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", "requires": { - "mimic-response": "1.0.1" + "mimic-response": "^1.0.0" } }, "deep-extend": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", - "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" }, "deep-is": { "version": "0.1.3", @@ -1718,12 +1733,20 @@ "dev": true }, "default-require-extensions": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", - "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", "dev": true, "requires": { - "strip-bom": "2.0.0" + "strip-bom": "^3.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } } }, "defaults": { @@ -1732,7 +1755,7 @@ "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", "dev": true, "requires": { - "clone": "1.0.4" + "clone": "^1.0.2" } }, "define-property": { @@ -1741,8 +1764,8 @@ "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", "dev": true, "requires": { - "is-descriptor": "1.0.2", - "isobject": "3.0.1" + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" }, "dependencies": { "is-accessor-descriptor": { @@ -1751,7 +1774,7 @@ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-data-descriptor": { @@ -1760,7 +1783,7 @@ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-descriptor": { @@ -1769,9 +1792,9 @@ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dev": true, "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" } }, "isobject": { @@ -1810,16 +1833,8 @@ "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", "dev": true, "requires": { - "inherits": "2.0.3", - "minimalistic-assert": "1.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" } }, "detect-indent": { @@ -1828,7 +1843,7 @@ "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", "dev": true, "requires": { - "repeating": "2.0.1" + "repeating": "^2.0.0" } }, "detect-libc": { @@ -1854,9 +1869,9 @@ "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "dev": true, "requires": { - "bn.js": "4.11.8", - "miller-rabin": "4.0.1", - "randombytes": "2.0.6" + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" } }, "dom-serialize": { @@ -1865,10 +1880,10 @@ "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", "dev": true, "requires": { - "custom-event": "1.0.1", - "ent": "2.2.0", - "extend": "3.0.1", - "void-elements": "2.0.1" + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" } }, "domain-browser": { @@ -1883,7 +1898,7 @@ "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", "dev": true, "requires": { - "is-obj": "1.0.1" + "is-obj": "^1.0.0" } }, "duplexer": { @@ -1899,13 +1914,14 @@ "dev": true }, "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", "dev": true, "optional": true, "requires": { - "jsbn": "0.1.1" + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" } }, "edge-launcher": { @@ -1926,15 +1942,15 @@ "integrity": "sha512-NbWsgAvcxxQrDNaLA2L5adZTKWO6mZwC57uSPQiZiFjpO0K6uVNCjFyRbLnhq8AWq2tmcuzs6mFpIzQXmvlnUQ==", "dev": true, "requires": { - "@types/node": "8.10.21", - "electron-download": "3.3.0", - "extract-zip": "1.6.7" + "@types/node": "^8.0.24", + "electron-download": "^3.0.1", + "extract-zip": "^1.0.3" }, "dependencies": { "@types/node": { - "version": "8.10.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.21.tgz", - "integrity": "sha512-87XkD9qDXm8fIax+5y7drx84cXsu34ZZqfB7Cial3Q/2lxSoJ/+DRaWckkCbxP41wFSIrrb939VhzaNxj4eY1w==", + "version": "8.10.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.24.tgz", + "integrity": "sha512-5YaBKa6oFuWy7ptIFMATyftIcpZTZtvgrzPThEbs+kl4Uu41oUxiRunG0k32QZjD6MXMELls//ry/epNxc11aQ==", "dev": true } } @@ -1945,28 +1961,47 @@ "integrity": "sha1-LP1U1pZsAZxNSa1l++Zcyc3vaMg=", "dev": true, "requires": { - "debug": "2.6.9", - "fs-extra": "0.30.0", - "home-path": "1.0.6", - "minimist": "1.2.0", - "nugget": "2.0.1", - "path-exists": "2.1.0", - "rc": "1.2.6", - "semver": "5.5.0", - "sumchecker": "1.3.1" + "debug": "^2.2.0", + "fs-extra": "^0.30.0", + "home-path": "^1.0.1", + "minimist": "^1.2.0", + "nugget": "^2.0.0", + "path-exists": "^2.1.0", + "rc": "^1.1.2", + "semver": "^5.3.0", + "sumchecker": "^1.2.0" }, "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true + "fs-extra": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", + "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0", + "klaw": "^1.0.0", + "path-is-absolute": "^1.0.0", + "rimraf": "^2.2.8" + } + }, + "jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } } } }, @@ -1985,31 +2020,15 @@ "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-3.0.3.tgz", "integrity": "sha512-7gJLZp34Db+lXiJsFzW8DunGnvxJgZclBZa1DNLbXOet3lRXkVKbFJ73mClbv+UTW6hW/EJ6MmSsofRiK1s6Dw==", "requires": { - "bluebird-lst": "1.0.5", - "builder-util-runtime": "4.4.1", - "electron-is-dev": "0.3.0", - "fs-extra-p": "4.6.1", - "js-yaml": "3.12.0", - "lazy-val": "1.0.3", - "lodash.isequal": "4.5.0", - "semver": "5.5.0", - "source-map-support": "0.5.6" - }, - "dependencies": { - "js-yaml": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", - "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", - "requires": { - "argparse": "1.0.10", - "esprima": "4.0.0" - } - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" - } + "bluebird-lst": "^1.0.5", + "builder-util-runtime": "~4.4.1", + "electron-is-dev": "^0.3.0", + "fs-extra-p": "^4.6.1", + "js-yaml": "^3.12.0", + "lazy-val": "^1.0.3", + "lodash.isequal": "^4.5.0", + "semver": "^5.5.0", + "source-map-support": "^0.5.6" } }, "elliptic": { @@ -2018,21 +2037,13 @@ "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", "dev": true, "requires": { - "bn.js": "4.11.8", - "brorand": "1.1.0", - "hash.js": "1.1.3", - "hmac-drbg": "1.0.1", - "inherits": "2.0.3", - "minimalistic-assert": "1.0.1", - "minimalistic-crypto-utils": "1.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" } }, "encodeurl": { @@ -2046,22 +2057,7 @@ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", "requires": { - "once": "1.4.0" - }, - "dependencies": { - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - } + "once": "^1.4.0" } }, "engine.io": { @@ -2159,21 +2155,21 @@ "dev": true }, "error-ex": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", - "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, "requires": { - "is-arrayish": "0.2.1" + "is-arrayish": "^0.2.1" } }, "error-stack-parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.1.tgz", - "integrity": "sha1-oyArj7AxFKqbQKDjZp5IsrZaAQo=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.2.tgz", + "integrity": "sha512-E1fPutRDdIj/hohG0UpT5mayXNCxXP9d+snxFsPU9X0XgccOumKraa3juDMwTUyi7+Bu5+mCGagjg4IYeNbOdw==", "dev": true, "requires": { - "stackframe": "1.0.4" + "stackframe": "^1.0.4" } }, "es6-promise": { @@ -2200,11 +2196,11 @@ "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", "dev": true, "requires": { - "esprima": "2.7.3", - "estraverse": "1.9.3", - "esutils": "2.0.2", - "optionator": "0.8.2", - "source-map": "0.2.0" + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" }, "dependencies": { "esprima": { @@ -2220,15 +2216,15 @@ "dev": true, "optional": true, "requires": { - "amdefine": "1.0.1" + "amdefine": ">=0.0.4" } } } }, "esprima": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "estraverse": { "version": "1.9.3", @@ -2248,19 +2244,19 @@ "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", "dev": true, "requires": { - "duplexer": "0.1.1", - "from": "0.1.7", - "map-stream": "0.1.0", + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", "pause-stream": "0.0.11", - "split": "0.3.3", - "stream-combiner": "0.0.4", - "through": "2.3.8" + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" } }, "eventemitter3": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", - "integrity": "sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", + "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==", "dev": true }, "events": { @@ -2275,8 +2271,8 @@ "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", "dev": true, "requires": { - "md5.js": "1.3.4", - "safe-buffer": "5.1.1" + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" } }, "execa": { @@ -2285,13 +2281,13 @@ "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", "dev": true, "requires": { - "cross-spawn": "5.1.0", - "get-stream": "3.0.0", - "is-stream": "1.1.0", - "npm-run-path": "2.0.2", - "p-finally": "1.0.0", - "signal-exit": "3.0.2", - "strip-eof": "1.0.0" + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" } }, "expand-braces": { @@ -2300,9 +2296,9 @@ "integrity": "sha1-SIsdHSRRyz06axks/AMPRMWFX+o=", "dev": true, "requires": { - "array-slice": "0.2.3", - "array-unique": "0.2.1", - "braces": "0.1.5" + "array-slice": "^0.2.3", + "array-unique": "^0.2.1", + "braces": "^0.1.2" }, "dependencies": { "braces": { @@ -2311,7 +2307,7 @@ "integrity": "sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY=", "dev": true, "requires": { - "expand-range": "0.1.1" + "expand-range": "^0.1.0" } }, "expand-range": { @@ -2320,8 +2316,8 @@ "integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=", "dev": true, "requires": { - "is-number": "0.1.1", - "repeat-string": "0.2.2" + "is-number": "^0.1.1", + "repeat-string": "^0.2.2" } }, "is-number": { @@ -2344,7 +2340,7 @@ "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", "dev": true, "requires": { - "is-posix-bracket": "0.1.1" + "is-posix-bracket": "^0.1.0" } }, "expand-range": { @@ -2353,7 +2349,7 @@ "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", "dev": true, "requires": { - "fill-range": "2.2.3" + "fill-range": "^2.1.0" } }, "expand-template": { @@ -2362,9 +2358,9 @@ "integrity": "sha512-cebqLtV8KOZfw0UI8TEFWxtczxxC1jvyUvx6H4fyp1K1FN7A4Q+uggVUlOsI1K8AGU0rwOGqP8nCapdrw8CYQg==" }, "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, "extend-shallow": { @@ -2373,7 +2369,7 @@ "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=", "dev": true, "requires": { - "kind-of": "1.1.0" + "kind-of": "^1.1.0" }, "dependencies": { "kind-of": { @@ -2390,7 +2386,7 @@ "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", "dev": true, "requires": { - "is-extglob": "1.0.0" + "is-extglob": "^1.0.0" } }, "extract-zip": { @@ -2403,6 +2399,17 @@ "debug": "2.6.9", "mkdirp": "0.5.1", "yauzl": "2.4.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } } }, "extsprintf": { @@ -2435,7 +2442,7 @@ "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", "dev": true, "requires": { - "pend": "1.2.0" + "pend": "~1.2.0" } }, "filename-regex": { @@ -2450,111 +2457,21 @@ "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", "dev": true, "requires": { - "glob": "7.1.2", - "minimatch": "3.0.4" - }, - "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - } + "glob": "^7.0.3", + "minimatch": "^3.0.3" } }, "fill-range": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", - "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", "dev": true, "requires": { - "is-number": "2.1.0", - "isobject": "2.1.0", - "randomatic": "1.1.7", - "repeat-element": "1.1.2", - "repeat-string": "1.6.1" + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" } }, "finalhandler": { @@ -2564,14 +2481,23 @@ "dev": true, "requires": { "debug": "2.6.9", - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "on-finished": "2.3.0", - "parseurl": "1.3.2", - "statuses": "1.3.1", - "unpipe": "1.0.0" + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.3.1", + "unpipe": "~1.0.0" }, "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, "statuses": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", @@ -2586,8 +2512,17 @@ "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", "dev": true, "requires": { - "path-exists": "2.1.0", - "pinkie-promise": "2.0.1" + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "follow-redirects": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.2.tgz", + "integrity": "sha512-kssLorP/9acIdpQ2udQVTiCS5LQmdEz9mvdIfDcl1gYX2tPKFADHSyFdvJS040XdFsPzemWtgI3q8mFVCxtX8A==", + "dev": true, + "requires": { + "debug": "^3.1.0" } }, "for-in": { @@ -2602,7 +2537,7 @@ "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", "dev": true, "requires": { - "for-in": "1.0.2" + "for-in": "^1.0.1" } }, "forever-agent": { @@ -2616,9 +2551,9 @@ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", "requires": { - "asynckit": "0.4.0", + "asynckit": "^0.4.0", "combined-stream": "1.0.6", - "mime-types": "2.1.18" + "mime-types": "^2.1.12" } }, "fragment-cache": { @@ -2627,7 +2562,7 @@ "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", "dev": true, "requires": { - "map-cache": "0.2.2" + "map-cache": "^0.2.2" } }, "from": { @@ -2642,7 +2577,7 @@ "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=", "dev": true, "requires": { - "null-check": "1.0.0" + "null-check": "^1.0.0" } }, "fs-constants": { @@ -2651,24 +2586,13 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, "fs-extra": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", - "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=", - "dev": true, + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", + "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "2.4.0", - "klaw": "1.3.1", - "path-is-absolute": "1.0.1", - "rimraf": "2.6.2" - }, - "dependencies": { - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - } + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" } }, "fs-extra-p": { @@ -2676,69 +2600,40 @@ "resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.6.1.tgz", "integrity": "sha512-IsTMbUS0svZKZTvqF4vDS9c/L7Mw9n8nZQWWeSzAGacOSe+8CzowhUN0tdZEZFIJNP5HC7L9j3MMikz/G4hDeQ==", "requires": { - "bluebird-lst": "1.0.5", - "fs-extra": "6.0.1" - }, - "dependencies": { - "fs-extra": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", - "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", - "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "4.0.0", - "universalify": "0.1.2" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "4.1.11" - } - } + "bluebird-lst": "^1.0.5", + "fs-extra": "^6.0.1" } }, "fs.realpath": { - "version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, "fsevents": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", - "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", + "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", "dev": true, "optional": true, "requires": { - "nan": "2.10.0", - "node-pre-gyp": "0.6.39" + "nan": "^2.9.2", + "node-pre-gyp": "^0.10.0" }, "dependencies": { "abbrev": { - "version": "1.1.0", + "version": "1.1.1", "bundled": true, "dev": true, "optional": true }, - "ajv": { - "version": "4.11.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" - } - }, "ansi-regex": { "version": "2.1.1", "bundled": true, "dev": true }, "aproba": { - "version": "1.1.1", + "version": "1.2.0", "bundled": true, "dev": true, "optional": true @@ -2749,92 +2644,26 @@ "dev": true, "optional": true, "requires": { - "delegates": "1.0.0", - "readable-stream": "2.2.9" + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" } }, - "asn1": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, - "assert-plus": { - "version": "0.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "asynckit": { - "version": "0.4.0", - "bundled": true, - "dev": true, - "optional": true - }, - "aws-sign2": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "aws4": { - "version": "1.6.0", - "bundled": true, - "dev": true, - "optional": true - }, "balanced-match": { - "version": "0.4.2", - "bundled": true, - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "block-stream": { - "version": "0.0.9", - "bundled": true, - "dev": true, - "requires": { - "inherits": "2.0.3" - } - }, - "boom": { - "version": "2.10.1", - "bundled": true, - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, - "brace-expansion": { - "version": "1.1.7", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "0.4.2", - "concat-map": "0.0.1" - } - }, - "buffer-shims": { "version": "1.0.0", "bundled": true, "dev": true }, - "caseless": { - "version": "0.12.0", + "brace-expansion": { + "version": "1.1.11", "bundled": true, "dev": true, - "optional": true + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } }, - "co": { - "version": "4.6.0", + "chownr": { + "version": "1.0.1", "bundled": true, "dev": true, "optional": true @@ -2844,14 +2673,6 @@ "bundled": true, "dev": true }, - "combined-stream": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "requires": { - "delayed-stream": "1.0.0" - } - }, "concat-map": { "version": "0.0.1", "bundled": true, @@ -2865,35 +2686,11 @@ "core-util-is": { "version": "1.0.2", "bundled": true, - "dev": true - }, - "cryptiles": { - "version": "2.0.5", - "bundled": true, "dev": true, - "requires": { - "boom": "2.10.1" - } - }, - "dashdash": { - "version": "1.14.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } + "optional": true }, "debug": { - "version": "2.6.8", + "version": "2.6.9", "bundled": true, "dev": true, "optional": true, @@ -2902,16 +2699,11 @@ } }, "deep-extend": { - "version": "0.4.2", + "version": "0.5.1", "bundled": true, "dev": true, "optional": true }, - "delayed-stream": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, "delegates": { "version": "1.0.0", "bundled": true, @@ -2919,74 +2711,25 @@ "optional": true }, "detect-libc": { - "version": "1.0.2", + "version": "1.0.3", "bundled": true, "dev": true, "optional": true }, - "ecc-jsbn": { - "version": "0.1.1", + "fs-minipass": { + "version": "1.2.5", "bundled": true, "dev": true, "optional": true, "requires": { - "jsbn": "0.1.1" - } - }, - "extend": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "extsprintf": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "bundled": true, - "dev": true, - "optional": true - }, - "form-data": { - "version": "2.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.15" + "minipass": "^2.2.1" } }, "fs.realpath": { "version": "1.0.0", "bundled": true, - "dev": true - }, - "fstream": { - "version": "1.0.11", - "bundled": true, "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "inherits": "2.0.3", - "mkdirp": "0.5.1", - "rimraf": "2.6.1" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fstream": "1.0.11", - "inherits": "2.0.3", - "minimatch": "3.0.4" - } + "optional": true }, "gauge": { "version": "2.7.4", @@ -2994,65 +2737,28 @@ "dev": true, "optional": true, "requires": { - "aproba": "1.1.1", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "getpass": { - "version": "0.1.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" } }, "glob": { "version": "7.1.2", "bundled": true, "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "graceful-fs": { - "version": "4.1.11", - "bundled": true, - "dev": true - }, - "har-schema": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "har-validator": { - "version": "4.2.1", - "bundled": true, - "dev": true, "optional": true, "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, "has-unicode": { @@ -3061,40 +2767,32 @@ "dev": true, "optional": true }, - "hawk": { - "version": "3.1.3", - "bundled": true, - "dev": true, - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hoek": { - "version": "2.16.3", - "bundled": true, - "dev": true - }, - "http-signature": { - "version": "1.1.1", + "iconv-lite": { + "version": "0.4.21", "bundled": true, "dev": true, "optional": true, "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.0", - "sshpk": "1.13.0" + "safer-buffer": "^2.1.0" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" } }, "inflight": { "version": "1.0.6", "bundled": true, "dev": true, + "optional": true, "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "once": "^1.3.0", + "wrappy": "1" } }, "inherits": { @@ -3103,7 +2801,7 @@ "dev": true }, "ini": { - "version": "1.3.4", + "version": "1.3.5", "bundled": true, "dev": true, "optional": true @@ -3113,107 +2811,21 @@ "bundled": true, "dev": true, "requires": { - "number-is-nan": "1.0.1" + "number-is-nan": "^1.0.0" } }, - "is-typedarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, "isarray": { "version": "1.0.0", "bundled": true, - "dev": true - }, - "isstream": { - "version": "0.1.2", - "bundled": true, "dev": true, "optional": true }, - "jodid25519": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsonify": "0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "jsonify": { - "version": "0.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "jsprim": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.0.2", - "json-schema": "0.2.3", - "verror": "1.3.6" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "mime-db": { - "version": "1.27.0", - "bundled": true, - "dev": true - }, - "mime-types": { - "version": "2.1.15", - "bundled": true, - "dev": true, - "requires": { - "mime-db": "1.27.0" - } - }, "minimatch": { "version": "3.0.4", "bundled": true, "dev": true, "requires": { - "brace-expansion": "1.1.7" + "brace-expansion": "^1.1.7" } }, "minimist": { @@ -3221,6 +2833,24 @@ "bundled": true, "dev": true }, + "minipass": { + "version": "2.2.4", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "^5.1.1", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, "mkdirp": { "version": "0.5.1", "bundled": true, @@ -3235,23 +2865,40 @@ "dev": true, "optional": true }, - "node-pre-gyp": { - "version": "0.6.39", + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "dev": true, + "optional": true + }, + "needle": { + "version": "2.2.0", "bundled": true, "dev": true, "optional": true, "requires": { - "detect-libc": "1.0.2", - "hawk": "3.1.3", - "mkdirp": "0.5.1", - "nopt": "4.0.1", - "npmlog": "4.1.0", - "rc": "1.2.1", - "request": "2.81.0", - "rimraf": "2.6.1", - "semver": "5.3.0", - "tar": "2.2.1", - "tar-pack": "3.4.0" + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.0", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.1.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" } }, "nopt": { @@ -3260,20 +2907,36 @@ "dev": true, "optional": true, "requires": { - "abbrev": "1.1.0", - "osenv": "0.1.4" + "abbrev": "1", + "osenv": "^0.1.4" } }, - "npmlog": { - "version": "4.1.0", + "npm-bundled": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.1.10", "bundled": true, "dev": true, "optional": true, "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" } }, "number-is-nan": { @@ -3281,12 +2944,6 @@ "bundled": true, "dev": true }, - "oauth-sign": { - "version": "0.8.2", - "bundled": true, - "dev": true, - "optional": true - }, "object-assign": { "version": "4.1.1", "bundled": true, @@ -3298,7 +2955,7 @@ "bundled": true, "dev": true, "requires": { - "wrappy": "1.0.2" + "wrappy": "1" } }, "os-homedir": { @@ -3314,53 +2971,37 @@ "optional": true }, "osenv": { - "version": "0.1.4", + "version": "0.1.5", "bundled": true, "dev": true, "optional": true, "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" } }, "path-is-absolute": { "version": "1.0.1", "bundled": true, - "dev": true - }, - "performance-now": { - "version": "0.2.0", - "bundled": true, "dev": true, "optional": true }, "process-nextick-args": { - "version": "1.0.7", - "bundled": true, - "dev": true - }, - "punycode": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true - }, - "qs": { - "version": "6.4.0", + "version": "2.0.0", "bundled": true, "dev": true, "optional": true }, "rc": { - "version": "1.2.1", + "version": "1.2.7", "bundled": true, "dev": true, "optional": true, "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.4", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" + "deep-extend": "^0.5.1", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" }, "dependencies": { "minimist": { @@ -3372,64 +3013,48 @@ } }, "readable-stream": { - "version": "2.2.9", - "bundled": true, - "dev": true, - "requires": { - "buffer-shims": "1.0.0", - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "1.0.1", - "util-deprecate": "1.0.2" - } - }, - "request": { - "version": "2.81.0", + "version": "2.3.6", "bundled": true, "dev": true, "optional": true, "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.15", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.0.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.2", - "tunnel-agent": "0.6.0", - "uuid": "3.0.1" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, "rimraf": { - "version": "2.6.1", + "version": "2.6.2", "bundled": true, "dev": true, + "optional": true, "requires": { - "glob": "7.1.2" + "glob": "^7.0.5" } }, "safe-buffer": { - "version": "5.0.1", + "version": "5.1.1", "bundled": true, "dev": true }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, "semver": { - "version": "5.3.0", + "version": "5.5.0", "bundled": true, "dev": true, "optional": true @@ -3446,69 +3071,31 @@ "dev": true, "optional": true }, - "sntp": { - "version": "1.0.9", - "bundled": true, - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, - "sshpk": { - "version": "1.13.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jodid25519": "1.0.2", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, "string-width": { "version": "1.0.2", "bundled": true, "dev": true, "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" } }, "string_decoder": { - "version": "1.0.1", + "version": "1.1.1", "bundled": true, "dev": true, + "optional": true, "requires": { - "safe-buffer": "5.0.1" + "safe-buffer": "~5.1.0" } }, - "stringstream": { - "version": "0.0.5", - "bundled": true, - "dev": true, - "optional": true - }, "strip-ansi": { "version": "3.0.1", "bundled": true, "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "strip-json-comments": { @@ -3518,94 +3105,44 @@ "optional": true }, "tar": { - "version": "2.2.1", - "bundled": true, - "dev": true, - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - }, - "tar-pack": { - "version": "3.4.0", + "version": "4.4.1", "bundled": true, "dev": true, "optional": true, "requires": { - "debug": "2.6.8", - "fstream": "1.0.11", - "fstream-ignore": "1.0.5", - "once": "1.4.0", - "readable-stream": "2.2.9", - "rimraf": "2.6.1", - "tar": "2.2.1", - "uid-number": "0.0.6" + "chownr": "^1.0.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.2.4", + "minizlib": "^1.1.0", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.1", + "yallist": "^3.0.2" } }, - "tough-cookie": { - "version": "2.3.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "punycode": "1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "bundled": true, - "dev": true, - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "bundled": true, - "dev": true, - "optional": true - }, "util-deprecate": { "version": "1.0.2", "bundled": true, - "dev": true - }, - "uuid": { - "version": "3.0.1", - "bundled": true, "dev": true, "optional": true }, - "verror": { - "version": "1.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "extsprintf": "1.0.2" - } - }, "wide-align": { "version": "1.1.2", "bundled": true, "dev": true, "optional": true, "requires": { - "string-width": "1.0.2" + "string-width": "^1.0.2" } }, "wrappy": { "version": "1.0.2", "bundled": true, "dev": true + }, + "yallist": { + "version": "3.0.2", + "bundled": true, + "dev": true } } }, @@ -3614,34 +3151,14 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.0", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.3" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "1.0.1" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" } }, "get-stdin": { @@ -3668,7 +3185,7 @@ "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", "dev": true, "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" } }, "github-from-package": { @@ -3677,16 +3194,17 @@ "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" }, "glob": { - "version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "dev": true, "requires": { - "fs.realpath": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "inflight": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "path-is-absolute": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, "glob-base": { @@ -3695,8 +3213,8 @@ "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", "dev": true, "requires": { - "glob-parent": "2.0.0", - "is-glob": "2.0.1" + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" } }, "glob-parent": { @@ -3705,7 +3223,7 @@ "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", "dev": true, "requires": { - "is-glob": "2.0.1" + "is-glob": "^2.0.0" } }, "global-dirs": { @@ -3714,7 +3232,7 @@ "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", "dev": true, "requires": { - "ini": "1.3.5" + "ini": "^1.3.4" } }, "globals": { @@ -3729,17 +3247,17 @@ "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", "dev": true, "requires": { - "create-error-class": "3.0.2", - "duplexer3": "0.1.4", - "get-stream": "3.0.0", - "is-redirect": "1.0.0", - "is-retry-allowed": "1.1.0", - "is-stream": "1.1.0", - "lowercase-keys": "1.0.1", - "safe-buffer": "5.1.1", - "timed-out": "4.0.1", - "unzip-response": "2.0.1", - "url-parse-lax": "1.0.0" + "create-error-class": "^3.0.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "unzip-response": "^2.0.1", + "url-parse-lax": "^1.0.0" } }, "graceful-fs": { @@ -3753,10 +3271,27 @@ "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", "dev": true, "requires": { - "async": "1.5.2", - "optimist": "0.6.1", - "source-map": "0.4.4", - "uglify-js": "2.8.29" + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4", + "uglify-js": "^2.6" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } } }, "har-schema": { @@ -3771,17 +3306,25 @@ "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", "dev": true, "requires": { - "ajv": "5.5.2", - "har-schema": "2.0.0" + "ajv": "^5.1.0", + "har-schema": "^2.0.0" } }, "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^0.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", + "dev": true + } } }, "has-binary": { @@ -3824,9 +3367,9 @@ "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", "dev": true, "requires": { - "get-value": "2.0.6", - "has-values": "1.0.0", - "isobject": "3.0.1" + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" }, "dependencies": { "isobject": { @@ -3843,8 +3386,8 @@ "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", "dev": true, "requires": { - "is-number": "3.0.0", - "kind-of": "4.0.0" + "is-number": "^3.0.0", + "kind-of": "^4.0.0" }, "dependencies": { "is-number": { @@ -3853,7 +3396,7 @@ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { @@ -3862,7 +3405,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -3873,7 +3416,7 @@ "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -3884,34 +3427,18 @@ "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", "dev": true, "requires": { - "inherits": "2.0.3", - "safe-buffer": "5.1.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" } }, "hash.js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", - "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz", + "integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==", "dev": true, "requires": { - "inherits": "2.0.3", - "minimalistic-assert": "1.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" } }, "hmac-drbg": { @@ -3920,9 +3447,9 @@ "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", "dev": true, "requires": { - "hash.js": "1.1.3", - "minimalistic-assert": "1.0.1", - "minimalistic-crypto-utils": "1.0.1" + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" } }, "home-path": { @@ -3932,9 +3459,9 @@ "dev": true }, "hosted-git-info": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", - "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", "dev": true }, "http-errors": { @@ -3943,28 +3470,21 @@ "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "dev": true, "requires": { - "depd": "1.1.2", + "depd": "~1.1.2", "inherits": "2.0.3", "setprototypeof": "1.1.0", - "statuses": "1.5.0" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "statuses": ">= 1.4.0 < 2" } }, "http-proxy": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.16.2.tgz", - "integrity": "sha1-Bt/ykpUr9k2+hHH6nfcwZtTzd0I=", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", + "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", "dev": true, "requires": { - "eventemitter3": "1.2.0", - "requires-port": "1.0.0" + "eventemitter3": "^3.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" } }, "http-signature": { @@ -3973,9 +3493,9 @@ "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "dev": true, "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.14.2" + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" } }, "https-browserify": { @@ -3985,15 +3505,18 @@ "dev": true }, "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", - "dev": true + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } }, "ieee754": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.11.tgz", - "integrity": "sha512-VhDzCKN7K8ufStx/CLj5/PDTMgph+qwN5Pkd5i0sGnVwk56zJ0lkT8Qzi1xqWLS0Wp29DgDtNeS7v8/wMoZeHg==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", + "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==", "dev": true }, "ignore-by-default": { @@ -4020,7 +3543,7 @@ "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", "dev": true, "requires": { - "repeating": "2.0.1" + "repeating": "^2.0.0" } }, "indexof": { @@ -4030,18 +3553,19 @@ "dev": true }, "inflight": { - "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "requires": { - "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "wrappy": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + "once": "^1.3.0", + "wrappy": "1" } }, "inherits": { - "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.5", @@ -4054,7 +3578,7 @@ "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", "dev": true, "requires": { - "source-map": "0.5.7" + "source-map": "~0.5.3" }, "dependencies": { "source-map": { @@ -4071,7 +3595,7 @@ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", "dev": true, "requires": { - "loose-envify": "1.3.1" + "loose-envify": "^1.0.0" } }, "is-accessor-descriptor": { @@ -4080,7 +3604,7 @@ "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" } }, "is-arrayish": { @@ -4095,7 +3619,7 @@ "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", "dev": true, "requires": { - "binary-extensions": "1.11.0" + "binary-extensions": "^1.0.0" } }, "is-buffer": { @@ -4110,7 +3634,7 @@ "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", "dev": true, "requires": { - "builtin-modules": "1.1.1" + "builtin-modules": "^1.0.0" } }, "is-ci": { @@ -4119,7 +3643,7 @@ "integrity": "sha512-c7TnwxLePuqIlxHgr7xtxzycJPegNHFuIrBkwbf8hc58//+Op1CqFkyS+xnIMkwn9UsJIwc174BIjkyBmSpjKg==", "dev": true, "requires": { - "ci-info": "1.1.3" + "ci-info": "^1.0.0" } }, "is-data-descriptor": { @@ -4128,7 +3652,7 @@ "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" } }, "is-descriptor": { @@ -4137,9 +3661,9 @@ "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "dev": true, "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" }, "dependencies": { "kind-of": { @@ -4162,7 +3686,7 @@ "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", "dev": true, "requires": { - "is-primitive": "2.0.0" + "is-primitive": "^2.0.0" } }, "is-extendable": { @@ -4183,13 +3707,16 @@ "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", "dev": true, "requires": { - "number-is-nan": "1.0.1" + "number-is-nan": "^1.0.0" } }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } }, "is-glob": { "version": "2.0.1", @@ -4197,7 +3724,7 @@ "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "dev": true, "requires": { - "is-extglob": "1.0.0" + "is-extglob": "^1.0.0" } }, "is-installed-globally": { @@ -4206,8 +3733,8 @@ "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", "dev": true, "requires": { - "global-dirs": "0.1.1", - "is-path-inside": "1.0.1" + "global-dirs": "^0.1.0", + "is-path-inside": "^1.0.0" } }, "is-npm": { @@ -4222,7 +3749,7 @@ "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" } }, "is-obj": { @@ -4231,30 +3758,13 @@ "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, - "is-odd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", - "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", - "dev": true, - "requires": { - "is-number": "4.0.0" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - } - } - }, "is-path-inside": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", "dev": true, "requires": { - "path-is-inside": "1.0.2" + "path-is-inside": "^1.0.1" } }, "is-plain-object": { @@ -4263,7 +3773,7 @@ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, "requires": { - "isobject": "3.0.1" + "isobject": "^3.0.1" }, "dependencies": { "isobject": { @@ -4333,10 +3843,13 @@ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isbinaryfile": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.2.tgz", - "integrity": "sha1-Sj6XTsDLqQBNP8bN5yCeppNopiE=", - "dev": true + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz", + "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", + "dev": true, + "requires": { + "buffer-alloc": "^1.2.0" + } }, "isexe": { "version": "2.0.0", @@ -4365,42 +3878,26 @@ "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", "dev": true, "requires": { - "abbrev": "1.0.9", - "async": "1.5.2", - "escodegen": "1.8.1", - "esprima": "2.7.3", - "glob": "5.0.15", - "handlebars": "4.0.11", - "js-yaml": "3.10.0", - "mkdirp": "0.5.1", - "nopt": "3.0.6", - "once": "1.4.0", - "resolve": "1.1.7", - "supports-color": "3.2.3", - "which": "1.3.0", - "wordwrap": "1.0.0" + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" }, "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", "dev": true }, "esprima": { @@ -4415,79 +3912,24 @@ "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", "dev": true, "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, "resolve": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", "dev": true }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "1.0.0" - } - }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true } } }, @@ -4497,44 +3939,18 @@ "integrity": "sha512-duj6AlLcsWNwUpfyfHt0nWIeRiZpuShnP40YTxOGQgtaN8fd6JYSxsvxUphTDy8V5MfDXo4s/xVCIIvVCO808g==", "dev": true, "requires": { - "async": "2.6.0", - "compare-versions": "3.1.0", - "fileset": "2.0.3", - "istanbul-lib-coverage": "1.2.0", - "istanbul-lib-hook": "1.2.0", - "istanbul-lib-instrument": "1.10.1", - "istanbul-lib-report": "1.1.4", - "istanbul-lib-source-maps": "1.2.4", - "istanbul-reports": "1.3.0", - "js-yaml": "3.10.0", - "mkdirp": "0.5.1", - "once": "1.4.0" - }, - "dependencies": { - "async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", - "dev": true, - "requires": { - "lodash": "4.17.4" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - } + "async": "^2.1.4", + "compare-versions": "^3.1.0", + "fileset": "^2.0.2", + "istanbul-lib-coverage": "^1.2.0", + "istanbul-lib-hook": "^1.2.0", + "istanbul-lib-instrument": "^1.10.1", + "istanbul-lib-report": "^1.1.4", + "istanbul-lib-source-maps": "^1.2.4", + "istanbul-reports": "^1.3.0", + "js-yaml": "^3.7.0", + "mkdirp": "^0.5.1", + "once": "^1.4.0" } }, "istanbul-lib-coverage": { @@ -4544,12 +3960,12 @@ "dev": true }, "istanbul-lib-hook": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.0.tgz", - "integrity": "sha512-p3En6/oGkFQV55Up8ZPC2oLxvgSxD8CzA0yBrhRZSh3pfv3OFj9aSGVC0yoerAi/O4u7jUVnOGVX1eVFM+0tmQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.1.tgz", + "integrity": "sha512-eLAMkPG9FU0v5L02lIkcj/2/Zlz9OuluaXikdr5iStk8FDbSwAixTK9TkYxbF0eNnzAJTwM2fkV2A1tpsIp4Jg==", "dev": true, "requires": { - "append-transform": "0.4.0" + "append-transform": "^1.0.0" } }, "istanbul-lib-instrument": { @@ -4558,21 +3974,13 @@ "integrity": "sha512-1dYuzkOCbuR5GRJqySuZdsmsNKPL3PTuyPevQfoCXJePT9C8y1ga75neU+Tuy9+yS3G/dgx8wgOmp2KLpgdoeQ==", "dev": true, "requires": { - "babel-generator": "6.26.1", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "istanbul-lib-coverage": "1.2.0", - "semver": "5.5.0" - }, - "dependencies": { - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true - } + "babel-generator": "^6.18.0", + "babel-template": "^6.16.0", + "babel-traverse": "^6.18.0", + "babel-types": "^6.18.0", + "babylon": "^6.18.0", + "istanbul-lib-coverage": "^1.2.0", + "semver": "^5.3.0" } }, "istanbul-lib-report": { @@ -4581,51 +3989,25 @@ "integrity": "sha512-Azqvq5tT0U09nrncK3q82e/Zjkxa4tkFZv7E6VcqP0QCPn6oNljDPfrZEC/umNXds2t7b8sRJfs6Kmpzt8m2kA==", "dev": true, "requires": { - "istanbul-lib-coverage": "1.2.0", - "mkdirp": "0.5.1", - "path-parse": "1.0.5", - "supports-color": "3.2.3" - }, - "dependencies": { - "path-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", - "dev": true - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "1.0.0" - } - } + "istanbul-lib-coverage": "^1.2.0", + "mkdirp": "^0.5.1", + "path-parse": "^1.0.5", + "supports-color": "^3.1.2" } }, "istanbul-lib-source-maps": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.4.tgz", - "integrity": "sha512-UzuK0g1wyQijiaYQxj/CdNycFhAd2TLtO2obKQMTZrZ1jzEMRY3rvpASEKkaxbRR6brvdovfA03znPa/pXcejg==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.5.tgz", + "integrity": "sha512-8O2T/3VhrQHn0XcJbP1/GN7kXMiRAlPi+fj3uEHrjBD8Oz7Py0prSC25C09NuAZS6bgW1NNKAvCSHZXB0irSGA==", "dev": true, "requires": { - "debug": "3.1.0", - "istanbul-lib-coverage": "1.2.0", - "mkdirp": "0.5.1", - "rimraf": "2.6.2", - "source-map": "0.5.7" + "debug": "^3.1.0", + "istanbul-lib-coverage": "^1.2.0", + "mkdirp": "^0.5.1", + "rimraf": "^2.6.1", + "source-map": "^0.5.3" }, "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -4640,7 +4022,7 @@ "integrity": "sha512-y2Z2IMqE1gefWUaVjrBm0mSKvUkaBy9Vqz8iwr/r40Y9hBbIteH5wqHG/9DLTfJ9xUnUT2j7A3+VVJ6EaYBllA==", "dev": true, "requires": { - "handlebars": "4.0.11" + "handlebars": "^4.0.3" } }, "jasmine": { @@ -4649,103 +4031,15 @@ "integrity": "sha1-K9Wf1+xuwOistk4J9Fpo7SrRlSo=", "dev": true, "requires": { - "glob": "7.1.2", - "jasmine-core": "3.1.0" + "glob": "^7.0.6", + "jasmine-core": "~3.1.0" }, "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, "jasmine-core": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.1.0.tgz", "integrity": "sha1-pHheE11d9lAk38kiSVPfWFvSdmw=", "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true } } }, @@ -4770,43 +4064,12 @@ "integrity": "sha512-sKR3Dj2VkPbNBxN65KEYIKL5O+/6sG+j4zyhvUUUMuN4CjsuDiUhoneaj4xsIY8XvyTD4l/b9jtJ1uwSb/mZPg==", "dev": true, "requires": { - "error-stack-parser": "2.0.1", - "minimatch": "3.0.4", - "source-map": "0.5.7", - "source-map-resolve": "0.5.1" + "error-stack-parser": "^2.0.1", + "minimatch": "^3.0.4", + "source-map": "^0.5.7", + "source-map-resolve": "^0.5.0" }, "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -4822,13 +4085,12 @@ "dev": true }, "js-yaml": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", - "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", - "dev": true, + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", "requires": { - "argparse": "1.0.10", - "esprima": "4.0.0" + "argparse": "^1.0.7", + "esprima": "^4.0.0" } }, "jsbn": { @@ -4869,12 +4131,11 @@ "dev": true }, "jsonfile": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", - "dev": true, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "requires": { - "graceful-fs": "4.1.11" + "graceful-fs": "^4.1.6" } }, "jsprim": { @@ -4895,134 +4156,46 @@ "integrity": "sha512-k5pBjHDhmkdaUccnC7gE3mBzZjcxyxYsYVaqiL2G5AqlfLyBO5nw2VdNK+O16cveEPd/gIOWULH7gkiYYwVNHg==", "dev": true, "requires": { - "bluebird": "3.5.1", - "body-parser": "1.18.2", - "chokidar": "1.7.0", - "colors": "1.1.2", - "combine-lists": "1.0.1", - "connect": "3.6.6", - "core-js": "2.4.1", - "di": "0.0.1", - "dom-serialize": "2.2.1", - "expand-braces": "0.1.2", - "glob": "7.1.2", - "graceful-fs": "4.1.11", - "http-proxy": "1.16.2", - "isbinaryfile": "3.0.2", - "lodash": "3.10.1", - "log4js": "0.6.38", - "mime": "1.6.0", - "minimatch": "3.0.4", - "optimist": "0.6.1", - "qjobs": "1.2.0", - "range-parser": "1.2.0", - "rimraf": "2.6.2", - "safe-buffer": "5.1.1", + "bluebird": "^3.3.0", + "body-parser": "^1.16.1", + "chokidar": "^1.4.1", + "colors": "^1.1.0", + "combine-lists": "^1.0.0", + "connect": "^3.6.0", + "core-js": "^2.2.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.0", + "expand-braces": "^0.1.1", + "glob": "^7.1.1", + "graceful-fs": "^4.1.2", + "http-proxy": "^1.13.0", + "isbinaryfile": "^3.0.0", + "lodash": "^3.8.0", + "log4js": "^0.6.31", + "mime": "^1.3.4", + "minimatch": "^3.0.2", + "optimist": "^0.6.1", + "qjobs": "^1.1.4", + "range-parser": "^1.2.0", + "rimraf": "^2.6.0", + "safe-buffer": "^5.0.1", "socket.io": "1.7.3", - "source-map": "0.5.7", + "source-map": "^0.5.3", "tmp": "0.0.31", - "useragent": "2.3.0" + "useragent": "^2.1.12" }, "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, "lodash": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", "dev": true }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true } } }, @@ -5032,8 +4205,8 @@ "integrity": "sha512-uf/ZVpAabDBPvdPdveyk1EPgbnloPvFFGgmRhYLTDH7gEB4nZdSBk8yTU47w1g/drLSx5uMOkjKk7IWKfWg/+w==", "dev": true, "requires": { - "fs-access": "1.0.1", - "which": "1.3.0" + "fs-access": "^1.0.0", + "which": "^1.2.1" } }, "karma-cli": { @@ -5042,76 +4215,22 @@ "integrity": "sha1-rmw8WKMTodALRRZMRVubhs4X+WA=", "dev": true, "requires": { - "resolve": "1.7.1" - }, - "dependencies": { - "path-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", - "dev": true - }, - "resolve": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", - "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", - "dev": true, - "requires": { - "path-parse": "1.0.5" - } - } + "resolve": "^1.1.6" } }, "karma-coverage": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-1.1.1.tgz", - "integrity": "sha1-Wv+LOc9plNwi3kyENix2ABtjfPY=", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-1.1.2.tgz", + "integrity": "sha512-eQawj4Cl3z/CjxslYy9ariU4uDh7cCNFZHNWXWRpl0pNeblY/4wHR7M7boTYXWrn9bY0z2pZmr11eKje/S/hIw==", "dev": true, "requires": { - "dateformat": "1.0.12", - "istanbul": "0.4.5", - "lodash": "3.10.1", - "minimatch": "3.0.4", - "source-map": "0.5.7" + "dateformat": "^1.0.6", + "istanbul": "^0.4.0", + "lodash": "^4.17.0", + "minimatch": "^3.0.0", + "source-map": "^0.5.1" }, "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -5121,46 +4240,13 @@ } }, "karma-coverage-istanbul-reporter": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-1.4.2.tgz", - "integrity": "sha512-sQHexslLF+QHzaKfK8+onTYMyvSwv+p5cDayVxhpEELGa3z0QuB+l0IMsicIkkBNMOJKQaqueiRoW7iuo7lsog==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-1.4.3.tgz", + "integrity": "sha1-O13/RmT6W41RlrmInj9hwforgNk=", "dev": true, "requires": { - "istanbul-api": "1.3.1", - "minimatch": "3.0.4" - }, - "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - } + "istanbul-api": "^1.3.1", + "minimatch": "^3.0.4" } }, "karma-detect-browsers": { @@ -5169,7 +4255,7 @@ "integrity": "sha512-EFku2S5IpUEpJR2XxJa/onW6tIuapa3kYWJDD7Tk6LqhhIxfKWvJ+vnleLop6utXT28204hZptnfH7PGSmk4Nw==", "dev": true, "requires": { - "which": "1.3.0" + "which": "^1.2.4" } }, "karma-edge-launcher": { @@ -5188,9 +4274,9 @@ "dev": true }, "karma-jasmine": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.1.1.tgz", - "integrity": "sha1-b+hA51oRYAydkehLM8RY4cRqNSk=", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.1.2.tgz", + "integrity": "sha1-OU8rJf+0pkS5rabyLUQ+L9CIhsM=", "dev": true }, "karma-jasmine-html-reporter": { @@ -5199,7 +4285,7 @@ "integrity": "sha1-SKjl7xiAdhfuK14zwRlMNbQ5Ukw=", "dev": true, "requires": { - "karma-jasmine": "1.1.1" + "karma-jasmine": "^1.0.2" } }, "karma-safari-launcher": { @@ -5209,181 +4295,81 @@ "dev": true }, "karma-typescript": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/karma-typescript/-/karma-typescript-3.0.12.tgz", - "integrity": "sha1-qiy90RFEKgnG28uq6vSEmWVMaRM=", + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/karma-typescript/-/karma-typescript-3.0.13.tgz", + "integrity": "sha1-iUivvRA6wZh6WWGg9DoboocQZ80=", "dev": true, "requires": { - "acorn": "4.0.13", - "assert": "1.4.1", - "async": "2.6.0", - "browser-resolve": "1.11.2", - "browserify-zlib": "0.2.0", - "buffer": "5.1.0", - "combine-source-map": "0.8.0", - "console-browserify": "1.1.0", - "constants-browserify": "1.0.0", - "convert-source-map": "1.5.1", - "crypto-browserify": "3.12.0", - "diff": "3.5.0", - "domain-browser": "1.2.0", - "events": "1.1.1", - "glob": "7.1.2", - "https-browserify": "1.0.0", + "acorn": "^4.0.4", + "assert": "^1.4.1", + "async": "^2.1.4", + "browser-resolve": "^1.11.0", + "browserify-zlib": "^0.2.0", + "buffer": "^5.0.6", + "combine-source-map": "^0.8.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "convert-source-map": "^1.5.0", + "crypto-browserify": "^3.11.1", + "diff": "^3.2.0", + "domain-browser": "^1.1.7", + "events": "^1.1.1", + "glob": "^7.1.1", + "https-browserify": "^1.0.0", "istanbul": "0.4.5", - "json-stringify-safe": "5.0.1", - "karma-coverage": "1.1.1", - "lodash": "4.17.4", - "log4js": "1.1.1", - "minimatch": "3.0.4", - "os-browserify": "0.3.0", - "pad": "2.0.3", + "json-stringify-safe": "^5.0.1", + "karma-coverage": "^1.1.1", + "lodash": "^4.17.4", + "log4js": "^1.1.1", + "minimatch": "^3.0.3", + "os-browserify": "^0.3.0", + "pad": "^2.0.0", "path-browserify": "0.0.0", - "process": "0.11.10", - "punycode": "1.4.1", - "querystring-es3": "0.2.1", - "readable-stream": "2.3.6", - "remap-istanbul": "0.10.1", + "process": "^0.11.10", + "punycode": "^1.4.1", + "querystring-es3": "^0.2.1", + "readable-stream": "^2.3.3", + "remap-istanbul": "^0.10.1", "source-map": "0.6.1", - "stream-browserify": "2.0.1", - "stream-http": "2.8.1", - "string_decoder": "1.1.1", - "timers-browserify": "2.0.8", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.3", + "timers-browserify": "^2.0.2", "tmp": "0.0.29", "tty-browserify": "0.0.0", - "url": "0.11.0", - "util": "0.10.3", + "url": "^0.11.0", + "util": "^0.10.3", "vm-browserify": "0.0.4" }, "dependencies": { - "async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "requires": { - "lodash": "4.17.4" + "ms": "2.0.0" } }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, "log4js": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/log4js/-/log4js-1.1.1.tgz", "integrity": "sha1-wh0px2BAieTyVYM+f5SzRh3h/0M=", "dev": true, "requires": { - "debug": "2.6.9", - "semver": "5.5.0", - "streamroller": "0.4.1" + "debug": "^2.2.0", + "semver": "^5.3.0", + "streamroller": "^0.4.0" } }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, "tmp": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz", "integrity": "sha1-8lEl/w3Z2jzLDC3Tce4SiLuRKMA=", "dev": true, "requires": { - "os-tmpdir": "1.0.2" + "os-tmpdir": "~1.0.1" } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true } } }, @@ -5393,14 +4379,7 @@ "integrity": "sha1-igamV3/fY3PgqmsRInfmPex3/RI=", "requires": { "nan": "2.8.0", - "prebuild-install": "2.5.3" - }, - "dependencies": { - "nan": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", - "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=" - } + "prebuild-install": "^2.4.1" } }, "kind-of": { @@ -5409,7 +4388,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } }, "klaw": { @@ -5418,7 +4397,7 @@ "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", "dev": true, "requires": { - "graceful-fs": "4.1.11" + "graceful-fs": "^4.1.9" } }, "latest-version": { @@ -5427,7 +4406,7 @@ "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", "dev": true, "requires": { - "package-json": "4.0.1" + "package-json": "^4.0.0" } }, "lazy-cache": { @@ -5448,8 +4427,8 @@ "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", "dev": true, "requires": { - "prelude-ls": "1.1.2", - "type-check": "0.3.2" + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" } }, "load-json-file": { @@ -5458,17 +4437,31 @@ "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { - "graceful-fs": "4.1.11", - "parse-json": "2.2.0", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "strip-bom": "2.0.0" + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } } }, "lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "dev": true }, "lodash.isequal": { "version": "4.5.0", @@ -5487,16 +4480,10 @@ "integrity": "sha1-LElBFmldb7JUgJQ9P8hy5mKlIv0=", "dev": true, "requires": { - "readable-stream": "1.0.34", - "semver": "4.3.6" + "readable-stream": "~1.0.2", + "semver": "~4.3.3" }, "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -5509,12 +4496,18 @@ "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", "isarray": "0.0.1", - "string_decoder": "0.10.31" + "string_decoder": "~0.10.x" } }, + "semver": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", + "dev": true + }, "string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", @@ -5530,12 +4523,12 @@ "dev": true }, "loose-envify": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, "requires": { - "js-tokens": "3.0.2" + "js-tokens": "^3.0.0 || ^4.0.0" } }, "loud-rejection": { @@ -5544,8 +4537,8 @@ "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", "dev": true, "requires": { - "currently-unhandled": "0.4.1", - "signal-exit": "3.0.2" + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" } }, "lowdb": { @@ -5553,18 +4546,11 @@ "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz", "integrity": "sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==", "requires": { - "graceful-fs": "4.1.11", - "is-promise": "2.1.0", - "lodash": "4.17.4", - "pify": "3.0.0", - "steno": "0.4.4" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - } + "graceful-fs": "^4.1.3", + "is-promise": "^2.1.0", + "lodash": "4", + "pify": "^3.0.0", + "steno": "^0.4.1" } }, "lowercase-keys": { @@ -5574,13 +4560,13 @@ "dev": true }, "lru-cache": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.2.tgz", - "integrity": "sha512-wgeVXhrDwAWnIF/yZARsFnMBtdFXOg1b8RIrhilp+0iDYN4mdQcNZElDZ0e4B64BhaxeQ5zN7PMyvu7we1kPeQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", "dev": true, "requires": { - "pseudomap": "1.0.2", - "yallist": "2.1.2" + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" } }, "lunr": { @@ -5589,20 +4575,12 @@ "integrity": "sha512-ydJpB8CX8cZ/VE+KMaYaFcZ6+o2LruM6NG76VXdflYTgluvVemz1lW4anE+pyBbLvxJHZdvD1Jy/fOqdzAEJog==" }, "make-dir": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.2.0.tgz", - "integrity": "sha512-aNUAa4UMg/UougV25bbrU4ZaaKNjJ/3/xnvg/twpmKROPdKZPZ9wGgI0opdZzO8q/zUFawoUuixuOv33eZ61Iw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", "dev": true, "requires": { - "pify": "3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } + "pify": "^3.0.0" } }, "map-cache": { @@ -5629,25 +4607,23 @@ "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", "dev": true, "requires": { - "object-visit": "1.0.1" + "object-visit": "^1.0.0" } }, + "math-random": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", + "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", + "dev": true + }, "md5.js": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", "dev": true, "requires": { - "hash-base": "3.0.4", - "inherits": "2.0.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "hash-base": "^3.0.0", + "inherits": "^2.0.1" } }, "media-typer": { @@ -5662,24 +4638,16 @@ "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "dev": true, "requires": { - "camelcase-keys": "2.1.0", - "decamelize": "1.2.0", - "loud-rejection": "1.6.0", - "map-obj": "1.0.1", - "minimist": "1.2.0", - "normalize-package-data": "2.4.0", - "object-assign": "4.1.0", - "read-pkg-up": "1.0.1", - "redent": "1.0.0", - "trim-newlines": "1.0.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" } }, "micromatch": { @@ -5688,19 +4656,19 @@ "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", "dev": true, "requires": { - "arr-diff": "2.0.0", - "array-unique": "0.2.1", - "braces": "1.8.5", - "expand-brackets": "0.1.5", - "extglob": "0.3.2", - "filename-regex": "2.0.1", - "is-extglob": "1.0.0", - "is-glob": "2.0.1", - "kind-of": "3.2.2", - "normalize-path": "2.1.1", - "object.omit": "2.0.1", - "parse-glob": "3.0.4", - "regex-cache": "0.4.4" + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" } }, "miller-rabin": { @@ -5709,8 +4677,8 @@ "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", "dev": true, "requires": { - "bn.js": "4.11.8", - "brorand": "1.1.0" + "bn.js": "^4.0.0", + "brorand": "^1.0.1" } }, "mime": { @@ -5720,16 +4688,16 @@ "dev": true }, "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", + "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==" }, "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", + "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", "requires": { - "mime-db": "1.33.0" + "mime-db": "~1.35.0" } }, "mimic-response": { @@ -5750,18 +4718,18 @@ "dev": true }, "minimatch": { - "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "requires": { - "brace-expansion": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz" + "brace-expansion": "^1.1.7" } }, "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, "mixin-deep": { "version": "1.3.1", @@ -5769,8 +4737,8 @@ "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", "dev": true, "requires": { - "for-in": "1.0.2", - "is-extendable": "1.0.1" + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" }, "dependencies": { "is-extendable": { @@ -5779,7 +4747,7 @@ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dev": true, "requires": { - "is-plain-object": "2.0.4" + "is-plain-object": "^2.0.4" } } } @@ -5805,30 +4773,27 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", - "dev": true, - "optional": true + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", + "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=" }, "nanomatch": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", - "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", "dev": true, "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "fragment-cache": "0.2.1", - "is-odd": "2.0.0", - "is-windows": "1.0.2", - "kind-of": "6.0.2", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" }, "dependencies": { "arr-diff": { @@ -5849,8 +4814,8 @@ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", "dev": true, "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" } }, "is-extendable": { @@ -5859,7 +4824,7 @@ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dev": true, "requires": { - "is-plain-object": "2.0.4" + "is-plain-object": "^2.0.4" } }, "kind-of": { @@ -5881,14 +4846,7 @@ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.4.3.tgz", "integrity": "sha512-b656V5C0628gOOA2kwcpNA/bxdlqYF9FvxJ+qqVX0ctdXNVZpS8J6xEUYir3WAKc7U0BH/NRlSpNbGsy+azjeg==", "requires": { - "semver": "5.5.0" - }, - "dependencies": { - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" - } + "semver": "^5.4.1" } }, "node-fetch": { @@ -5902,21 +4860,21 @@ "integrity": "sha1-naYR6giYL0uUIGs760zJZl8gwwA=" }, "nodemon": { - "version": "1.17.3", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.17.3.tgz", - "integrity": "sha512-8AtS+wA5u6qoE12LONjqOzUzxAI5ObzSw6U5LgqpaO/0y6wwId4l5dN0ZulYyYdpLZD1MbkBp7GjG1hqaoRqYg==", + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.18.3.tgz", + "integrity": "sha512-XdVfAjGlDKU2nqoGgycxTndkJ5fdwvWJ/tlMGk2vHxMZBrSPVh86OM6z7viAv8BBJWjMgeuYQBofzr6LUoi+7g==", "dev": true, "requires": { - "chokidar": "2.0.3", - "debug": "3.1.0", - "ignore-by-default": "1.0.1", - "minimatch": "3.0.4", - "pstree.remy": "1.1.0", - "semver": "5.5.0", - "supports-color": "5.4.0", - "touch": "3.1.0", - "undefsafe": "2.0.2", - "update-notifier": "2.5.0" + "chokidar": "^2.0.2", + "debug": "^3.1.0", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.0.4", + "pstree.remy": "^1.1.0", + "semver": "^5.5.0", + "supports-color": "^5.2.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.2", + "update-notifier": "^2.3.0" }, "dependencies": { "anymatch": { @@ -5925,8 +4883,8 @@ "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", "dev": true, "requires": { - "micromatch": "3.1.10", - "normalize-path": "2.1.1" + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" } }, "arr-diff": { @@ -5941,38 +4899,22 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, "braces": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "dev": true, "requires": { - "arr-flatten": "1.1.0", - "array-unique": "0.3.2", - "extend-shallow": "2.0.1", - "fill-range": "4.0.0", - "isobject": "3.0.1", - "repeat-element": "1.1.2", - "snapdragon": "0.8.2", - "snapdragon-node": "2.1.1", - "split-string": "3.1.0", - "to-regex": "3.0.2" + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" }, "dependencies": { "extend-shallow": { @@ -5981,44 +4923,30 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } }, "chokidar": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", - "integrity": "sha512-zW8iXYZtXMx4kux/nuZVXjkLP+CyIK5Al5FHnj1OgTKGZfp4Oy6/ymtMSKFv3GD8DviEmUPmJg9eFdJ/JzudMg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", + "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", "dev": true, "requires": { - "anymatch": "2.0.0", - "async-each": "1.0.1", - "braces": "2.3.2", - "fsevents": "1.1.3", - "glob-parent": "3.1.0", - "inherits": "2.0.3", - "is-binary-path": "1.0.1", - "is-glob": "4.0.0", - "normalize-path": "2.1.1", - "path-is-absolute": "1.0.1", - "readdirp": "2.1.0", - "upath": "1.0.4" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" + "anymatch": "^2.0.0", + "async-each": "^1.0.0", + "braces": "^2.3.0", + "fsevents": "^1.2.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "lodash.debounce": "^4.0.8", + "normalize-path": "^2.1.1", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0", + "upath": "^1.0.5" } }, "expand-brackets": { @@ -6027,13 +4955,13 @@ "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", "dev": true, "requires": { - "debug": "2.6.9", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "posix-character-classes": "0.1.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" }, "dependencies": { "debug": { @@ -6051,7 +4979,7 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } }, "extend-shallow": { @@ -6060,7 +4988,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } }, "is-accessor-descriptor": { @@ -6069,7 +4997,7 @@ "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { @@ -6078,7 +5006,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -6089,7 +5017,7 @@ "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { @@ -6098,7 +5026,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -6109,9 +5037,9 @@ "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "dev": true, "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" } }, "kind-of": { @@ -6128,8 +5056,8 @@ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", "dev": true, "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" }, "dependencies": { "is-extendable": { @@ -6138,7 +5066,7 @@ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dev": true, "requires": { - "is-plain-object": "2.0.4" + "is-plain-object": "^2.0.4" } } } @@ -6149,14 +5077,14 @@ "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", "dev": true, "requires": { - "array-unique": "0.3.2", - "define-property": "1.0.0", - "expand-brackets": "2.1.4", - "extend-shallow": "2.0.1", - "fragment-cache": "0.2.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" }, "dependencies": { "define-property": { @@ -6165,7 +5093,7 @@ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dev": true, "requires": { - "is-descriptor": "1.0.2" + "is-descriptor": "^1.0.0" } }, "extend-shallow": { @@ -6174,7 +5102,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } @@ -6185,10 +5113,10 @@ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "dev": true, "requires": { - "extend-shallow": "2.0.1", - "is-number": "3.0.0", - "repeat-string": "1.6.1", - "to-regex-range": "2.1.1" + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" }, "dependencies": { "extend-shallow": { @@ -6197,7 +5125,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } @@ -6208,8 +5136,8 @@ "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", "dev": true, "requires": { - "is-glob": "3.1.0", - "path-dirname": "1.0.2" + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" }, "dependencies": { "is-glob": { @@ -6218,7 +5146,7 @@ "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", "dev": true, "requires": { - "is-extglob": "2.1.1" + "is-extglob": "^2.1.0" } } } @@ -6229,19 +5157,13 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-data-descriptor": { @@ -6250,7 +5172,7 @@ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-descriptor": { @@ -6259,9 +5181,9 @@ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dev": true, "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" } }, "is-extglob": { @@ -6276,7 +5198,7 @@ "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", "dev": true, "requires": { - "is-extglob": "2.1.1" + "is-extglob": "^2.1.1" } }, "is-number": { @@ -6285,7 +5207,7 @@ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { @@ -6294,7 +5216,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -6317,49 +5239,28 @@ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "dev": true, "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "braces": "2.3.2", - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "extglob": "2.0.4", - "fragment-cache": "0.2.1", - "kind-of": "6.0.2", - "nanomatch": "1.2.9", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" } }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true - }, "supports-color": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "dev": true, "requires": { - "has-flag": "3.0.0" + "has-flag": "^3.0.0" } } } @@ -6375,7 +5276,7 @@ "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", "dev": true, "requires": { - "abbrev": "1.0.9" + "abbrev": "1" } }, "normalize-package-data": { @@ -6384,10 +5285,10 @@ "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", "dev": true, "requires": { - "hosted-git-info": "2.6.0", - "is-builtin-module": "1.0.0", - "semver": "4.3.6", - "validate-npm-package-license": "3.0.3" + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" } }, "normalize-path": { @@ -6396,7 +5297,7 @@ "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "dev": true, "requires": { - "remove-trailing-separator": "1.1.0" + "remove-trailing-separator": "^1.0.1" } }, "npm-run-path": { @@ -6405,7 +5306,7 @@ "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "dev": true, "requires": { - "path-key": "2.0.1" + "path-key": "^2.0.0" } }, "npmlog": { @@ -6413,10 +5314,10 @@ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "requires": { - "are-we-there-yet": "1.1.5", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" } }, "nugget": { @@ -6425,20 +5326,23 @@ "integrity": "sha1-IBCVpIfhrTYIGzQy+jytpPjQcbA=", "dev": true, "requires": { - "debug": "2.6.9", - "minimist": "1.2.0", - "pretty-bytes": "1.0.4", - "progress-stream": "1.2.0", - "request": "2.87.0", - "single-line-log": "1.1.2", + "debug": "^2.1.3", + "minimist": "^1.1.0", + "pretty-bytes": "^1.0.2", + "progress-stream": "^1.1.0", + "request": "^2.45.0", + "single-line-log": "^1.1.2", "throttleit": "0.0.2" }, "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } } } }, @@ -6460,9 +5364,9 @@ "dev": true }, "object-assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", - "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-component": { "version": "0.0.3", @@ -6476,9 +5380,9 @@ "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", "dev": true, "requires": { - "copy-descriptor": "0.1.1", - "define-property": "0.2.5", - "kind-of": "3.2.2" + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" }, "dependencies": { "define-property": { @@ -6487,7 +5391,7 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } } } @@ -6504,7 +5408,7 @@ "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", "dev": true, "requires": { - "isobject": "3.0.1" + "isobject": "^3.0.0" }, "dependencies": { "isobject": { @@ -6521,8 +5425,8 @@ "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", "dev": true, "requires": { - "for-own": "0.1.5", - "is-extendable": "0.1.1" + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" } }, "object.pick": { @@ -6531,7 +5435,7 @@ "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", "dev": true, "requires": { - "isobject": "3.0.1" + "isobject": "^3.0.1" }, "dependencies": { "isobject": { @@ -6552,11 +5456,11 @@ } }, "once": { - "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { - "wrappy": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + "wrappy": "1" } }, "optimist": { @@ -6565,8 +5469,16 @@ "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", "dev": true, "requires": { - "minimist": "0.0.10", - "wordwrap": "0.0.3" + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + } } }, "optionator": { @@ -6575,12 +5487,12 @@ "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", "dev": true, "requires": { - "deep-is": "0.1.3", - "fast-levenshtein": "2.0.6", - "levn": "0.3.0", - "prelude-ls": "1.1.2", - "type-check": "0.3.2", - "wordwrap": "1.0.0" + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" }, "dependencies": { "wordwrap": { @@ -6626,27 +5538,19 @@ "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", "dev": true, "requires": { - "got": "6.7.1", - "registry-auth-token": "3.3.2", - "registry-url": "3.1.0", - "semver": "5.5.0" - }, - "dependencies": { - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true - } + "got": "^6.7.1", + "registry-auth-token": "^3.0.1", + "registry-url": "^3.0.3", + "semver": "^5.1.0" } }, "pad": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pad/-/pad-2.0.3.tgz", - "integrity": "sha512-YVlBmpDQilhUl69RY/0Ku9Il0sNPLgVOVePhCJUfN8qDZKrq1zIY+ZwtCLtaWFtJzuJswwAcZHxf4a5RliV2pQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pad/-/pad-2.1.0.tgz", + "integrity": "sha512-VeCrKYxgIoJ5RBpxogsoDiizLbZ2ZN+9ua+caIxe4e/a64kIzb9eVZ2CwWYd+arPbnM4N81lzlhTsHLtjSKJ0g==", "dev": true, "requires": { - "wcwidth": "1.0.1" + "wcwidth": "^1.0.1" } }, "pako": { @@ -6666,11 +5570,11 @@ "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", "dev": true, "requires": { - "asn1.js": "4.10.1", - "browserify-aes": "1.2.0", - "create-hash": "1.2.0", - "evp_bytestokey": "1.0.3", - "pbkdf2": "3.0.14" + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3" } }, "parse-glob": { @@ -6679,10 +5583,10 @@ "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", "dev": true, "requires": { - "glob-base": "0.3.0", - "is-dotfile": "1.0.3", - "is-extglob": "1.0.0", - "is-glob": "2.0.1" + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" } }, "parse-json": { @@ -6691,7 +5595,7 @@ "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", "dev": true, "requires": { - "error-ex": "1.3.1" + "error-ex": "^1.2.0" } }, "parsejson": { @@ -6700,7 +5604,7 @@ "integrity": "sha1-q343WfIJ7OmUN5c/fQ8fZK4OZKs=", "dev": true, "requires": { - "better-assert": "1.0.2" + "better-assert": "~1.0.0" } }, "parseqs": { @@ -6709,7 +5613,7 @@ "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", "dev": true, "requires": { - "better-assert": "1.0.2" + "better-assert": "~1.0.0" } }, "parseuri": { @@ -6718,7 +5622,7 @@ "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", "dev": true, "requires": { - "better-assert": "1.0.2" + "better-assert": "~1.0.0" } }, "parseurl": { @@ -6751,11 +5655,12 @@ "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", "dev": true, "requires": { - "pinkie-promise": "2.0.1" + "pinkie-promise": "^2.0.0" } }, "path-is-absolute": { - "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, @@ -6771,15 +5676,29 @@ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", "dev": true }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, "path-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", "dev": true, "requires": { - "graceful-fs": "4.1.11", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } } }, "pause-stream": { @@ -6788,20 +5707,20 @@ "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", "dev": true, "requires": { - "through": "2.3.8" + "through": "~2.3" } }, "pbkdf2": { - "version": "3.0.14", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.14.tgz", - "integrity": "sha512-gjsZW9O34fm0R7PaLHRJmLLVfSoesxztjPjE9o6R+qtVJij90ltg1joIovN9GKrRW3t1PzhDDG3UMEMFfZ+1wA==", + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz", + "integrity": "sha512-y4CXP3thSxqf7c0qmOF+9UeOTrifiVTIM+u7NWlq+PRsHbr7r7dpCmvzrZxa96JJUNi0Y5w9VqG5ZNeCVMoDcA==", "dev": true, "requires": { - "create-hash": "1.2.0", - "create-hmac": "1.1.7", - "ripemd160": "2.0.1", - "safe-buffer": "5.1.1", - "sha.js": "2.4.11" + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" } }, "pend": { @@ -6817,10 +5736,9 @@ "dev": true }, "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" }, "pinkie": { "version": "2.0.4", @@ -6834,7 +5752,7 @@ "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", "dev": true, "requires": { - "pinkie": "2.0.4" + "pinkie": "^2.0.0" } }, "plugin-error": { @@ -6843,11 +5761,11 @@ "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=", "dev": true, "requires": { - "ansi-cyan": "0.1.1", - "ansi-red": "0.1.1", - "arr-diff": "1.1.0", - "arr-union": "2.1.0", - "extend-shallow": "1.1.4" + "ansi-cyan": "^0.1.1", + "ansi-red": "^0.1.1", + "arr-diff": "^1.0.1", + "arr-union": "^2.0.1", + "extend-shallow": "^1.1.2" }, "dependencies": { "arr-diff": { @@ -6856,8 +5774,8 @@ "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo=", "dev": true, "requires": { - "arr-flatten": "1.1.0", - "array-slice": "0.2.3" + "arr-flatten": "^1.0.1", + "array-slice": "^0.2.3" } } } @@ -6879,28 +5797,21 @@ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-2.5.3.tgz", "integrity": "sha512-/rI36cN2g7vDQnKWN8Uzupi++KjyqS9iS+/fpwG4Ea8d0Pip0PQ5bshUNzVwt+/D2MRfhVAplYMMvWLqWrCF/g==", "requires": { - "detect-libc": "1.0.3", - "expand-template": "1.1.1", + "detect-libc": "^1.0.3", + "expand-template": "^1.0.2", "github-from-package": "0.0.0", - "minimist": "1.2.0", - "mkdirp": "0.5.1", - "node-abi": "2.4.3", - "noop-logger": "0.1.1", - "npmlog": "4.1.2", - "os-homedir": "1.0.2", - "pump": "2.0.1", - "rc": "1.2.6", - "simple-get": "2.8.1", - "tar-fs": "1.16.3", - "tunnel-agent": "0.6.0", - "which-pm-runs": "1.0.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "node-abi": "^2.2.0", + "noop-logger": "^0.1.1", + "npmlog": "^4.0.1", + "os-homedir": "^1.0.1", + "pump": "^2.0.1", + "rc": "^1.1.6", + "simple-get": "^2.7.0", + "tar-fs": "^1.13.0", + "tunnel-agent": "^0.6.0", + "which-pm-runs": "^1.0.0" } }, "prelude-ls": { @@ -6927,8 +5838,8 @@ "integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=", "dev": true, "requires": { - "get-stdin": "4.0.1", - "meow": "3.7.0" + "get-stdin": "^4.0.1", + "meow": "^3.1.0" } }, "process": { @@ -6948,59 +5859,8 @@ "integrity": "sha1-LNPP6jO6OonJwSHsM0er6asSX3c=", "dev": true, "requires": { - "speedometer": "0.1.4", - "through2": "0.2.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "through2": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz", - "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=", - "dev": true, - "requires": { - "readable-stream": "1.1.14", - "xtend": "2.1.2" - } - }, - "xtend": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", - "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", - "dev": true, - "requires": { - "object-keys": "0.4.0" - } - } + "speedometer": "~0.1.2", + "through2": "~0.2.3" } }, "ps-tree": { @@ -7009,7 +5869,7 @@ "integrity": "sha1-tCGyQUDWID8e08dplrRCewjowBQ=", "dev": true, "requires": { - "event-stream": "3.3.4" + "event-stream": "~3.3.0" } }, "pseudomap": { @@ -7024,7 +5884,7 @@ "integrity": "sha512-q5I5vLRMVtdWa8n/3UEzZX7Lfghzrg9eG2IKk2ENLSofKRCXVqMvMUHxCKgXNaqH/8ebhBxrqftHWnyTFweJ5Q==", "dev": true, "requires": { - "ps-tree": "1.1.0" + "ps-tree": "^1.1.0" } }, "public-encrypt": { @@ -7033,11 +5893,11 @@ "integrity": "sha512-4kJ5Esocg8X3h8YgJsKAuoesBgB7mqH3eowiDzMUPKiRDDE7E/BqqZD1hnTByIaAFiwAw246YEltSq7tdrOH0Q==", "dev": true, "requires": { - "bn.js": "4.11.8", - "browserify-rsa": "4.0.1", - "create-hash": "1.2.0", - "parse-asn1": "5.1.1", - "randombytes": "2.0.6" + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1" } }, "pump": { @@ -7045,23 +5905,8 @@ "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", "requires": { - "end-of-stream": "1.4.1", - "once": "1.4.0" - }, - "dependencies": { - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - } + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, "punycode": { @@ -7077,9 +5922,9 @@ "dev": true }, "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, "querystring": { @@ -7095,43 +5940,27 @@ "dev": true }, "randomatic": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", - "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz", + "integrity": "sha512-VdxFOIEY3mNO5PtSRkkle/hPJDHvQhK21oa73K4yAc9qmp6N429gAyF1gZMOTMeS0/AYzaV/2Trcef+NaIonSA==", "dev": true, "requires": { - "is-number": "3.0.0", - "kind-of": "4.0.0" + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" }, "dependencies": { "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - } - } + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true }, "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true } } }, @@ -7141,7 +5970,7 @@ "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", "dev": true, "requires": { - "safe-buffer": "5.1.1" + "safe-buffer": "^5.1.0" } }, "randomfill": { @@ -7150,8 +5979,8 @@ "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", "dev": true, "requires": { - "randombytes": "2.0.6", - "safe-buffer": "5.1.1" + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" } }, "range-parser": { @@ -7161,65 +5990,26 @@ "dev": true }, "raw-body": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", - "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", "dev": true, "requires": { "bytes": "3.0.0", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", "unpipe": "1.0.0" - }, - "dependencies": { - "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", - "dev": true - }, - "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", - "dev": true, - "requires": { - "depd": "1.1.1", - "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": "1.5.0" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", - "dev": true - } } }, "rc": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.6.tgz", - "integrity": "sha1-6xiYnG1PTxYsOZ953dKfODVWgJI=", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" } }, "read-pkg": { @@ -7228,9 +6018,9 @@ "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", "dev": true, "requires": { - "load-json-file": "1.1.0", - "normalize-package-data": "2.4.0", - "path-type": "1.1.0" + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" } }, "read-pkg-up": { @@ -7239,8 +6029,8 @@ "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", "dev": true, "requires": { - "find-up": "1.1.2", - "read-pkg": "1.1.0" + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" } }, "readable-stream": { @@ -7248,20 +6038,13 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.1", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - } + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, "readdirp": { @@ -7270,43 +6053,10 @@ "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", "dev": true, "requires": { - "graceful-fs": "4.1.11", - "minimatch": "3.0.4", - "readable-stream": "2.3.6", - "set-immediate-shim": "1.0.1" - }, - "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - } + "graceful-fs": "^4.1.2", + "minimatch": "^3.0.2", + "readable-stream": "^2.0.2", + "set-immediate-shim": "^1.0.1" } }, "redent": { @@ -7315,8 +6065,8 @@ "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", "dev": true, "requires": { - "indent-string": "2.1.0", - "strip-indent": "1.0.1" + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" } }, "regenerator-runtime": { @@ -7331,7 +6081,7 @@ "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", "dev": true, "requires": { - "is-equal-shallow": "0.1.3" + "is-equal-shallow": "^0.1.3" } }, "regex-not": { @@ -7340,8 +6090,8 @@ "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", "dev": true, "requires": { - "extend-shallow": "3.0.2", - "safe-regex": "1.1.0" + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" }, "dependencies": { "extend-shallow": { @@ -7350,8 +6100,8 @@ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", "dev": true, "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" } }, "is-extendable": { @@ -7360,7 +6110,7 @@ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dev": true, "requires": { - "is-plain-object": "2.0.4" + "is-plain-object": "^2.0.4" } } } @@ -7371,8 +6121,8 @@ "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", "dev": true, "requires": { - "rc": "1.2.6", - "safe-buffer": "5.1.1" + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" } }, "registry-url": { @@ -7381,7 +6131,7 @@ "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", "dev": true, "requires": { - "rc": "1.2.6" + "rc": "^1.0.1" } }, "remap-istanbul": { @@ -7390,50 +6140,49 @@ "integrity": "sha512-gsNQXs5kJLhErICSyYhzVZ++C8LBW8dgwr874Y2QvzAUS75zBlD/juZgXs39nbYJ09fZDlX2AVLVJAY2jbFJoQ==", "dev": true, "requires": { - "amdefine": "1.0.1", + "amdefine": "^1.0.0", "istanbul": "0.4.5", - "minimatch": "3.0.4", - "plugin-error": "0.1.2", - "source-map": "0.6.1", + "minimatch": "^3.0.3", + "plugin-error": "^0.1.2", + "source-map": "^0.6.1", "through2": "2.0.1" }, "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", "dev": true }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", "dev": true, "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" } }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "through2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.1.tgz", + "integrity": "sha1-OE51MU1J8y3hLuu4E2uOtrXVnak=", "dev": true, "requires": { - "brace-expansion": "1.1.11" + "readable-stream": "~2.0.0", + "xtend": "~4.0.0" } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true } } }, @@ -7461,7 +6210,7 @@ "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "dev": true, "requires": { - "is-finite": "1.0.2" + "is-finite": "^1.0.0" } }, "request": { @@ -7470,26 +6219,26 @@ "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", "dev": true, "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.7.0", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.2", - "har-validator": "5.0.3", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.18", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.6.0", - "uuid": "3.3.2" + "aws-sign2": "~0.7.0", + "aws4": "^1.6.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.1", + "forever-agent": "~0.6.1", + "form-data": "~2.3.1", + "har-validator": "~5.0.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.17", + "oauth-sign": "~0.8.2", + "performance-now": "^2.1.0", + "qs": "~6.5.1", + "safe-buffer": "^5.1.1", + "tough-cookie": "~2.3.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.1.0" } }, "requires-port": { @@ -7498,6 +6247,15 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, + "resolve": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", + "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "dev": true, + "requires": { + "path-parse": "^1.0.5" + } + }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -7517,7 +6275,7 @@ "dev": true, "optional": true, "requires": { - "align-text": "0.1.4" + "align-text": "^0.1.1" } }, "rimraf": { @@ -7526,34 +6284,17 @@ "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", "dev": true, "requires": { - "glob": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz" + "glob": "^7.0.5" } }, "ripemd160": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", - "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", "dev": true, "requires": { - "hash-base": "2.0.2", - "inherits": "2.0.3" - }, - "dependencies": { - "hash-base": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", - "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=", - "dev": true, - "requires": { - "inherits": "2.0.3" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "hash-base": "^3.0.0", + "inherits": "^2.0.1" } }, "rx": { @@ -7571,9 +6312,9 @@ } }, "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safe-regex": { "version": "1.1.0", @@ -7581,7 +6322,7 @@ "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "requires": { - "ret": "0.1.15" + "ret": "~0.1.10" } }, "safer-buffer": { @@ -7596,10 +6337,9 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "semver": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", - "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", - "dev": true + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" }, "semver-diff": { "version": "2.1.0", @@ -7607,15 +6347,7 @@ "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", "dev": true, "requires": { - "semver": "5.5.0" - }, - "dependencies": { - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true - } + "semver": "^5.0.3" } }, "set-blocking": { @@ -7635,10 +6367,10 @@ "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", "dev": true, "requires": { - "extend-shallow": "2.0.1", - "is-extendable": "0.1.1", - "is-plain-object": "2.0.4", - "split-string": "3.1.0" + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" }, "dependencies": { "extend-shallow": { @@ -7647,7 +6379,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } @@ -7670,16 +6402,8 @@ "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dev": true, "requires": { - "inherits": "2.0.3", - "safe-buffer": "5.1.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" } }, "shebang-command": { @@ -7688,7 +6412,7 @@ "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", "dev": true, "requires": { - "shebang-regex": "1.0.0" + "shebang-regex": "^1.0.0" } }, "shebang-regex": { @@ -7712,24 +6436,9 @@ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.1.tgz", "integrity": "sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==", "requires": { - "decompress-response": "3.3.0", - "once": "1.4.0", - "simple-concat": "1.0.0" - }, - "dependencies": { - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - } + "decompress-response": "^3.3.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" } }, "single-line-log": { @@ -7738,29 +6447,7 @@ "integrity": "sha1-wvg/Jzo+GhbtsJlWYdoO1e8DM2Q=", "dev": true, "requires": { - "string-width": "1.0.2" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } + "string-width": "^1.0.1" } }, "snapdragon": { @@ -7769,23 +6456,32 @@ "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", "dev": true, "requires": { - "base": "0.11.2", - "debug": "2.6.9", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "map-cache": "0.2.2", - "source-map": "0.5.7", - "source-map-resolve": "0.5.1", - "use": "3.1.0" + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" }, "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } }, "extend-shallow": { @@ -7794,7 +6490,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } }, "source-map": { @@ -7811,9 +6507,9 @@ "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", "dev": true, "requires": { - "define-property": "1.0.0", - "isobject": "3.0.1", - "snapdragon-util": "3.0.1" + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" }, "dependencies": { "define-property": { @@ -7822,7 +6518,7 @@ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dev": true, "requires": { - "is-descriptor": "1.0.2" + "is-descriptor": "^1.0.0" } }, "is-accessor-descriptor": { @@ -7831,7 +6527,7 @@ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-data-descriptor": { @@ -7840,7 +6536,7 @@ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-descriptor": { @@ -7849,9 +6545,9 @@ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dev": true, "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" } }, "isobject": { @@ -7874,7 +6570,7 @@ "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.2.0" } }, "socket.io": { @@ -7906,6 +6602,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", "dev": true + }, + "object-assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", + "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=", + "dev": true } } }, @@ -8014,25 +6716,21 @@ } }, "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": "1.0.1" - } + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "source-map-resolve": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.1.tgz", - "integrity": "sha512-0KW2wvzfxm8NCTb30z0LMNyPqWCdDGE2viwzUaucqJdkTRXtZiSY3I+2A6nVAjmdOy0I4gU8DwnVVGsk9jvP2A==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", "dev": true, "requires": { - "atob": "2.1.0", - "decode-uri-component": "0.2.0", - "resolve-url": "0.2.1", - "source-map-url": "0.4.0", - "urix": "0.1.0" + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" } }, "source-map-support": { @@ -8040,15 +6738,8 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz", "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==", "requires": { - "buffer-from": "1.1.0", - "source-map": "0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, "source-map-url": { @@ -8069,8 +6760,8 @@ "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", "dev": true, "requires": { - "spdx-expression-parse": "3.0.0", - "spdx-license-ids": "3.0.0" + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" } }, "spdx-exceptions": { @@ -8085,8 +6776,8 @@ "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", "dev": true, "requires": { - "spdx-exceptions": "2.1.0", - "spdx-license-ids": "3.0.0" + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" } }, "spdx-license-ids": { @@ -8107,7 +6798,7 @@ "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", "dev": true, "requires": { - "through": "2.3.8" + "through": "2" } }, "split-string": { @@ -8116,7 +6807,7 @@ "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", "dev": true, "requires": { - "extend-shallow": "3.0.2" + "extend-shallow": "^3.0.0" }, "dependencies": { "extend-shallow": { @@ -8125,8 +6816,8 @@ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", "dev": true, "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" } }, "is-extendable": { @@ -8135,7 +6826,7 @@ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dev": true, "requires": { - "is-plain-object": "2.0.4" + "is-plain-object": "^2.0.4" } } } @@ -8151,15 +6842,15 @@ "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", "dev": true, "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.2", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "safer-buffer": "2.1.2", - "tweetnacl": "0.14.5" + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" } }, "stackframe": { @@ -8174,8 +6865,8 @@ "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", "dev": true, "requires": { - "define-property": "0.2.5", - "object-copy": "0.1.0" + "define-property": "^0.2.5", + "object-copy": "^0.1.0" }, "dependencies": { "define-property": { @@ -8184,7 +6875,7 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } } } @@ -8200,7 +6891,7 @@ "resolved": "https://registry.npmjs.org/steno/-/steno-0.4.4.tgz", "integrity": "sha1-BxEFvfwobmYVwEA8J+nXtdy4Vcs=", "requires": { - "graceful-fs": "4.1.11" + "graceful-fs": "^4.1.3" } }, "stream-browserify": { @@ -8209,16 +6900,8 @@ "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", "dev": true, "requires": { - "inherits": "2.0.3", - "readable-stream": "2.3.6" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" } }, "stream-combiner": { @@ -8227,28 +6910,20 @@ "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", "dev": true, "requires": { - "duplexer": "0.1.1" + "duplexer": "~0.1.1" } }, "stream-http": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.1.tgz", - "integrity": "sha512-cQ0jo17BLca2r0GfRdZKYAGLU6JRoIWxqSOakUMuKOT6MOK7AAlE856L33QuDmAy/eeOrhLee3dZKX0Uadu93A==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", "dev": true, "requires": { - "builtin-status-codes": "3.0.0", - "inherits": "2.0.3", - "readable-stream": "2.3.6", - "to-arraybuffer": "1.0.1", - "xtend": "4.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" } }, "streamroller": { @@ -8257,10 +6932,10 @@ "integrity": "sha1-1DW9WXQ3Or2b2QaDWVEwhRBswF8=", "dev": true, "requires": { - "date-format": "0.0.0", - "debug": "0.7.4", - "mkdirp": "0.5.1", - "readable-stream": "1.1.14" + "date-format": "^0.0.0", + "debug": "^0.7.2", + "mkdirp": "^0.5.1", + "readable-stream": "^1.1.7" }, "dependencies": { "debug": { @@ -8269,12 +6944,6 @@ "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=", "dev": true }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -8287,10 +6956,10 @@ "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", "isarray": "0.0.1", - "string_decoder": "0.10.31" + "string_decoder": "~0.10.x" } }, "string_decoder": { @@ -8302,27 +6971,13 @@ } }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "3.0.0" - } - } + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" } }, "string_decoder": { @@ -8330,7 +6985,7 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { - "safe-buffer": "5.1.1" + "safe-buffer": "~5.1.0" } }, "strip-ansi": { @@ -8338,7 +6993,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "strip-bom": { @@ -8347,7 +7002,7 @@ "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", "dev": true, "requires": { - "is-utf8": "0.2.1" + "is-utf8": "^0.2.0" } }, "strip-eof": { @@ -8362,7 +7017,7 @@ "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", "dev": true, "requires": { - "get-stdin": "4.0.1" + "get-stdin": "^4.0.1" } }, "strip-json-comments": { @@ -8376,15 +7031,29 @@ "integrity": "sha1-ebs7RFbdBPGOvbwNcDodHa7FEF0=", "dev": true, "requires": { - "debug": "2.6.9", - "es6-promise": "4.2.4" + "debug": "^2.2.0", + "es6-promise": "^4.0.5" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } } }, "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } }, "symbol-observable": { "version": "1.0.1", @@ -8396,33 +7065,20 @@ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", "integrity": "sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==", "requires": { - "chownr": "1.0.1", - "mkdirp": "0.5.1", - "pump": "1.0.3", - "tar-stream": "1.6.1" + "chownr": "^1.0.1", + "mkdirp": "^0.5.1", + "pump": "^1.0.0", + "tar-stream": "^1.1.2" }, "dependencies": { - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1.0.2" - } - }, "pump": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", "requires": { - "end-of-stream": "1.4.1", - "once": "1.4.0" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" } } }, @@ -8431,13 +7087,13 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.1.tgz", "integrity": "sha512-IFLM5wp3QrJODQFPm6/to3LJZrONdBY/otxcvDIQzu217zKye6yVR3hhi9lAjrC2Z+m/j5oDxMPb1qcd8cIvpA==", "requires": { - "bl": "1.2.2", - "buffer-alloc": "1.2.0", - "end-of-stream": "1.4.1", - "fs-constants": "1.0.0", - "readable-stream": "2.3.6", - "to-buffer": "1.1.1", - "xtend": "4.0.1" + "bl": "^1.0.0", + "buffer-alloc": "^1.1.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.0", + "xtend": "^4.0.0" } }, "term-size": { @@ -8446,7 +7102,7 @@ "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", "dev": true, "requires": { - "execa": "0.7.0" + "execa": "^0.7.0" } }, "throttleit": { @@ -8462,39 +7118,31 @@ "dev": true }, "through2": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.1.tgz", - "integrity": "sha1-OE51MU1J8y3hLuu4E2uOtrXVnak=", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz", + "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=", "dev": true, "requires": { - "readable-stream": "2.0.6", - "xtend": "4.0.1" + "readable-stream": "~1.1.9", + "xtend": "~2.1.1" }, "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", "dev": true }, "readable-stream": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "0.10.31", - "util-deprecate": "1.0.2" + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" } }, "string_decoder": { @@ -8502,6 +7150,15 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true + }, + "xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", + "dev": true, + "requires": { + "object-keys": "~0.4.0" + } } } }, @@ -8512,12 +7169,12 @@ "dev": true }, "timers-browserify": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.8.tgz", - "integrity": "sha512-a+QofyerK/mw3zl0nOlU33TP0vnbaF8pDKevVI5hT5LMUFEUQPtbhSohuvRZWB8UW1bP9Bhfqqw6KA3gsbP4Uw==", + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz", + "integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==", "dev": true, "requires": { - "setimmediate": "1.0.5" + "setimmediate": "^1.0.4" } }, "tmp": { @@ -8526,7 +7183,7 @@ "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=", "dev": true, "requires": { - "os-tmpdir": "1.0.2" + "os-tmpdir": "~1.0.1" } }, "to-array": { @@ -8558,7 +7215,7 @@ "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" } }, "to-regex": { @@ -8567,10 +7224,10 @@ "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", "dev": true, "requires": { - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "regex-not": "1.0.2", - "safe-regex": "1.1.0" + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" }, "dependencies": { "extend-shallow": { @@ -8579,8 +7236,8 @@ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", "dev": true, "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" } }, "is-extendable": { @@ -8589,7 +7246,7 @@ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dev": true, "requires": { - "is-plain-object": "2.0.4" + "is-plain-object": "^2.0.4" } } } @@ -8600,8 +7257,8 @@ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", "dev": true, "requires": { - "is-number": "3.0.0", - "repeat-string": "1.6.1" + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" }, "dependencies": { "is-number": { @@ -8610,7 +7267,7 @@ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" } } } @@ -8621,7 +7278,7 @@ "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", "dev": true, "requires": { - "nopt": "1.0.10" + "nopt": "~1.0.10" }, "dependencies": { "nopt": { @@ -8630,7 +7287,7 @@ "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", "dev": true, "requires": { - "abbrev": "1.0.9" + "abbrev": "1" } } } @@ -8641,7 +7298,7 @@ "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", "dev": true, "requires": { - "punycode": "1.4.1" + "punycode": "^1.4.1" } }, "tree-kill": { @@ -8663,306 +7320,82 @@ "dev": true }, "tslib": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", - "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==" + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" }, "tslint": { - "version": "5.9.1", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.9.1.tgz", - "integrity": "sha1-ElX4ej/1frCw4fDmEKi0dIBGya4=", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.11.0.tgz", + "integrity": "sha1-mPMMAurjzecAYgHkwzywi0hYHu0=", "dev": true, "requires": { - "babel-code-frame": "6.26.0", - "builtin-modules": "1.1.1", - "chalk": "2.3.1", - "commander": "2.14.1", - "diff": "3.4.0", - "glob": "7.1.2", - "js-yaml": "3.10.0", - "minimatch": "3.0.4", - "resolve": "1.5.0", - "semver": "5.5.0", - "tslib": "1.9.0", - "tsutils": "2.21.1" + "babel-code-frame": "^6.22.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^3.2.0", + "glob": "^7.1.1", + "js-yaml": "^3.7.0", + "minimatch": "^3.0.4", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.27.2" }, "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { - "chalk": "1.1.3", - "esutils": "2.0.2", - "js-tokens": "3.0.2" - }, - "dependencies": { - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - } - } + "color-convert": "^1.9.0" } }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, "chalk": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", - "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", "dev": true, "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "5.2.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.1" - } - }, - "supports-color": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", - "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", - "dev": true, - "requires": { - "has-flag": "3.0.0" - } - } + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" } }, - "color-convert": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", - "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, "commander": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", - "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==", + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", "dev": true }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "diff": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.4.0.tgz", - "integrity": "sha512-QpVuMTEoJMF7cKzi6bvWhRulU1fZqZnvyVQgNhPaxxuTYwyjn/j1v9falseQ/uXWwPnO56RBfwtg4h/EQXmucA==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", - "dev": true - }, - "resolve": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", - "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", - "dev": true, - "requires": { - "path-parse": "1.0.5" - } - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "tslib": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", - "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==", - "dev": true - }, - "tsutils": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.21.1.tgz", - "integrity": "sha512-heMkdeQ9iUc90ynfiNo5Y+GXrEEGy86KMvnSTfHO+Q40AuNQ1lZGXcv58fuU9XTUxI0V7YIN9xPN+CO9b1Gn3w==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "dev": true, "requires": { - "tslib": "1.9.0" + "has-flag": "^3.0.0" } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true } } }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", @@ -8974,7 +7407,7 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "requires": { - "safe-buffer": "5.1.1" + "safe-buffer": "^5.0.1" } }, "tweetnacl": { @@ -8990,7 +7423,7 @@ "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", "dev": true, "requires": { - "prelude-ls": "1.1.2" + "prelude-ls": "~1.1.2" } }, "type-is": { @@ -9000,7 +7433,7 @@ "dev": true, "requires": { "media-typer": "0.3.0", - "mime-types": "2.1.18" + "mime-types": "~2.1.18" } }, "typedarray": { @@ -9015,15 +7448,15 @@ "integrity": "sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw==", "dev": true, "requires": { - "circular-json": "0.3.3", - "lodash": "4.17.4", - "postinstall-build": "5.0.1" + "circular-json": "^0.3.1", + "lodash": "^4.17.4", + "postinstall-build": "^5.0.1" } }, "typescript": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.1.tgz", - "integrity": "sha512-bqB1yS6o9TNA9ZC/MJxM0FZzPnZdtHj0xWK/IZ5khzVqdpGul/R/EIiHRgFXlwTD7PSIaYVnGKq1QgMCu2mnqw==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", + "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", "dev": true }, "uglify-js": { @@ -9033,9 +7466,9 @@ "dev": true, "optional": true, "requires": { - "source-map": "0.5.7", - "uglify-to-browserify": "1.0.2", - "yargs": "3.10.0" + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" }, "dependencies": { "source-map": { @@ -9066,7 +7499,18 @@ "integrity": "sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY=", "dev": true, "requires": { - "debug": "2.6.9" + "debug": "^2.2.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } } }, "union-value": { @@ -9075,10 +7519,10 @@ "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", "dev": true, "requires": { - "arr-union": "3.1.0", - "get-value": "2.0.6", - "is-extendable": "0.1.1", - "set-value": "0.4.3" + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" }, "dependencies": { "arr-union": { @@ -9093,7 +7537,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } }, "set-value": { @@ -9102,10 +7546,10 @@ "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", "dev": true, "requires": { - "extend-shallow": "2.0.1", - "is-extendable": "0.1.1", - "is-plain-object": "2.0.4", - "to-object-path": "0.3.0" + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" } } } @@ -9116,7 +7560,7 @@ "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", "dev": true, "requires": { - "crypto-random-string": "1.0.0" + "crypto-random-string": "^1.0.0" } }, "universalify": { @@ -9136,8 +7580,8 @@ "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", "dev": true, "requires": { - "has-value": "0.3.1", - "isobject": "3.0.1" + "has-value": "^0.3.1", + "isobject": "^3.0.0" }, "dependencies": { "has-value": { @@ -9146,9 +7590,9 @@ "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", "dev": true, "requires": { - "get-value": "2.0.6", - "has-values": "0.1.4", - "isobject": "2.1.0" + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" }, "dependencies": { "isobject": { @@ -9183,9 +7627,9 @@ "dev": true }, "upath": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.0.4.tgz", - "integrity": "sha512-d4SJySNBXDaQp+DPrziv3xGS6w3d2Xt69FijJr86zMPBy23JEloMCEOUBBzuN7xCtjLCnmB9tI/z7SBCahHBOw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", + "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", "dev": true }, "update-notifier": { @@ -9194,16 +7638,16 @@ "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", "dev": true, "requires": { - "boxen": "1.3.0", - "chalk": "2.4.0", - "configstore": "3.1.2", - "import-lazy": "2.1.0", - "is-ci": "1.1.0", - "is-installed-globally": "0.1.0", - "is-npm": "1.0.0", - "latest-version": "3.1.0", - "semver-diff": "2.1.0", - "xdg-basedir": "3.0.0" + "boxen": "^1.2.1", + "chalk": "^2.0.1", + "configstore": "^3.0.0", + "import-lazy": "^2.1.0", + "is-ci": "^1.0.10", + "is-installed-globally": "^0.1.0", + "is-npm": "^1.0.0", + "latest-version": "^3.0.0", + "semver-diff": "^2.0.0", + "xdg-basedir": "^3.0.0" }, "dependencies": { "ansi-styles": { @@ -9212,18 +7656,18 @@ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { - "color-convert": "1.9.1" + "color-convert": "^1.9.0" } }, "chalk": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.0.tgz", - "integrity": "sha512-Wr/w0f4o9LuE7K53cD0qmbAMM+2XNLzR29vFn5hqko4sxGlUsyy363NvmyGIyk5tpe9cjTr9SJYbysEyPkRnFw==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", "dev": true, "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.4.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" } }, "has-flag": { @@ -9238,7 +7682,7 @@ "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "dev": true, "requires": { - "has-flag": "3.0.0" + "has-flag": "^3.0.0" } } } @@ -9273,25 +7717,14 @@ "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", "dev": true, "requires": { - "prepend-http": "1.0.4" + "prepend-http": "^1.0.1" } }, "use": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", - "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", - "dev": true, - "requires": { - "kind-of": "6.0.2" - }, - "dependencies": { - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true }, "useragent": { "version": "2.3.0", @@ -9299,25 +7732,17 @@ "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==", "dev": true, "requires": { - "lru-cache": "4.1.2", - "tmp": "0.0.31" + "lru-cache": "4.1.x", + "tmp": "0.0.x" } }, "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", "dev": true, "requires": { - "inherits": "2.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", - "dev": true - } + "inherits": "2.0.3" } }, "util-deprecate": { @@ -9338,13 +7763,13 @@ "dev": true }, "validate-npm-package-license": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", - "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, "requires": { - "spdx-correct": "3.0.0", - "spdx-expression-parse": "3.0.0" + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" } }, "verror": { @@ -9353,9 +7778,9 @@ "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "dev": true, "requires": { - "assert-plus": "1.0.0", + "assert-plus": "^1.0.0", "core-util-is": "1.0.2", - "extsprintf": "1.3.0" + "extsprintf": "^1.2.0" } }, "vm-browserify": { @@ -9379,16 +7804,16 @@ "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", "dev": true, "requires": { - "defaults": "1.0.3" + "defaults": "^1.0.3" } }, "which": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", - "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, "requires": { - "isexe": "2.0.0" + "isexe": "^2.0.0" } }, "which-pm-runs": { @@ -9401,7 +7826,7 @@ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "requires": { - "string-width": "2.1.1" + "string-width": "^1.0.2 || 2" } }, "widest-line": { @@ -9410,7 +7835,40 @@ "integrity": "sha1-AUKk6KJD+IgsAjOqDgKBqnYVInM=", "dev": true, "requires": { - "string-width": "2.1.1" + "string-width": "^2.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } } }, "window-size": { @@ -9427,9 +7885,9 @@ "dev": true }, "wrappy": { - "version": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write-file-atomic": { "version": "2.3.0", @@ -9437,9 +7895,9 @@ "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", "dev": true, "requires": { - "graceful-fs": "4.1.11", - "imurmurhash": "0.1.4", - "signal-exit": "3.0.2" + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" } }, "ws": { @@ -9448,8 +7906,8 @@ "integrity": "sha1-iiRPoFJAHgjJiGz0SoUYnh/UBn8=", "dev": true, "requires": { - "options": "0.0.6", - "ultron": "1.0.2" + "options": ">=0.0.5", + "ultron": "1.0.x" } }, "wtf-8": { @@ -9488,10 +7946,19 @@ "dev": true, "optional": true, "requires": { - "camelcase": "1.2.1", - "cliui": "2.1.0", - "decamelize": "1.2.0", + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", "window-size": "0.1.0" + }, + "dependencies": { + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true, + "optional": true + } } }, "yauzl": { @@ -9500,7 +7967,7 @@ "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", "dev": true, "requires": { - "fd-slicer": "1.0.1" + "fd-slicer": "~1.0.1" } }, "yeast": { From b724448081a0962d3c3894319502ed380fe40145 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 13 Aug 2018 09:42:52 -0400 Subject: [PATCH 0479/1626] search service implementation with lunr --- package-lock.json | 12 +- package.json | 4 +- src/abstractions/search.service.ts | 3 +- src/angular/components/ciphers.component.ts | 24 +-- src/models/view/cipherView.ts | 4 - src/services/cipher.service.ts | 20 ++- src/services/search.service.ts | 160 ++++++++++++++++---- 7 files changed, 169 insertions(+), 58 deletions(-) diff --git a/package-lock.json b/package-lock.json index 90c0ac7057..c63ec41307 100644 --- a/package-lock.json +++ b/package-lock.json @@ -115,9 +115,9 @@ } }, "@types/lunr": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@types/lunr/-/lunr-2.1.5.tgz", - "integrity": "sha512-esk3CG25hRtHsVHm+LOjiSFYdw8be3uIY653WUwR43Bro914HSimPgPpqgajkhTJ0awK3RQfaIxP7zvbtCpcyg==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@types/lunr/-/lunr-2.1.6.tgz", + "integrity": "sha512-Bz6fUhX1llTa7ygQJN3ttoVkkrpW7xxSEP7D7OYFO/FCBKqKqruRUZtJzTtYA0GkQX13lxU5u+8LuCviJlAXkQ==", "dev": true }, "@types/node": { @@ -4570,9 +4570,9 @@ } }, "lunr": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.1.6.tgz", - "integrity": "sha512-ydJpB8CX8cZ/VE+KMaYaFcZ6+o2LruM6NG76VXdflYTgluvVemz1lW4anE+pyBbLvxJHZdvD1Jy/fOqdzAEJog==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.1.tgz", + "integrity": "sha1-ETYWorYC3cEJMqe/ik5uV+v+zfI=" }, "make-dir": { "version": "1.3.0", diff --git a/package.json b/package.json index c34ba06efc..df787ee65e 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@types/form-data": "^2.2.1", "@types/jasmine": "^2.8.2", "@types/lowdb": "^1.0.1", - "@types/lunr": "2.1.5", + "@types/lunr": "^2.1.6", "@types/node": "8.0.19", "@types/node-fetch": "^1.6.9", "@types/node-forge": "0.7.1", @@ -75,7 +75,7 @@ "form-data": "2.3.2", "keytar": "4.2.1", "lowdb": "1.0.0", - "lunr": "2.1.6", + "lunr": "2.3.1", "node-fetch": "2.1.2", "node-forge": "0.7.1", "papaparse": "4.3.5", diff --git a/src/abstractions/search.service.ts b/src/abstractions/search.service.ts index c5983e2427..1a4aa75e4b 100644 --- a/src/abstractions/search.service.ts +++ b/src/abstractions/search.service.ts @@ -1,6 +1,7 @@ import { CipherView } from '../models/view/cipherView'; export abstract class SearchService { + clearIndex: () => void; indexCiphers: () => Promise; - searchCiphers: (query: string) => Promise; + searchCiphers: (query: string, filter?: (cipher: CipherView) => boolean) => Promise; } diff --git a/src/angular/components/ciphers.component.ts b/src/angular/components/ciphers.component.ts index fea70ce280..4ce7f85b6e 100644 --- a/src/angular/components/ciphers.component.ts +++ b/src/angular/components/ciphers.component.ts @@ -4,7 +4,7 @@ import { Output, } from '@angular/core'; -import { CipherService } from '../../abstractions/cipher.service'; +import { SearchService } from '../../abstractions/search.service'; import { CipherView } from '../../models/view/cipherView'; @@ -23,11 +23,12 @@ export class CiphersComponent { protected allCiphers: CipherView[] = []; protected filter: (cipher: CipherView) => boolean = null; - constructor(protected cipherService: CipherService) { } + private searchTimeout: any = null; + + constructor(protected searchService: SearchService) { } async load(filter: (cipher: CipherView) => boolean = null) { - this.allCiphers = await this.cipherService.getAllDecrypted(); - this.applyFilter(filter); + await this.applyFilter(filter); this.loaded = true; } @@ -37,13 +38,18 @@ export class CiphersComponent { await this.load(this.filter); } - applyFilter(filter: (cipher: CipherView) => boolean = null) { + async applyFilter(filter: (cipher: CipherView) => boolean = null) { this.filter = filter; - if (this.filter == null) { - this.ciphers = this.allCiphers; - } else { - this.ciphers = this.allCiphers.filter(this.filter); + await this.search(0); + } + + search(timeout: number = 0) { + if (this.searchTimeout != null) { + clearTimeout(this.searchTimeout); } + this.searchTimeout = setTimeout(async () => { + this.ciphers = await this.searchService.searchCiphers(this.searchText, this.filter); + }, timeout); } selectCipher(cipher: CipherView) { diff --git a/src/models/view/cipherView.ts b/src/models/view/cipherView.ts index c9ef130048..5c69728c1c 100644 --- a/src/models/view/cipherView.ts +++ b/src/models/view/cipherView.ts @@ -78,10 +78,6 @@ export class CipherView implements View { return this.fields && this.fields.length > 0; } - get login_username(): string { - return this.login != null ? this.login.username : null; - } - get passwordRevisionDisplayDate(): Date { if (this.login == null) { return null; diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 6fb6c3ab5e..73f4ba0d7f 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -38,6 +38,7 @@ import { CipherService as CipherServiceAbstraction } from '../abstractions/ciphe import { CryptoService } from '../abstractions/crypto.service'; import { I18nService } from '../abstractions/i18n.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; +import { SearchService } from '../abstractions/search.service'; import { SettingsService } from '../abstractions/settings.service'; import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; @@ -51,12 +52,25 @@ const Keys = { }; export class CipherService implements CipherServiceAbstraction { - decryptedCipherCache: CipherView[]; + // tslint:disable-next-line + _decryptedCipherCache: CipherView[]; constructor(private cryptoService: CryptoService, private userService: UserService, private settingsService: SettingsService, private apiService: ApiService, private storageService: StorageService, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService) { + private platformUtilsService: PlatformUtilsService, private searchService: () => SearchService) { + } + + get decryptedCipherCache() { + return this._decryptedCipherCache; + } + set decryptedCipherCache(value: CipherView[]) { + this._decryptedCipherCache = value; + if (value == null) { + this.searchService().clearIndex(); + } else { + this.searchService().indexCiphers(); + } } clearCache(): void { @@ -591,7 +605,7 @@ export class CipherService implements CipherServiceAbstraction { async clear(userId: string): Promise { await this.storageService.remove(Keys.ciphersPrefix + userId); - this.decryptedCipherCache = null; + this.clearCache(); } async moveManyWithServer(ids: string[], folderId: string): Promise { diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 1307d0db74..8aa5562807 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -3,58 +3,152 @@ import * as lunr from 'lunr'; import { CipherView } from '../models/view/cipherView'; import { CipherService } from '../abstractions/cipher.service'; +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { SearchService as SearchServiceAbstraction } from '../abstractions/search.service'; -export class SearchService implements SearchServiceAbstraction { - private index: lunr.Index; +import { DeviceType } from '../enums/deviceType'; +import { FieldType } from '../enums/fieldType'; - constructor(private cipherService: CipherService) { +export class SearchService implements SearchServiceAbstraction { + private indexing = false; + private index: lunr.Index = null; + private onlySearchName = false; + + constructor(private cipherService: CipherService, platformUtilsService: PlatformUtilsService) { + this.onlySearchName = platformUtilsService.getDevice() === DeviceType.EdgeExtension; + } + + clearIndex(): void { + this.index = null; } async indexCiphers(): Promise { + if (this.indexing) { + return; + } + // tslint:disable-next-line + console.time('search indexing'); + this.indexing = true; + this.index = null; const builder = new lunr.Builder(); builder.ref('id'); - builder.field('name'); - builder.field('subTitle'); + (builder as any).field('shortId', { boost: 100, extractor: (c: CipherView) => c.id.substr(0, 8) }); + (builder as any).field('name', { boost: 10 }); + (builder as any).field('subTitle', { boost: 5 }); builder.field('notes'); - builder.field('login_username'); - builder.field('login_uri'); - - const ciphers = await this.cipherService.getAllDecrypted(); - ciphers.forEach((c) => { - builder.add(c); + (builder as any).field('login.username', { + extractor: (c: CipherView) => c.login != null ? c.login.username : null, }); - + (builder as any).field('login.uris', { + boost: 2, + extractor: (c: CipherView) => c.login == null || !c.login.hasUris ? null : + c.login.uris.filter((u) => u.hostname != null).map((u) => u.hostname), + }); + (builder as any).field('fields', { + extractor: (c: CipherView) => { + if (!c.hasFields) { + return null; + } + const fields = c.fields.filter((f) => f.type === FieldType.Text).map((f) => { + let field = ''; + if (f.name != null) { + field += f.name; + } + if (f.value != null) { + if (field !== '') { + field += ' '; + } + field += f.value; + } + return field; + }); + return fields.filter((f) => f.trim() !== ''); + }, + }); + (builder as any).field('attachments', { + extractor: (c: CipherView) => !c.hasAttachments ? null : c.attachments.map((a) => a.fileName), + }); + const ciphers = await this.cipherService.getAllDecrypted(); + ciphers.forEach((c) => builder.add(c)); this.index = builder.build(); + this.indexing = false; + // tslint:disable-next-line + console.timeEnd('search indexing'); } - async searchCiphers(query: string): Promise { + async searchCiphers(query: string, filter: (cipher: CipherView) => boolean = null): + Promise { const results: CipherView[] = []; - if (this.index == null) { - return results; + if (query != null) { + query = query.trim().toLowerCase(); + } + if (query === '') { + query = null; + } + + let ciphers = await this.cipherService.getAllDecrypted(); + if (filter != null) { + ciphers = ciphers.filter(filter); + } + + if (query == null || (this.index == null && query.length < 2)) { + return ciphers; + } + + if (this.index == null) { + // Fall back to basic search if index is not available + return ciphers.filter((c) => { + if (c.name != null && c.name.toLowerCase().indexOf(query) > -1) { + return true; + } + if (this.onlySearchName) { + return false; + } + if (query.length >= 8 && c.id.startsWith(query)) { + return true; + } + if (c.subTitle != null && c.subTitle.toLowerCase().indexOf(query) > -1) { + return true; + } + if (c.login && c.login.uri != null && c.login.uri.toLowerCase().indexOf(query) > -1) { + return true; + } + return false; + }); } - const ciphers = await this.cipherService.getAllDecrypted(); const ciphersMap = new Map(); - ciphers.forEach((c) => { - ciphersMap.set(c.id, c); - }); + ciphers.forEach((c) => ciphersMap.set(c.id, c)); - query = this.transformQuery(query); - const searchResults = this.index.search(query); - searchResults.forEach((r) => { - if (ciphersMap.has(r.ref)) { - results.push(ciphersMap.get(r.ref)); - } - }); + let searchResults: lunr.Index.Result[] = null; + const isQueryString = query != null && query.length > 1 && query.indexOf('>') === 0; + if (isQueryString) { + try { + searchResults = this.index.search(query.substr(1)); + } catch { } + } else { + // tslint:disable-next-line + const soWild = lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING; + searchResults = this.index.query((q) => { + q.term(query, { fields: ['name'], wildcard: soWild }); + q.term(query, { fields: ['subTitle'], wildcard: soWild }); + q.term(query, { fields: ['login.uris'], wildcard: soWild }); + lunr.tokenizer(query).forEach((token) => { + q.term(token.toString(), {}); + }); + }); + } + if (searchResults != null) { + searchResults.forEach((r) => { + if (ciphersMap.has(r.ref)) { + results.push(ciphersMap.get(r.ref)); + } + }); + } + if (results != null) { + results.sort(this.cipherService.getLocaleSortingFunction()); + } return results; } - - private transformQuery(query: string) { - if (query.indexOf('>') === 0) { - return query.substr(1); - } - return '*' + query + '*'; - } } From d917651d9f844dc07abf3e817073aa1e10213b9a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 13 Aug 2018 11:52:55 -0400 Subject: [PATCH 0480/1626] search pending and is searchable --- src/abstractions/search.service.ts | 1 + src/angular/components/ciphers.component.ts | 12 ++++++++++-- src/services/search.service.ts | 8 +++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/abstractions/search.service.ts b/src/abstractions/search.service.ts index 1a4aa75e4b..4050a27230 100644 --- a/src/abstractions/search.service.ts +++ b/src/abstractions/search.service.ts @@ -2,6 +2,7 @@ import { CipherView } from '../models/view/cipherView'; export abstract class SearchService { clearIndex: () => void; + isSearchable: (query: string) => boolean; indexCiphers: () => Promise; searchCiphers: (query: string, filter?: (cipher: CipherView) => boolean) => Promise; } diff --git a/src/angular/components/ciphers.component.ts b/src/angular/components/ciphers.component.ts index 4ce7f85b6e..916c0adce6 100644 --- a/src/angular/components/ciphers.component.ts +++ b/src/angular/components/ciphers.component.ts @@ -22,6 +22,7 @@ export class CiphersComponent { protected allCiphers: CipherView[] = []; protected filter: (cipher: CipherView) => boolean = null; + protected searchPending = false; private searchTimeout: any = null; @@ -40,15 +41,22 @@ export class CiphersComponent { async applyFilter(filter: (cipher: CipherView) => boolean = null) { this.filter = filter; - await this.search(0); + await this.search(null); } - search(timeout: number = 0) { + async search(timeout: number = null) { + this.searchPending = false; if (this.searchTimeout != null) { clearTimeout(this.searchTimeout); } + if (timeout == null) { + this.ciphers = await this.searchService.searchCiphers(this.searchText, this.filter); + return; + } + this.searchPending = true; this.searchTimeout = setTimeout(async () => { this.ciphers = await this.searchService.searchCiphers(this.searchText, this.filter); + this.searchPending = false; }, timeout); } diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 8aa5562807..1cb1afda13 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -22,6 +22,12 @@ export class SearchService implements SearchServiceAbstraction { this.index = null; } + isSearchable(query: string): boolean { + const notSearchable = query == null || (this.index == null && query.length < 2) || + (this.index != null && query.length < 2 && query.indexOf('>') !== 0); + return !notSearchable; + } + async indexCiphers(): Promise { if (this.indexing) { return; @@ -91,7 +97,7 @@ export class SearchService implements SearchServiceAbstraction { ciphers = ciphers.filter(filter); } - if (query == null || (this.index == null && query.length < 2)) { + if (!this.isSearchable(query)) { return ciphers; } From 74c870683a5dd8b30d5ea80796d894f2a29db30d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 13 Aug 2018 14:09:03 -0400 Subject: [PATCH 0481/1626] sequentualize cipher service getAllDecrypted --- src/services/cipher.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 73f4ba0d7f..b5f1c046e9 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -43,6 +43,7 @@ import { SettingsService } from '../abstractions/settings.service'; import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; +import { sequentialize } from '../misc/sequentialize'; import { Utils } from '../misc/utils'; const Keys = { @@ -259,6 +260,7 @@ export class CipherService implements CipherServiceAbstraction { return response; } + @sequentialize(() => 'getAllDecrypted') async getAllDecrypted(): Promise { if (this.decryptedCipherCache != null) { return this.decryptedCipherCache; From 364192b27a54d76bd95929ee88a954548c0229c9 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 13 Aug 2018 14:09:10 -0400 Subject: [PATCH 0482/1626] clear search index on lock --- src/services/lock.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts index 4d00022741..194c45d130 100644 --- a/src/services/lock.service.ts +++ b/src/services/lock.service.ts @@ -7,6 +7,7 @@ import { FolderService } from '../abstractions/folder.service'; import { LockService as LockServiceAbstraction } from '../abstractions/lock.service'; import { MessagingService } from '../abstractions/messaging.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; +import { SearchService } from '../abstractions/search.service'; import { StorageService } from '../abstractions/storage.service'; export class LockService implements LockServiceAbstraction { @@ -15,7 +16,8 @@ export class LockService implements LockServiceAbstraction { constructor(private cipherService: CipherService, private folderService: FolderService, private collectionService: CollectionService, private cryptoService: CryptoService, private platformUtilsService: PlatformUtilsService, private storageService: StorageService, - private messagingService: MessagingService, private lockedCallback: () => Promise) { + private messagingService: MessagingService, private searchService: SearchService, + private lockedCallback: () => Promise) { } init(checkOnInterval: boolean) { @@ -74,6 +76,7 @@ export class LockService implements LockServiceAbstraction { this.folderService.clearCache(); this.cipherService.clearCache(); this.collectionService.clearCache(); + this.searchService.clearIndex(); this.messagingService.send('locked'); if (this.lockedCallback != null) { await this.lockedCallback(); From bdb2efd77054d28a686dcf50a849ffbf62a996d6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 13 Aug 2018 14:28:10 -0400 Subject: [PATCH 0483/1626] searchCiphersBasic --- src/abstractions/search.service.ts | 1 + src/services/cipher.service.ts | 10 +++++--- src/services/search.service.ts | 41 +++++++++++++++++------------- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/abstractions/search.service.ts b/src/abstractions/search.service.ts index 4050a27230..d9f87d50bd 100644 --- a/src/abstractions/search.service.ts +++ b/src/abstractions/search.service.ts @@ -5,4 +5,5 @@ export abstract class SearchService { isSearchable: (query: string) => boolean; indexCiphers: () => Promise; searchCiphers: (query: string, filter?: (cipher: CipherView) => boolean) => Promise; + searchCiphersBasic: (ciphers: CipherView[], query: string) => CipherView[]; } diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index b5f1c046e9..3903495af8 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -67,10 +67,12 @@ export class CipherService implements CipherServiceAbstraction { } set decryptedCipherCache(value: CipherView[]) { this._decryptedCipherCache = value; - if (value == null) { - this.searchService().clearIndex(); - } else { - this.searchService().indexCiphers(); + if (this.searchService != null) { + if (value == null) { + this.searchService().clearIndex(); + } else { + this.searchService().indexCiphers(); + } } } diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 1cb1afda13..852da8eb81 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -103,24 +103,7 @@ export class SearchService implements SearchServiceAbstraction { if (this.index == null) { // Fall back to basic search if index is not available - return ciphers.filter((c) => { - if (c.name != null && c.name.toLowerCase().indexOf(query) > -1) { - return true; - } - if (this.onlySearchName) { - return false; - } - if (query.length >= 8 && c.id.startsWith(query)) { - return true; - } - if (c.subTitle != null && c.subTitle.toLowerCase().indexOf(query) > -1) { - return true; - } - if (c.login && c.login.uri != null && c.login.uri.toLowerCase().indexOf(query) > -1) { - return true; - } - return false; - }); + return this.searchCiphersBasic(ciphers, query); } const ciphersMap = new Map(); @@ -157,4 +140,26 @@ export class SearchService implements SearchServiceAbstraction { } return results; } + + searchCiphersBasic(ciphers: CipherView[], query: string) { + query = query.trim().toLowerCase(); + return ciphers.filter((c) => { + if (c.name != null && c.name.toLowerCase().indexOf(query) > -1) { + return true; + } + if (this.onlySearchName) { + return false; + } + if (query.length >= 8 && c.id.startsWith(query)) { + return true; + } + if (c.subTitle != null && c.subTitle.toLowerCase().indexOf(query) > -1) { + return true; + } + if (c.login && c.login.uri != null && c.login.uri.toLowerCase().indexOf(query) > -1) { + return true; + } + return false; + }); + } } From 2efe788d96c35d418d747edad0c48ce3c46f2b7c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 13 Aug 2018 16:00:21 -0400 Subject: [PATCH 0484/1626] joined fields and attachments indexing --- src/services/search.service.ts | 70 ++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 852da8eb81..281fe2c5f2 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -50,30 +50,11 @@ export class SearchService implements SearchServiceAbstraction { extractor: (c: CipherView) => c.login == null || !c.login.hasUris ? null : c.login.uris.filter((u) => u.hostname != null).map((u) => u.hostname), }); - (builder as any).field('fields', { - extractor: (c: CipherView) => { - if (!c.hasFields) { - return null; - } - const fields = c.fields.filter((f) => f.type === FieldType.Text).map((f) => { - let field = ''; - if (f.name != null) { - field += f.name; - } - if (f.value != null) { - if (field !== '') { - field += ' '; - } - field += f.value; - } - return field; - }); - return fields.filter((f) => f.trim() !== ''); - }, - }); - (builder as any).field('attachments', { - extractor: (c: CipherView) => !c.hasAttachments ? null : c.attachments.map((a) => a.fileName), - }); + (builder as any).field('fields', { extractor: (c: CipherView) => this.fieldExtractor(c, false) }); + (builder as any).field('fields_joined', { extractor: (c: CipherView) => this.fieldExtractor(c, true) }); + (builder as any).field('attachments', { extractor: (c: CipherView) => this.attachmentExtractor(c, false) }); + (builder as any).field('attachments_joined', + { extractor: (c: CipherView) => this.attachmentExtractor(c, true) }); const ciphers = await this.cipherService.getAllDecrypted(); ciphers.forEach((c) => builder.add(c)); this.index = builder.build(); @@ -162,4 +143,45 @@ export class SearchService implements SearchServiceAbstraction { return false; }); } + + private fieldExtractor(c: CipherView, joined: boolean) { + if (!c.hasFields) { + return null; + } + let fields: string[] = []; + c.fields.forEach((f) => { + if (f.name != null) { + fields.push(f.name); + } + if (f.type === FieldType.Text && f.value != null) { + fields.push(f.value); + } + }); + fields = fields.filter((f) => f.trim() !== ''); + if (fields.length === 0) { + return null; + } + return joined ? fields.join(' ') : fields; + } + + private attachmentExtractor(c: CipherView, joined: boolean) { + if (!c.hasAttachments) { + return null; + } + let attachments: string[] = []; + c.attachments.forEach((a) => { + if (a != null && a.fileName != null) { + if (joined && a.fileName.indexOf('.') > -1) { + attachments.push(a.fileName.substr(0, a.fileName.lastIndexOf('.'))); + } else { + attachments.push(a.fileName); + } + } + }); + attachments = attachments.filter((f) => f.trim() !== ''); + if (attachments.length === 0) { + return null; + } + return joined ? attachments.join(' ') : attachments; + } } From 8448b48cd7c65c3d7891bc00246e27777daaeb58 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 13 Aug 2018 16:03:24 -0400 Subject: [PATCH 0485/1626] dont sort search results --- src/services/search.service.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 281fe2c5f2..3d4238c468 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -116,9 +116,6 @@ export class SearchService implements SearchServiceAbstraction { } }); } - if (results != null) { - results.sort(this.cipherService.getLocaleSortingFunction()); - } return results; } From a7bbdf9c9391c8d58fee0231e1d4cfe34af55cdb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 13 Aug 2018 16:27:28 -0400 Subject: [PATCH 0486/1626] remove allciphers --- src/angular/components/ciphers.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/angular/components/ciphers.component.ts b/src/angular/components/ciphers.component.ts index 916c0adce6..a7968e7b5c 100644 --- a/src/angular/components/ciphers.component.ts +++ b/src/angular/components/ciphers.component.ts @@ -20,7 +20,6 @@ export class CiphersComponent { searchText: string; searchPlaceholder: string = null; - protected allCiphers: CipherView[] = []; protected filter: (cipher: CipherView) => boolean = null; protected searchPending = false; From 9f26f9f37771f8f450b380b4c05ffd5d9164f099 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 14 Aug 2018 15:12:10 -0400 Subject: [PATCH 0487/1626] support for prelogin kdf info --- src/abstractions/api.service.ts | 3 ++ src/abstractions/auth.service.ts | 2 + src/abstractions/crypto.service.ts | 4 +- src/abstractions/user.service.ts | 10 ++--- src/angular/components/export.component.ts | 12 ++---- src/angular/components/lock.component.ts | 4 +- src/angular/components/register.component.ts | 8 +++- src/enums/kdfType.ts | 3 ++ src/models/request/preloginRequest.ts | 7 ++++ src/models/request/registerRequest.ts | 9 +++- src/models/response/preloginResponse.ts | 11 +++++ src/services/api.service.ts | 7 ++++ src/services/auth.service.ts | 32 ++++++++++++--- src/services/crypto.service.ts | 16 +++++++- src/services/user.service.ts | 43 +++++++++++++++++--- 15 files changed, 140 insertions(+), 31 deletions(-) create mode 100644 src/enums/kdfType.ts create mode 100644 src/models/request/preloginRequest.ts create mode 100644 src/models/response/preloginResponse.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index debb78580d..9b9bf73c0a 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -27,6 +27,7 @@ import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; import { PaymentRequest } from '../models/request/paymentRequest'; +import { PreloginRequest } from '../models/request/preloginRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { SeatRequest } from '../models/request/seatRequest'; import { StorageRequest } from '../models/request/storageRequest'; @@ -70,6 +71,7 @@ import { OrganizationUserDetailsResponse, OrganizationUserUserDetailsResponse, } from '../models/response/organizationUserResponse'; +import { PreloginResponse } from '../models/response/preloginResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SyncResponse } from '../models/response/syncResponse'; import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; @@ -93,6 +95,7 @@ export abstract class ApiService { getProfile: () => Promise; getUserBilling: () => Promise; putProfile: (request: UpdateProfileRequest) => Promise; + postPrelogin: (request: PreloginRequest) => Promise; postEmailToken: (request: EmailTokenRequest) => Promise; postEmail: (request: EmailRequest) => Promise; postPassword: (request: PasswordRequest) => Promise; diff --git a/src/abstractions/auth.service.ts b/src/abstractions/auth.service.ts index 7b721433c0..b654365b15 100644 --- a/src/abstractions/auth.service.ts +++ b/src/abstractions/auth.service.ts @@ -1,6 +1,7 @@ import { TwoFactorProviderType } from '../enums/twoFactorProviderType'; import { AuthResult } from '../models/domain/authResult'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; export abstract class AuthService { email: string; @@ -16,4 +17,5 @@ export abstract class AuthService { logOut: (callback: Function) => void; getSupportedTwoFactorProviders: (win: Window) => any[]; getDefaultTwoFactorProvider: (u2fSupported: boolean) => TwoFactorProviderType; + makePreloginKey: (masterPassword: string, email: string) => Promise; } diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 9014a77726..8181ccf587 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -3,6 +3,8 @@ import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; import { ProfileOrganizationResponse } from '../models/response/profileOrganizationResponse'; +import { KdfType } from '../enums/kdfType'; + export abstract class CryptoService { setKey: (key: SymmetricCryptoKey) => Promise; setKeyHash: (keyHash: string) => Promise<{}>; @@ -25,7 +27,7 @@ export abstract class CryptoService { clearOrgKeys: (memoryOnly?: boolean) => Promise; clearKeys: () => Promise; toggleKey: () => Promise; - makeKey: (password: string, salt: string) => Promise; + makeKey: (password: string, salt: string, kdf: KdfType, kdfIterations: number) => Promise; makeShareKey: () => Promise<[CipherString, SymmetricCryptoKey]>; makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, CipherString]>; hashPassword: (password: string, key: SymmetricCryptoKey) => Promise; diff --git a/src/abstractions/user.service.ts b/src/abstractions/user.service.ts index 1a16c4a1fa..ab2239a845 100644 --- a/src/abstractions/user.service.ts +++ b/src/abstractions/user.service.ts @@ -1,16 +1,16 @@ import { OrganizationData } from '../models/data/organizationData'; import { Organization } from '../models/domain/organization'; -export abstract class UserService { - userId: string; - email: string; - stamp: string; +import { KdfType } from '../enums/kdfType'; - setUserIdAndEmail: (userId: string, email: string) => Promise; +export abstract class UserService { + setInformation: (userId: string, email: string, kdf: KdfType, kdfIterations: number) => Promise; setSecurityStamp: (stamp: string) => Promise; getUserId: () => Promise; getEmail: () => Promise; getSecurityStamp: () => Promise; + getKdf: () => Promise; + getKdfIterations: () => Promise; clear: () => Promise; isAuthenticated: () => Promise; getOrganization: (id: string) => Promise; diff --git a/src/angular/components/export.component.ts b/src/angular/components/export.component.ts index c509354f57..128a36d9ab 100644 --- a/src/angular/components/export.component.ts +++ b/src/angular/components/export.component.ts @@ -10,7 +10,6 @@ import { CryptoService } from '../../abstractions/crypto.service'; import { ExportService } from '../../abstractions/export.service'; import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; -import { UserService } from '../../abstractions/user.service'; export class ExportComponent { @Output() onSaved = new EventEmitter(); @@ -20,9 +19,9 @@ export class ExportComponent { showPassword = false; constructor(protected analytics: Angulartics2, protected toasterService: ToasterService, - protected cryptoService: CryptoService, protected userService: UserService, - protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, - protected exportService: ExportService, protected win: Window) { } + protected cryptoService: CryptoService, protected i18nService: I18nService, + protected platformUtilsService: PlatformUtilsService, protected exportService: ExportService, + protected win: Window) { } async submit() { if (this.masterPassword == null || this.masterPassword === '') { @@ -31,11 +30,8 @@ export class ExportComponent { return; } - const email = await this.userService.getEmail(); - const key = await this.cryptoService.makeKey(this.masterPassword, email); - const keyHash = await this.cryptoService.hashPassword(this.masterPassword, key); + const keyHash = await this.cryptoService.hashPassword(this.masterPassword, null); const storedKeyHash = await this.cryptoService.getKeyHash(); - if (storedKeyHash != null && keyHash != null && storedKeyHash === keyHash) { try { this.formPromise = this.getExportData(); diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 8fd2880000..695fa3679b 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -28,7 +28,9 @@ export class LockComponent { } const email = await this.userService.getEmail(); - const key = await this.cryptoService.makeKey(this.masterPassword, email); + const kdf = await this.userService.getKdf(); + const kdfIterations = await this.userService.getKdfIterations(); + const key = await this.cryptoService.makeKey(this.masterPassword, email, kdf, kdfIterations); const keyHash = await this.cryptoService.hashPassword(this.masterPassword, key); const storedKeyHash = await this.cryptoService.getKeyHash(); diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index 77f9959243..75df514fe4 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -12,6 +12,8 @@ import { CryptoService } from '../../abstractions/crypto.service'; import { I18nService } from '../../abstractions/i18n.service'; import { StateService } from '../../abstractions/state.service'; +import { KdfType } from '../../enums/kdfType'; + export class RegisterComponent { name: string = ''; email: string = ''; @@ -57,12 +59,14 @@ export class RegisterComponent { this.name = this.name === '' ? null : this.name; this.email = this.email.toLowerCase(); - const key = await this.cryptoService.makeKey(this.masterPassword, this.email); + const kdf = KdfType.PBKDF2; + const kdfIterations = 5000; + const key = await this.cryptoService.makeKey(this.masterPassword, this.email, kdf, kdfIterations); const encKey = await this.cryptoService.makeEncKey(key); const hashedPassword = await this.cryptoService.hashPassword(this.masterPassword, key); const keys = await this.cryptoService.makeKeyPair(encKey[0]); const request = new RegisterRequest(this.email, this.name, hashedPassword, - this.hint, encKey[1].encryptedString); + this.hint, encKey[1].encryptedString, kdf, kdfIterations); request.keys = new KeysRequest(keys[0], keys[1].encryptedString); const orgInvite = await this.stateService.get('orgInvitation'); if (orgInvite != null && orgInvite.token != null && orgInvite.organizationUserId != null) { diff --git a/src/enums/kdfType.ts b/src/enums/kdfType.ts new file mode 100644 index 0000000000..53eb59c4ce --- /dev/null +++ b/src/enums/kdfType.ts @@ -0,0 +1,3 @@ +export enum KdfType { + PBKDF2 = 0, +} diff --git a/src/models/request/preloginRequest.ts b/src/models/request/preloginRequest.ts new file mode 100644 index 0000000000..bee6058c7d --- /dev/null +++ b/src/models/request/preloginRequest.ts @@ -0,0 +1,7 @@ +export class PreloginRequest { + email: string; + + constructor(email: string) { + this.email = email; + } +} diff --git a/src/models/request/registerRequest.ts b/src/models/request/registerRequest.ts index 7d3e25fc36..d8caa4201a 100644 --- a/src/models/request/registerRequest.ts +++ b/src/models/request/registerRequest.ts @@ -1,5 +1,7 @@ import { KeysRequest } from './keysRequest'; +import { KdfType } from '../../enums/kdfType'; + export class RegisterRequest { name: string; email: string; @@ -9,12 +11,17 @@ export class RegisterRequest { keys: KeysRequest; token: string; organizationUserId: string; + kdf: KdfType; + kdfIterations: number; - constructor(email: string, name: string, masterPasswordHash: string, masterPasswordHint: string, key: string) { + constructor(email: string, name: string, masterPasswordHash: string, masterPasswordHint: string, key: string, + kdf: KdfType, kdfIterations: number) { this.name = name; this.email = email; this.masterPasswordHash = masterPasswordHash; this.masterPasswordHint = masterPasswordHint ? masterPasswordHint : null; this.key = key; + this.kdf = kdf; + this.kdfIterations = kdfIterations; } } diff --git a/src/models/response/preloginResponse.ts b/src/models/response/preloginResponse.ts new file mode 100644 index 0000000000..8a893b5f52 --- /dev/null +++ b/src/models/response/preloginResponse.ts @@ -0,0 +1,11 @@ +import { KdfType } from '../../enums/kdfType'; + +export class PreloginResponse { + kdf: KdfType; + kdfIterations: number; + + constructor(response: any) { + this.kdf = response.Kdf; + this.kdfIterations = response.KdfIterations; + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 091c58a663..4ffdf9e71b 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -33,6 +33,7 @@ import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; import { PaymentRequest } from '../models/request/paymentRequest'; +import { PreloginRequest } from '../models/request/preloginRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { SeatRequest } from '../models/request/seatRequest'; import { StorageRequest } from '../models/request/storageRequest'; @@ -77,6 +78,7 @@ import { OrganizationUserDetailsResponse, OrganizationUserUserDetailsResponse, } from '../models/response/organizationUserResponse'; +import { PreloginResponse } from '../models/response/preloginResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SyncResponse } from '../models/response/syncResponse'; import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; @@ -196,6 +198,11 @@ export class ApiService implements ApiServiceAbstraction { return new ProfileResponse(r); } + async postPrelogin(request: PreloginRequest): Promise { + const r = await this.send('POST', '/accounts/prelogin', request, false, true); + return new PreloginResponse(r); + } + postEmailToken(request: EmailTokenRequest): Promise { return this.send('POST', '/accounts/email-token', request, true, false); } diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 3e2364acf0..0f991eddd9 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,3 +1,4 @@ +import { KdfType } from '../enums/kdfType'; import { TwoFactorProviderType } from '../enums/twoFactorProviderType'; import { AuthResult } from '../models/domain/authResult'; @@ -5,8 +6,10 @@ import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; import { DeviceRequest } from '../models/request/deviceRequest'; import { KeysRequest } from '../models/request/keysRequest'; +import { PreloginRequest } from '../models/request/preloginRequest'; import { TokenRequest } from '../models/request/tokenRequest'; +import { ErrorResponse } from '../models/response/errorResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; @@ -77,6 +80,8 @@ export class AuthService { selectedTwoFactorProviderType: TwoFactorProviderType = null; private key: SymmetricCryptoKey; + private kdf: KdfType; + private kdfIterations: number; constructor(private cryptoService: CryptoService, private apiService: ApiService, private userService: UserService, private tokenService: TokenService, @@ -108,8 +113,7 @@ export class AuthService { async logIn(email: string, masterPassword: string): Promise { this.selectedTwoFactorProviderType = null; - email = email.toLowerCase(); - const key = await this.cryptoService.makeKey(masterPassword, email); + const key = await this.makePreloginKey(masterPassword, email); const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); return await this.logInHelper(email, hashedPassword, key); } @@ -123,8 +127,7 @@ export class AuthService { async logInComplete(email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean): Promise { this.selectedTwoFactorProviderType = null; - email = email.toLowerCase(); - const key = await this.cryptoService.makeKey(masterPassword, email); + const key = await this.makePreloginKey(masterPassword, email); const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); return await this.logInHelper(email, hashedPassword, key, twoFactorProvider, twoFactorToken, remember); } @@ -195,6 +198,24 @@ export class AuthService { return providerType; } + async makePreloginKey(masterPassword: string, email: string): Promise { + email = email.toLowerCase(); + this.kdf = null; + this.kdfIterations = null; + try { + const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email)); + if (preloginResponse != null) { + this.kdf = preloginResponse.kdf; + this.kdfIterations = preloginResponse.kdfIterations; + } + } catch (e) { + if (!(e instanceof ErrorResponse) || e.statusCode !== 404) { + throw e; + } + } + return this.cryptoService.makeKey(masterPassword, email, this.kdf, this.kdfIterations); + } + private async logInHelper(email: string, hashedPassword: string, key: SymmetricCryptoKey, twoFactorProvider?: TwoFactorProviderType, twoFactorToken?: string, remember?: boolean): Promise { const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email); @@ -235,7 +256,8 @@ export class AuthService { } await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken); - await this.userService.setUserIdAndEmail(this.tokenService.getUserId(), this.tokenService.getEmail()); + await this.userService.setInformation(this.tokenService.getUserId(), this.tokenService.getEmail(), + this.kdf, this.kdfIterations); if (this.setCryptoKeys) { await this.cryptoService.setKey(key); await this.cryptoService.setKeyHash(hashedPassword); diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index f70359a4e6..815a152e6d 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -1,4 +1,5 @@ import { EncryptionType } from '../enums/encryptionType'; +import { KdfType } from '../enums/kdfType'; import { CipherString } from '../models/domain/cipherString'; import { EncryptedObject } from '../models/domain/encryptedObject'; @@ -272,8 +273,19 @@ export class CryptoService implements CryptoServiceAbstraction { await this.setKey(key); } - async makeKey(password: string, salt: string): Promise { - const key = await this.cryptoFunctionService.pbkdf2(password, salt, 'sha256', 5000); + async makeKey(password: string, salt: string, kdf: KdfType, kdfIterations: number): + Promise { + let key: ArrayBuffer = null; + if (kdf == null || kdf === KdfType.PBKDF2) { + if (kdfIterations == null) { + kdfIterations = 5000; + } else if (kdfIterations < 5000) { + throw new Error('PBKDF2 iteration minimum is 5000.'); + } + key = await this.cryptoFunctionService.pbkdf2(password, salt, 'sha256', kdfIterations); + } else { + throw new Error('Unknown Kdf.'); + } return new SymmetricCryptoKey(key); } diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 3b149643d0..df80de16a3 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -5,28 +5,37 @@ import { UserService as UserServiceAbstraction } from '../abstractions/user.serv import { OrganizationData } from '../models/data/organizationData'; import { Organization } from '../models/domain/organization'; +import { KdfType } from '../enums/kdfType'; + const Keys = { userId: 'userId', userEmail: 'userEmail', stamp: 'securityStamp', + kdf: 'kdf', + kdfIterations: 'kdfIterations', organizationsPrefix: 'organizations_', }; export class UserService implements UserServiceAbstraction { - userId: string; - email: string; - stamp: string; + private userId: string; + private email: string; + private stamp: string; + private kdf: KdfType; + private kdfIterations: number; - constructor(private tokenService: TokenService, private storageService: StorageService) { - } + constructor(private tokenService: TokenService, private storageService: StorageService) { } - setUserIdAndEmail(userId: string, email: string): Promise { + setInformation(userId: string, email: string, kdf: KdfType, kdfIterations: number): Promise { this.email = email; this.userId = userId; + this.kdf = kdf; + this.kdfIterations = kdfIterations; return Promise.all([ this.storageService.save(Keys.userEmail, email), this.storageService.save(Keys.userId, userId), + this.storageService.save(Keys.kdf, kdf), + this.storageService.save(Keys.kdfIterations, kdfIterations), ]); } @@ -62,6 +71,24 @@ export class UserService implements UserServiceAbstraction { return this.stamp; } + async getKdf(): Promise { + if (this.kdf != null) { + return this.kdf; + } + + this.kdf = await this.storageService.get(Keys.kdf); + return this.kdf; + } + + async getKdfIterations(): Promise { + if (this.kdfIterations != null) { + return this.kdfIterations; + } + + this.kdfIterations = await this.storageService.get(Keys.kdfIterations); + return this.kdfIterations; + } + async clear(): Promise { const userId = await this.getUserId(); @@ -69,10 +96,14 @@ export class UserService implements UserServiceAbstraction { this.storageService.remove(Keys.userId), this.storageService.remove(Keys.userEmail), this.storageService.remove(Keys.stamp), + this.storageService.remove(Keys.kdf), + this.storageService.remove(Keys.kdfIterations), this.clearOrganizations(userId), ]); this.userId = this.email = this.stamp = null; + this.kdf = null; + this.kdfIterations = null; } async isAuthenticated(): Promise { From d56c5ff4f157b346500ceb0c256e88daaf4c4684 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 15 Aug 2018 09:01:00 -0400 Subject: [PATCH 0488/1626] just check statusCode --- src/services/auth.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 0f991eddd9..5a4227f8b4 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -209,7 +209,7 @@ export class AuthService { this.kdfIterations = preloginResponse.kdfIterations; } } catch (e) { - if (!(e instanceof ErrorResponse) || e.statusCode !== 404) { + if (e == null || e.statusCode !== 404) { throw e; } } From f16fc58d707b9ed55355e62440fb95185f972090 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 15 Aug 2018 11:43:52 -0400 Subject: [PATCH 0489/1626] allow original cipher to be passed during encrypt --- src/abstractions/cipher.service.ts | 2 +- src/services/cipher.service.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index 1046077659..71fa641228 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -14,7 +14,7 @@ export abstract class CipherService { decryptedCipherCache: CipherView[]; clearCache: () => void; - encrypt: (model: CipherView, key?: SymmetricCryptoKey) => Promise; + encrypt: (model: CipherView, key?: SymmetricCryptoKey, originalCipher?: Cipher) => Promise; encryptFields: (fieldsModel: FieldView[], key: SymmetricCryptoKey) => Promise; encryptField: (fieldModel: FieldView, key: SymmetricCryptoKey) => Promise; get: (id: string) => Promise; diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 3903495af8..5222ca0688 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -80,11 +80,14 @@ export class CipherService implements CipherServiceAbstraction { this.decryptedCipherCache = null; } - async encrypt(model: CipherView, key?: SymmetricCryptoKey): Promise { + async encrypt(model: CipherView, key?: SymmetricCryptoKey, originalCipher: Cipher = null): Promise { // Adjust password history if (model.id != null) { - const existingCipher = await (await this.get(model.id)).decrypt(); - if (existingCipher != null) { + if (originalCipher == null) { + originalCipher = await this.get(model.id); + } + if (originalCipher != null) { + const existingCipher = await originalCipher.decrypt(); model.passwordHistory = existingCipher.passwordHistory || []; if (model.type === CipherType.Login && existingCipher.type === CipherType.Login) { if (existingCipher.login.password != null && existingCipher.login.password !== '' && From 9ba3c176262b7d06209998305ab73d91c3e72458 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 16 Aug 2018 23:32:37 -0400 Subject: [PATCH 0490/1626] allow ciphers to be passed into search service --- src/abstractions/search.service.ts | 3 ++- src/services/search.service.ts | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/abstractions/search.service.ts b/src/abstractions/search.service.ts index d9f87d50bd..1ef8f92cd0 100644 --- a/src/abstractions/search.service.ts +++ b/src/abstractions/search.service.ts @@ -4,6 +4,7 @@ export abstract class SearchService { clearIndex: () => void; isSearchable: (query: string) => boolean; indexCiphers: () => Promise; - searchCiphers: (query: string, filter?: (cipher: CipherView) => boolean) => Promise; + searchCiphers: (query: string, filter?: (cipher: CipherView) => boolean, + ciphers?: CipherView[]) => Promise; searchCiphersBasic: (ciphers: CipherView[], query: string) => CipherView[]; } diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 3d4238c468..b779eff00b 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -63,7 +63,7 @@ export class SearchService implements SearchServiceAbstraction { console.timeEnd('search indexing'); } - async searchCiphers(query: string, filter: (cipher: CipherView) => boolean = null): + async searchCiphers(query: string, filter: (cipher: CipherView) => boolean = null, ciphers: CipherView[] = null): Promise { const results: CipherView[] = []; if (query != null) { @@ -73,7 +73,9 @@ export class SearchService implements SearchServiceAbstraction { query = null; } - let ciphers = await this.cipherService.getAllDecrypted(); + if (ciphers == null) { + ciphers = await this.cipherService.getAllDecrypted(); + } if (filter != null) { ciphers = ciphers.filter(filter); } @@ -82,7 +84,8 @@ export class SearchService implements SearchServiceAbstraction { return ciphers; } - if (this.index == null) { + const index = this.getIndexForSearch(); + if (index == null) { // Fall back to basic search if index is not available return this.searchCiphersBasic(ciphers, query); } @@ -94,12 +97,12 @@ export class SearchService implements SearchServiceAbstraction { const isQueryString = query != null && query.length > 1 && query.indexOf('>') === 0; if (isQueryString) { try { - searchResults = this.index.search(query.substr(1)); + searchResults = index.search(query.substr(1)); } catch { } } else { // tslint:disable-next-line const soWild = lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING; - searchResults = this.index.query((q) => { + searchResults = index.query((q) => { q.term(query, { fields: ['name'], wildcard: soWild }); q.term(query, { fields: ['subTitle'], wildcard: soWild }); q.term(query, { fields: ['login.uris'], wildcard: soWild }); @@ -141,6 +144,10 @@ export class SearchService implements SearchServiceAbstraction { }); } + getIndexForSearch(): lunr.Index { + return this.index; + } + private fieldExtractor(c: CipherView, joined: boolean) { if (!c.hasFields) { return null; From bdbba8c4938c70bba5cb19ba98df812d65a155ec Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 17 Aug 2018 11:05:06 -0400 Subject: [PATCH 0491/1626] switch to only trailing wildcard searches --- src/services/search.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index b779eff00b..16fef284fa 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -101,7 +101,7 @@ export class SearchService implements SearchServiceAbstraction { } catch { } } else { // tslint:disable-next-line - const soWild = lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING; + const soWild = lunr.Query.wildcard.TRAILING; searchResults = index.query((q) => { q.term(query, { fields: ['name'], wildcard: soWild }); q.term(query, { fields: ['subTitle'], wildcard: soWild }); From 1f9fbe43d7a78349e6fe994fa70d9b773af5c0f9 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 17 Aug 2018 11:07:50 -0400 Subject: [PATCH 0492/1626] trim lunr query after > --- src/services/search.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 16fef284fa..b5de21d6cb 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -97,7 +97,7 @@ export class SearchService implements SearchServiceAbstraction { const isQueryString = query != null && query.length > 1 && query.indexOf('>') === 0; if (isQueryString) { try { - searchResults = index.search(query.substr(1)); + searchResults = index.search(query.substr(1).trim()); } catch { } } else { // tslint:disable-next-line From bf9a9c5f9fb5933faeeb07a85f127373ebfe7284 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 17 Aug 2018 12:24:56 -0400 Subject: [PATCH 0493/1626] fix copy options --- src/angular/components/password-generator-history.component.ts | 2 +- src/angular/components/password-generator.component.ts | 2 +- src/angular/components/password-history.component.ts | 2 +- src/angular/components/view.component.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/angular/components/password-generator-history.component.ts b/src/angular/components/password-generator-history.component.ts index 015d64fcc0..45c1006992 100644 --- a/src/angular/components/password-generator-history.component.ts +++ b/src/angular/components/password-generator-history.component.ts @@ -27,7 +27,7 @@ export class PasswordGeneratorHistoryComponent implements OnInit { copy(password: string) { this.analytics.eventTrack.next({ action: 'Copied Historical Password' }); - const copyOptions = this.win != null ? { doc: this.win.document } : null; + const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(password, copyOptions); this.toasterService.popAsync('info', null, this.i18nService.t('valueCopied', this.i18nService.t('password'))); } diff --git a/src/angular/components/password-generator.component.ts b/src/angular/components/password-generator.component.ts index 0a1608dbb0..c6cd72fb6d 100644 --- a/src/angular/components/password-generator.component.ts +++ b/src/angular/components/password-generator.component.ts @@ -61,7 +61,7 @@ export class PasswordGeneratorComponent implements OnInit { copy() { this.analytics.eventTrack.next({ action: 'Copied Generated Password' }); - const copyOptions = this.win != null ? { doc: this.win.document } : null; + const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(this.password, copyOptions); this.toasterService.popAsync('info', null, this.i18nService.t('valueCopied', this.i18nService.t('password'))); } diff --git a/src/angular/components/password-history.component.ts b/src/angular/components/password-history.component.ts index 350dc6ed78..b847f9ea33 100644 --- a/src/angular/components/password-history.component.ts +++ b/src/angular/components/password-history.component.ts @@ -25,7 +25,7 @@ export class PasswordHistoryComponent implements OnInit { copy(password: string) { this.analytics.eventTrack.next({ action: 'Copied Password History' }); - const copyOptions = this.win != null ? { doc: this.win.document } : null; + const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(password, copyOptions); this.toasterService.popAsync('info', null, this.i18nService.t('valueCopied', this.i18nService.t('password'))); } diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index b05fd6a76d..33403710bf 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -122,7 +122,7 @@ export class ViewComponent implements OnDestroy { } this.analytics.eventTrack.next({ action: 'Copied ' + aType }); - const copyOptions = this.win != null ? { doc: this.win.document } : null; + const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(value, copyOptions); this.toasterService.popAsync('info', null, this.i18nService.t('valueCopied', this.i18nService.t(typeI18nKey))); From 0d69c3f26668b68a16a01339bfa199aaf2ad09e3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 17 Aug 2018 23:57:42 -0400 Subject: [PATCH 0494/1626] update to file save dialog --- .../services/electronPlatformUtils.service.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index fdd0bd86a1..f1d0c67d45 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -3,6 +3,7 @@ import { remote, shell, } from 'electron'; +import * as fs from 'fs'; import { isDev, @@ -113,13 +114,13 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { } saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void { - const blob = new Blob([blobData], blobOptions); - const a = win.document.createElement('a'); - a.href = win.URL.createObjectURL(blob); - a.download = fileName; - window.document.body.appendChild(a); - a.click(); - window.document.body.removeChild(a); + remote.dialog.showSaveDialog(remote.getCurrentWindow(), { + defaultPath: fileName, + }, (filename) => { + fs.writeFile(filename, Buffer.from(blobData), (err) => { + // error check? + }); + }); } getApplicationVersion(): string { From aed1c5e92762f1d2fa037aa9eb4d2b8e82f45707 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 18 Aug 2018 00:24:48 -0400 Subject: [PATCH 0495/1626] dont show tags on save dialog (mac) --- src/electron/services/electronPlatformUtils.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index f1d0c67d45..b39046e2b1 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -116,6 +116,7 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void { remote.dialog.showSaveDialog(remote.getCurrentWindow(), { defaultPath: fileName, + showsTagField: false, }, (filename) => { fs.writeFile(filename, Buffer.from(blobData), (err) => { // error check? From b64757132faf1ebb5438bec00720c58604fd29f6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 20 Aug 2018 08:41:12 -0400 Subject: [PATCH 0496/1626] index login.uris_split --- src/services/search.service.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index b5de21d6cb..70e78b54e6 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -9,6 +9,8 @@ import { SearchService as SearchServiceAbstraction } from '../abstractions/searc import { DeviceType } from '../enums/deviceType'; import { FieldType } from '../enums/fieldType'; +const IgnoredTlds = ['com', 'net', 'org', 'io', 'co', 'uk', 'au', 'nz', 'fr', 'de', 'eu', 'me', 'jp', 'cn']; + export class SearchService implements SearchServiceAbstraction { private indexing = false; private index: lunr.Index = null; @@ -50,6 +52,31 @@ export class SearchService implements SearchServiceAbstraction { extractor: (c: CipherView) => c.login == null || !c.login.hasUris ? null : c.login.uris.filter((u) => u.hostname != null).map((u) => u.hostname), }); + (builder as any).field('login.uris_split', { + boost: 2, + extractor: (c: CipherView) => { + if (c.login == null || !c.login.hasUris) { + return null; + } + let uriParts: string[] = []; + c.login.uris.forEach((u) => { + if (u.hostname == null) { + return; + } + const parts = u.hostname.split('.'); + if (parts.length > 0 && parts.length <= 2) { + uriParts.push(parts[0]); + } else if (parts.length > 2) { + uriParts = uriParts.concat(parts.slice(0, parts.length - 2)); + const lastBit = parts[parts.length - 2]; + if (IgnoredTlds.indexOf(lastBit) === -1) { + uriParts.push(lastBit); + } + } + }); + return uriParts.length === 0 ? null : uriParts; + }, + }); (builder as any).field('fields', { extractor: (c: CipherView) => this.fieldExtractor(c, false) }); (builder as any).field('fields_joined', { extractor: (c: CipherView) => this.fieldExtractor(c, true) }); (builder as any).field('attachments', { extractor: (c: CipherView) => this.attachmentExtractor(c, false) }); From ddee5908f1486c43de8ca908d6b38232d8f4dbfd Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 20 Aug 2018 13:45:32 -0400 Subject: [PATCH 0497/1626] notification service --- package-lock.json | 72 ++++++++------- package.json | 3 +- src/abstractions/environment.service.ts | 1 + src/abstractions/notifications.service.ts | 6 ++ .../components/environment.component.ts | 4 + src/enums/notificationType.ts | 14 +++ src/models/response/notificationResponse.ts | 71 +++++++++++++++ src/services/environment.service.ts | 6 ++ src/services/notifications.service.ts | 90 +++++++++++++++++++ 9 files changed, 236 insertions(+), 31 deletions(-) create mode 100644 src/abstractions/notifications.service.ts create mode 100644 src/enums/notificationType.ts create mode 100644 src/models/response/notificationResponse.ts create mode 100644 src/services/notifications.service.ts diff --git a/package-lock.json b/package-lock.json index c63ec41307..2a9b8547ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,6 +84,11 @@ "tslib": "^1.7.1" } }, + "@aspnet/signalr": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@aspnet/signalr/-/signalr-1.0.2.tgz", + "integrity": "sha512-sXleqUCCbodCOqUA8MjLSvtAgDTvDhEq6j3JyAq/w4RMJhpZ+dXK9+6xEMbzag2hisq5e/8vDC82JYutkcOISQ==" + }, "@types/form-data": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", @@ -1937,9 +1942,9 @@ "dev": true }, "electron": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/electron/-/electron-2.0.5.tgz", - "integrity": "sha512-NbWsgAvcxxQrDNaLA2L5adZTKWO6mZwC57uSPQiZiFjpO0K6uVNCjFyRbLnhq8AWq2tmcuzs6mFpIzQXmvlnUQ==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/electron/-/electron-2.0.7.tgz", + "integrity": "sha512-MRrDE6mrp+ZrIBpZM27pxbO2yEDKYfkmc6Ll79BtedMNEZsY4+oblupeDJL6RM6meUIp82KMo63W7fP65Tb89Q==", "dev": true, "requires": { "@types/node": "^8.0.24", @@ -1948,9 +1953,9 @@ }, "dependencies": { "@types/node": { - "version": "8.10.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.24.tgz", - "integrity": "sha512-5YaBKa6oFuWy7ptIFMATyftIcpZTZtvgrzPThEbs+kl4Uu41oUxiRunG0k32QZjD6MXMELls//ry/epNxc11aQ==", + "version": "8.10.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.26.tgz", + "integrity": "sha512-opk6bLLErLSwyVVJeSH5Ek7ZWOBSsN0JrvXTNVGLXLAXKB9xlTYajrplR44xVyMrmbut94H6uJ9jqzM/12jxkA==", "dev": true } } @@ -3301,12 +3306,12 @@ "dev": true }, "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", + "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", "dev": true, "requires": { - "ajv": "^5.1.0", + "ajv": "^5.3.0", "har-schema": "^2.0.0" } }, @@ -5358,9 +5363,9 @@ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", "dev": true }, "object-assign": { @@ -5878,6 +5883,12 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, + "psl": { + "version": "1.1.29", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", + "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==", + "dev": true + }, "pstree.remy": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.0.tgz", @@ -6214,31 +6225,31 @@ } }, "request": { - "version": "2.87.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", - "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", "dev": true, "requires": { "aws-sign2": "~0.7.0", - "aws4": "^1.6.0", + "aws4": "^1.8.0", "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.1", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", "forever-agent": "~0.6.1", - "form-data": "~2.3.1", - "har-validator": "~5.0.3", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.17", - "oauth-sign": "~0.8.2", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", "performance-now": "^2.1.0", - "qs": "~6.5.1", - "safe-buffer": "^5.1.1", - "tough-cookie": "~2.3.3", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", "tunnel-agent": "^0.6.0", - "uuid": "^3.1.0" + "uuid": "^3.3.2" } }, "requires-port": { @@ -7293,11 +7304,12 @@ } }, "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", "dev": true, "requires": { + "psl": "^1.1.24", "punycode": "^1.4.1" } }, diff --git a/package.json b/package.json index df787ee65e..1ca6ee9ca0 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@types/papaparse": "4.1.31", "@types/webcrypto": "0.0.28", "concurrently": "3.5.1", - "electron": "2.0.5", + "electron": "2.0.7", "jasmine": "^3.1.0", "jasmine-core": "^2.8.0", "jasmine-spec-reporter": "^4.2.1", @@ -67,6 +67,7 @@ "@angular/platform-browser-dynamic": "5.2.0", "@angular/router": "5.2.0", "@angular/upgrade": "5.2.0", + "@aspnet/signalr": "1.0.2", "angular2-toaster": "4.0.2", "angulartics2": "5.0.1", "core-js": "2.4.1", diff --git a/src/abstractions/environment.service.ts b/src/abstractions/environment.service.ts index 8c27710f2c..e561341d36 100644 --- a/src/abstractions/environment.service.ts +++ b/src/abstractions/environment.service.ts @@ -4,6 +4,7 @@ export abstract class EnvironmentService { apiUrl: string; identityUrl: string; iconsUrl: string; + notificationsUrl: string; getWebVaultUrl: () => string; setUrlsFromStorage: () => Promise; diff --git a/src/abstractions/notifications.service.ts b/src/abstractions/notifications.service.ts new file mode 100644 index 0000000000..540b9885fc --- /dev/null +++ b/src/abstractions/notifications.service.ts @@ -0,0 +1,6 @@ +import { EnvironmentService } from './environment.service'; + +export abstract class NotificationsService { + init: (environmentService: EnvironmentService) => Promise; + updateConnection: () => Promise; +} diff --git a/src/angular/components/environment.component.ts b/src/angular/components/environment.component.ts index 3bc31043f1..d7d4226d9b 100644 --- a/src/angular/components/environment.component.ts +++ b/src/angular/components/environment.component.ts @@ -16,6 +16,7 @@ export class EnvironmentComponent { identityUrl: string; apiUrl: string; webVaultUrl: string; + notificationsUrl: string; baseUrl: string; showCustom = false; @@ -26,6 +27,7 @@ export class EnvironmentComponent { this.apiUrl = environmentService.apiUrl || ''; this.identityUrl = environmentService.identityUrl || ''; this.iconsUrl = environmentService.iconsUrl || ''; + this.notificationsUrl = environmentService.notificationsUrl || ''; } async submit() { @@ -35,6 +37,7 @@ export class EnvironmentComponent { identity: this.identityUrl, webVault: this.webVaultUrl, icons: this.iconsUrl, + notifications: this.notificationsUrl, }); // re-set urls since service can change them, ex: prefixing https:// @@ -43,6 +46,7 @@ export class EnvironmentComponent { this.identityUrl = resUrls.identity; this.webVaultUrl = resUrls.webVault; this.iconsUrl = resUrls.icons; + this.notificationsUrl = resUrls.notifications; this.analytics.eventTrack.next({ action: 'Set Environment URLs' }); this.toasterService.popAsync('success', null, this.i18nService.t('environmentSaved')); diff --git a/src/enums/notificationType.ts b/src/enums/notificationType.ts new file mode 100644 index 0000000000..2ceddadd5d --- /dev/null +++ b/src/enums/notificationType.ts @@ -0,0 +1,14 @@ +export enum NotificationType { + SyncCipherUpdate = 0, + SyncCipherCreate = 1, + SyncLoginDelete = 2, + SyncFolderDelete = 3, + SyncCiphers = 4, + + SyncVault = 5, + SyncOrgKeys = 6, + SyncFolderCreate = 7, + SyncFolderUpdate = 8, + SyncCipherDelete = 9, + SyncSettings = 10, +} diff --git a/src/models/response/notificationResponse.ts b/src/models/response/notificationResponse.ts new file mode 100644 index 0000000000..2f8a2b01fa --- /dev/null +++ b/src/models/response/notificationResponse.ts @@ -0,0 +1,71 @@ +import { NotificationType } from '../../enums/notificationType'; + +export class NotificationResponse { + contextId: string; + type: NotificationType; + payload: any; + + constructor(response: any) { + this.contextId = response.contextId || response.ContextId; + this.type = response.type != null ? response.type : response.Type; + + const payload = response.payload || response.Payload; + switch (this.type) { + case NotificationType.SyncCipherCreate: + case NotificationType.SyncCipherDelete: + case NotificationType.SyncCipherUpdate: + case NotificationType.SyncLoginDelete: + this.payload = new SyncCipherNotification(payload); + break; + case NotificationType.SyncFolderCreate: + case NotificationType.SyncFolderDelete: + case NotificationType.SyncFolderUpdate: + this.payload = new SyncFolderNotification(payload); + break; + case NotificationType.SyncVault: + case NotificationType.SyncCiphers: + case NotificationType.SyncOrgKeys: + case NotificationType.SyncSettings: + this.payload = new SyncUserNotification(payload); + break; + default: + break; + } + } +} + +export class SyncCipherNotification { + id: string; + userId: string; + organizationId: string; + revisionDate: Date; + + constructor(response: any) { + this.id = response.Id; + this.userId = response.UserId; + this.organizationId = response.OrganizationId; + this.revisionDate = new Date(response.RevisionDate); + } +} + +export class SyncFolderNotification { + id: string; + userId: string; + revisionDate: Date; + + constructor(response: any) { + this.id = response.Id; + this.userId = response.UserId; + this.revisionDate = new Date(response.RevisionDate); + } +} + +export class SyncUserNotification { + userId: string; + date: Date; + + constructor(response: any) { + this.userId = response.UserId; + this.date = new Date(response.Date); + } +} diff --git a/src/services/environment.service.ts b/src/services/environment.service.ts index e41a27c54c..a50909eb23 100644 --- a/src/services/environment.service.ts +++ b/src/services/environment.service.ts @@ -12,6 +12,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { apiUrl: string; identityUrl: string; iconsUrl: string; + notificationsUrl: string; constructor(private apiService: ApiService, private storageService: StorageService) {} @@ -31,6 +32,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { api: null, identity: null, icons: null, + notifications: null, webVault: null, }; @@ -46,6 +48,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { this.apiUrl = envUrls.api = urls.api; this.identityUrl = envUrls.identity = urls.identity; this.iconsUrl = urls.icons; + this.notificationsUrl = urls.notifications; await this.apiService.setUrls(envUrls); } @@ -55,6 +58,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { urls.api = this.formatUrl(urls.api); urls.identity = this.formatUrl(urls.identity); urls.icons = this.formatUrl(urls.icons); + urls.notifications = this.formatUrl(urls.notifications); await this.storageService.save(ConstantsService.environmentUrlsKey, { base: urls.base, @@ -62,6 +66,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { identity: urls.identity, webVault: urls.webVault, icons: urls.icons, + notifications: urls.notifications, }); this.baseUrl = urls.base; @@ -69,6 +74,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { this.apiUrl = urls.api; this.identityUrl = urls.identity; this.iconsUrl = urls.icons; + this.notificationsUrl = urls.notifications; const envUrls = new EnvironmentUrls(); if (this.baseUrl) { diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts new file mode 100644 index 0000000000..55327c33a8 --- /dev/null +++ b/src/services/notifications.service.ts @@ -0,0 +1,90 @@ +import * as signalR from '@aspnet/signalr'; + +import { NotificationType } from '../enums/notificationType'; + +import { CipherService } from '../abstractions/cipher.service'; +import { CollectionService } from '../abstractions/collection.service'; +import { EnvironmentService } from '../abstractions/environment.service'; +import { FolderService } from '../abstractions/folder.service'; +import { NotificationsService as NotificationsServiceAbstraction } from '../abstractions/notifications.service'; +import { SettingsService } from '../abstractions/settings.service'; +import { SyncService } from '../abstractions/sync.service'; +import { TokenService } from '../abstractions/token.service'; +import { UserService } from '../abstractions/user.service'; + +import { NotificationResponse } from '../models/response/notificationResponse'; + +export class NotificationsService implements NotificationsServiceAbstraction { + private signalrConnection: signalR.HubConnection; + + constructor(private userService: UserService, private tokenService: TokenService, + private syncService: SyncService) { } + + async init(environmentService: EnvironmentService): Promise { + let url = 'https://notifications.bitwarden.com'; + if (environmentService.notificationsUrl != null) { + url = environmentService.notificationsUrl; + } else if (environmentService.baseUrl != null) { + url = environmentService.baseUrl + '/notifications'; + } + + if (this.signalrConnection != null) { + await this.signalrConnection.stop(); + this.signalrConnection = null; + } + + this.signalrConnection = new signalR.HubConnectionBuilder() + .withUrl(url + '/hub', { + accessTokenFactory: () => this.tokenService.getToken(), + }) + .configureLogging(signalR.LogLevel.Information) + .build(); + + this.signalrConnection.on('ReceiveMessage', async (data: any) => { + await this.processNotification(new NotificationResponse(data)); + }); + + this.updateConnection(); + } + + async updateConnection(): Promise { + try { + if (await this.userService.isAuthenticated()) { + await this.signalrConnection.start(); + } else { + await this.signalrConnection.stop(); + } + } catch (e) { + // tslint:disable-next-line + console.error(e.toString()); + } + } + + private async processNotification(notification: NotificationResponse) { + if (notification == null) { + return; + } + + switch (notification.type) { + case NotificationType.SyncCipherCreate: + case NotificationType.SyncCipherDelete: + case NotificationType.SyncCipherUpdate: + case NotificationType.SyncLoginDelete: + this.syncService.fullSync(false); + break; + case NotificationType.SyncFolderCreate: + case NotificationType.SyncFolderDelete: + case NotificationType.SyncFolderUpdate: + this.syncService.fullSync(false); + break; + case NotificationType.SyncVault: + case NotificationType.SyncCiphers: + case NotificationType.SyncOrgKeys: + case NotificationType.SyncSettings: + this.syncService.fullSync(false); + break; + default: + break; + } + } +} From d0c51bacfd63b67a48a1975a9b67c812c2ae4872 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 20 Aug 2018 16:01:26 -0400 Subject: [PATCH 0498/1626] sync folders and ciphers. fix dates --- src/abstractions/api.service.ts | 1 + src/abstractions/sync.service.ts | 11 ++- src/models/data/cipherData.ts | 2 +- src/models/data/passwordHistoryData.ts | 2 +- src/models/domain/cipher.ts | 4 +- src/models/domain/folder.ts | 3 + src/models/domain/password.ts | 6 +- src/models/request/passwordHistoryRequest.ts | 5 - src/models/response/cipherResponse.ts | 2 +- .../response/passwordHistoryResponse.ts | 2 +- src/services/api.service.ts | 5 + src/services/cipher.service.ts | 4 +- src/services/folder.service.ts | 4 +- src/services/notifications.service.ts | 28 +++--- src/services/sync.service.ts | 97 +++++++++++++++---- 15 files changed, 127 insertions(+), 49 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 9b9bf73c0a..3ee640286a 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -117,6 +117,7 @@ export abstract class ApiService { postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise; postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise; + getFolder: (id: string) => Promise; postFolder: (request: FolderRequest) => Promise; putFolder: (id: string, request: FolderRequest) => Promise; deleteFolder: (id: string) => Promise; diff --git a/src/abstractions/sync.service.ts b/src/abstractions/sync.service.ts index ef4b4e8dc8..694c0517ea 100644 --- a/src/abstractions/sync.service.ts +++ b/src/abstractions/sync.service.ts @@ -1,9 +1,16 @@ +import { + SyncCipherNotification, + SyncFolderNotification, +} from '../models/response/notificationResponse'; + export abstract class SyncService { syncInProgress: boolean; getLastSync: () => Promise; setLastSync: (date: Date) => Promise; - syncStarted: () => void; - syncCompleted: (successfully: boolean) => void; fullSync: (forceSync: boolean) => Promise; + syncUpsertFolder: (notification: SyncFolderNotification) => Promise; + syncDeleteFolder: (notification: SyncFolderNotification) => Promise; + syncUpsertCipher: (notification: SyncCipherNotification) => Promise; + syncDeleteCipher: (notification: SyncFolderNotification) => Promise; } diff --git a/src/models/data/cipherData.ts b/src/models/data/cipherData.ts index e3b9944de7..84dece2e81 100644 --- a/src/models/data/cipherData.ts +++ b/src/models/data/cipherData.ts @@ -18,7 +18,7 @@ export class CipherData { edit: boolean; organizationUseTotp: boolean; favorite: boolean; - revisionDate: Date; + revisionDate: string; type: CipherType; sizeName: string; name: string; diff --git a/src/models/data/passwordHistoryData.ts b/src/models/data/passwordHistoryData.ts index 1ef1c87a48..067c46fd18 100644 --- a/src/models/data/passwordHistoryData.ts +++ b/src/models/data/passwordHistoryData.ts @@ -2,7 +2,7 @@ import { PasswordHistoryResponse } from '../response/passwordHistoryResponse'; export class PasswordHistoryData { password: string; - lastUsedDate: Date; + lastUsedDate: string; constructor(response?: PasswordHistoryResponse) { if (response == null) { diff --git a/src/models/domain/cipher.ts b/src/models/domain/cipher.ts index d66ee67cc2..1c3040edc8 100644 --- a/src/models/domain/cipher.ts +++ b/src/models/domain/cipher.ts @@ -54,7 +54,7 @@ export class Cipher extends Domain { this.favorite = obj.favorite; this.organizationUseTotp = obj.organizationUseTotp; this.edit = obj.edit; - this.revisionDate = obj.revisionDate; + this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null; this.collectionIds = obj.collectionIds; this.localData = localData; @@ -178,7 +178,7 @@ export class Cipher extends Domain { c.edit = this.edit; c.organizationUseTotp = this.organizationUseTotp; c.favorite = this.favorite; - c.revisionDate = this.revisionDate; + c.revisionDate = this.revisionDate.toISOString(); c.type = this.type; c.collectionIds = this.collectionIds; diff --git a/src/models/domain/folder.ts b/src/models/domain/folder.ts index 77b9fb310d..c0a8d179a9 100644 --- a/src/models/domain/folder.ts +++ b/src/models/domain/folder.ts @@ -8,6 +8,7 @@ import Domain from './domain'; export class Folder extends Domain { id: string; name: CipherString; + revisionDate: Date; constructor(obj?: FolderData, alreadyEncrypted: boolean = false) { super(); @@ -19,6 +20,8 @@ export class Folder extends Domain { id: null, name: null, }, alreadyEncrypted, ['id']); + + this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null; } decrypt(): Promise { diff --git a/src/models/domain/password.ts b/src/models/domain/password.ts index c60d0937fb..ef735ba0e1 100644 --- a/src/models/domain/password.ts +++ b/src/models/domain/password.ts @@ -17,8 +17,8 @@ export class Password extends Domain { this.buildDomainModel(this, obj, { password: null, - lastUsedDate: null, - }, alreadyEncrypted, ['lastUsedDate']); + }, alreadyEncrypted); + this.lastUsedDate = new Date(obj.lastUsedDate); } async decrypt(orgId: string): Promise { @@ -30,7 +30,7 @@ export class Password extends Domain { toPasswordHistoryData(): PasswordHistoryData { const ph = new PasswordHistoryData(); - ph.lastUsedDate = this.lastUsedDate; + ph.lastUsedDate = this.lastUsedDate.toISOString(); this.buildDataModel(this, ph, { password: null, }); diff --git a/src/models/request/passwordHistoryRequest.ts b/src/models/request/passwordHistoryRequest.ts index c917de1d10..7b74cb1201 100644 --- a/src/models/request/passwordHistoryRequest.ts +++ b/src/models/request/passwordHistoryRequest.ts @@ -1,9 +1,4 @@ export class PasswordHistoryRequest { password: string; lastUsedDate: Date; - - constructor(response: any) { - this.password = response.Password; - this.lastUsedDate = response.LastUsedDate; - } } diff --git a/src/models/response/cipherResponse.ts b/src/models/response/cipherResponse.ts index 3d2770fb82..87ceec6338 100644 --- a/src/models/response/cipherResponse.ts +++ b/src/models/response/cipherResponse.ts @@ -22,7 +22,7 @@ export class CipherResponse { favorite: boolean; edit: boolean; organizationUseTotp: boolean; - revisionDate: Date; + revisionDate: string; attachments: AttachmentResponse[]; passwordHistory: PasswordHistoryResponse[]; collectionIds: string[]; diff --git a/src/models/response/passwordHistoryResponse.ts b/src/models/response/passwordHistoryResponse.ts index 8630da8f03..03c210de1c 100644 --- a/src/models/response/passwordHistoryResponse.ts +++ b/src/models/response/passwordHistoryResponse.ts @@ -1,6 +1,6 @@ export class PasswordHistoryResponse { password: string; - lastUsedDate: Date; + lastUsedDate: string; constructor(response: any) { this.password = response.Password; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 4ffdf9e71b..20d28c4241 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -286,6 +286,11 @@ export class ApiService implements ApiServiceAbstraction { // Folder APIs + async getFolder(id: string): Promise { + const r = await this.send('GET', '/folders/' + id, null, true, true); + return new FolderResponse(r); + } + async postFolder(request: FolderRequest): Promise { const r = await this.send('POST', '/folders', request, true, true); return new FolderResponse(r); diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 5222ca0688..3ade53634c 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -644,7 +644,9 @@ export class CipherService implements CipherServiceAbstraction { } if (typeof id === 'string') { - const i = id as string; + if (ciphers[id] == null) { + return; + } delete ciphers[id]; } else { (id as string[]).forEach((i) => { diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index c03cb0c298..34cecf775c 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -152,7 +152,9 @@ export class FolderService implements FolderServiceAbstraction { } if (typeof id === 'string') { - const i = id as string; + if (folders[id] == null) { + return; + } delete folders[id]; } else { (id as string[]).forEach((i) => { diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index 55327c33a8..cfbb0dc5d4 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -2,23 +2,24 @@ import * as signalR from '@aspnet/signalr'; import { NotificationType } from '../enums/notificationType'; -import { CipherService } from '../abstractions/cipher.service'; -import { CollectionService } from '../abstractions/collection.service'; +import { AppIdService } from '../abstractions/appId.service'; import { EnvironmentService } from '../abstractions/environment.service'; -import { FolderService } from '../abstractions/folder.service'; import { NotificationsService as NotificationsServiceAbstraction } from '../abstractions/notifications.service'; -import { SettingsService } from '../abstractions/settings.service'; import { SyncService } from '../abstractions/sync.service'; import { TokenService } from '../abstractions/token.service'; import { UserService } from '../abstractions/user.service'; -import { NotificationResponse } from '../models/response/notificationResponse'; +import { + NotificationResponse, + SyncCipherNotification, + SyncFolderNotification, +} from '../models/response/notificationResponse'; export class NotificationsService implements NotificationsServiceAbstraction { private signalrConnection: signalR.HubConnection; constructor(private userService: UserService, private tokenService: TokenService, - private syncService: SyncService) { } + private syncService: SyncService, private appIdService: AppIdService) { } async init(environmentService: EnvironmentService): Promise { let url = 'https://notifications.bitwarden.com'; @@ -61,21 +62,26 @@ export class NotificationsService implements NotificationsServiceAbstraction { } private async processNotification(notification: NotificationResponse) { - if (notification == null) { + const appId = await this.appIdService.getAppId(); + if (notification == null || notification.contextId === appId) { return; } switch (notification.type) { case NotificationType.SyncCipherCreate: - case NotificationType.SyncCipherDelete: case NotificationType.SyncCipherUpdate: + this.syncService.syncUpsertCipher(notification.payload as SyncCipherNotification); + break; + case NotificationType.SyncCipherDelete: case NotificationType.SyncLoginDelete: - this.syncService.fullSync(false); + this.syncService.syncDeleteCipher(notification.payload as SyncCipherNotification); break; case NotificationType.SyncFolderCreate: - case NotificationType.SyncFolderDelete: case NotificationType.SyncFolderUpdate: - this.syncService.fullSync(false); + this.syncService.syncUpsertFolder(notification.payload as SyncFolderNotification); + break; + case NotificationType.SyncFolderDelete: + this.syncService.syncDeleteFolder(notification.payload as SyncFolderNotification); break; case NotificationType.SyncVault: case NotificationType.SyncCiphers: diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index 07dd66e003..ae1a8fa7ca 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -18,6 +18,10 @@ import { CipherResponse } from '../models/response/cipherResponse'; import { CollectionDetailsResponse } from '../models/response/collectionResponse'; import { DomainsResponse } from '../models/response/domainsResponse'; import { FolderResponse } from '../models/response/folderResponse'; +import { + SyncCipherNotification, + SyncFolderNotification, +} from '../models/response/notificationResponse'; import { ProfileResponse } from '../models/response/profileResponse'; const Keys = { @@ -57,22 +61,11 @@ export class SyncService implements SyncServiceAbstraction { await this.storageService.save(Keys.lastSyncPrefix + userId, date.toJSON()); } - syncStarted() { - this.syncInProgress = true; - this.messagingService.send('syncStarted'); - } - - syncCompleted(successfully: boolean) { - this.syncInProgress = false; - this.messagingService.send('syncCompleted', { successfully: successfully }); - } - async fullSync(forceSync: boolean): Promise { this.syncStarted(); const isAuthenticated = await this.userService.isAuthenticated(); if (!isAuthenticated) { - this.syncCompleted(false); - return false; + return this.syncCompleted(false); } const now = new Date(); @@ -81,14 +74,12 @@ export class SyncService implements SyncServiceAbstraction { const skipped = needsSyncResult[1]; if (skipped) { - this.syncCompleted(false); - return false; + return this.syncCompleted(false); } if (!needsSync) { await this.setLastSync(now); - this.syncCompleted(false); - return false; + return this.syncCompleted(false); } const userId = await this.userService.getUserId(); @@ -102,16 +93,82 @@ export class SyncService implements SyncServiceAbstraction { await this.syncSettings(userId, response.domains); await this.setLastSync(now); - this.syncCompleted(true); - return true; + return this.syncCompleted(true); } catch (e) { - this.syncCompleted(false); - return false; + return this.syncCompleted(false); } } + async syncUpsertFolder(notification: SyncFolderNotification): Promise { + this.syncStarted(); + if (await this.userService.isAuthenticated()) { + try { + const remoteFolder = await this.apiService.getFolder(notification.id); + const localFolder = await this.folderService.get(notification.id); + if (remoteFolder != null && + (localFolder == null || localFolder.revisionDate < notification.revisionDate)) { + const userId = await this.userService.getUserId(); + await this.folderService.upsert(new FolderData(remoteFolder, userId)); + this.messagingService.send('syncedUpsertedFolder', { folderId: notification.id }); + return this.syncCompleted(true); + } + } catch { } + } + return this.syncCompleted(false); + } + + async syncDeleteFolder(notification: SyncFolderNotification): Promise { + this.syncStarted(); + if (await this.userService.isAuthenticated()) { + await this.folderService.delete(notification.id); + this.messagingService.send('syncedDeletedFolder', { folderId: notification.id }); + this.syncCompleted(true); + return true; + } + return this.syncCompleted(false); + } + + async syncUpsertCipher(notification: SyncCipherNotification): Promise { + this.syncStarted(); + if (await this.userService.isAuthenticated()) { + try { + const remoteCipher = await this.apiService.getCipher(notification.id); + const localCipher = await this.cipherService.get(notification.id); + if (remoteCipher != null && + (localCipher == null || localCipher.revisionDate < notification.revisionDate)) { + const userId = await this.userService.getUserId(); + await this.cipherService.upsert(new CipherData(remoteCipher, userId)); + this.messagingService.send('syncedUpsertedCipher', { cipherId: notification.id }); + return this.syncCompleted(true); + } + } catch { } + } + return this.syncCompleted(false); + } + + async syncDeleteCipher(notification: SyncCipherNotification): Promise { + this.syncStarted(); + if (await this.userService.isAuthenticated()) { + await this.cipherService.delete(notification.id); + this.messagingService.send('syncedDeletedCipher', { cipherId: notification.id }); + return this.syncCompleted(true); + } + return this.syncCompleted(false); + } + // Helpers + private syncStarted() { + this.syncInProgress = true; + this.messagingService.send('syncStarted'); + } + + private syncCompleted(successfully: boolean): boolean { + this.syncInProgress = false; + this.messagingService.send('syncCompleted', { successfully: successfully }); + return successfully; + } + private async needsSyncing(forceSync: boolean) { if (forceSync) { return [true, false]; From 9bd8b73e27429b70a11e0016f794d1a976076c9f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 20 Aug 2018 16:20:51 -0400 Subject: [PATCH 0499/1626] standardize date types --- src/models/api/loginApi.ts | 2 +- src/models/data/loginData.ts | 2 +- src/models/domain/login.ts | 4 ++-- src/models/request/cipherRequest.ts | 3 ++- src/models/response/billingResponse.ts | 16 ++++++++-------- src/models/response/breachAccountResponse.ts | 6 +++--- src/models/response/eventResponse.ts | 2 +- .../response/organizationBillingResponse.ts | 4 ++-- src/models/view/folderView.ts | 2 ++ 9 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/models/api/loginApi.ts b/src/models/api/loginApi.ts index 7f5fac7b6b..2f9f5579af 100644 --- a/src/models/api/loginApi.ts +++ b/src/models/api/loginApi.ts @@ -4,7 +4,7 @@ export class LoginApi { uris: LoginUriApi[]; username: string; password: string; - passwordRevisionDate?: Date; + passwordRevisionDate: string; totp: string; constructor(data: any) { diff --git a/src/models/data/loginData.ts b/src/models/data/loginData.ts index 31a4d6bcce..e3139a8c51 100644 --- a/src/models/data/loginData.ts +++ b/src/models/data/loginData.ts @@ -6,7 +6,7 @@ export class LoginData { uris: LoginUriData[]; username: string; password: string; - passwordRevisionDate?: Date; + passwordRevisionDate: string; totp: string; constructor(data?: LoginApi) { diff --git a/src/models/domain/login.ts b/src/models/domain/login.ts index 23fc207335..dc721503f4 100644 --- a/src/models/domain/login.ts +++ b/src/models/domain/login.ts @@ -20,7 +20,7 @@ export class Login extends Domain { return; } - this.passwordRevisionDate = obj.passwordRevisionDate; + this.passwordRevisionDate = obj.passwordRevisionDate != null ? new Date(obj.passwordRevisionDate) : null; this.buildDomainModel(this, obj, { username: null, password: null, @@ -55,7 +55,7 @@ export class Login extends Domain { toLoginData(): LoginData { const l = new LoginData(); - l.passwordRevisionDate = this.passwordRevisionDate; + l.passwordRevisionDate = this.passwordRevisionDate != null ? this.passwordRevisionDate.toISOString() : null; this.buildDataModel(this, l, { username: null, password: null, diff --git a/src/models/request/cipherRequest.ts b/src/models/request/cipherRequest.ts index fa2caa326d..46d1762018 100644 --- a/src/models/request/cipherRequest.ts +++ b/src/models/request/cipherRequest.ts @@ -39,7 +39,8 @@ export class CipherRequest { uris: null, username: cipher.login.username ? cipher.login.username.encryptedString : null, password: cipher.login.password ? cipher.login.password.encryptedString : null, - passwordRevisionDate: cipher.login.passwordRevisionDate, + passwordRevisionDate: cipher.login.passwordRevisionDate != null ? + cipher.login.passwordRevisionDate.toISOString() : null, totp: cipher.login.totp ? cipher.login.totp.encryptedString : null, }; diff --git a/src/models/response/billingResponse.ts b/src/models/response/billingResponse.ts index d3e451696a..4c3cd5ad01 100644 --- a/src/models/response/billingResponse.ts +++ b/src/models/response/billingResponse.ts @@ -9,7 +9,7 @@ export class BillingResponse { upcomingInvoice: BillingInvoiceResponse; charges: BillingChargeResponse[] = []; license: any; - expiration: Date; + expiration: string; constructor(response: any) { this.storageName = response.StorageName; @@ -43,11 +43,11 @@ export class BillingSourceResponse { } export class BillingSubscriptionResponse { - trialStartDate: Date; - trialEndDate: Date; - periodStartDate: Date; - periodEndDate: Date; - cancelledDate: Date; + trialStartDate: string; + trialEndDate: string; + periodStartDate: string; + periodEndDate: string; + cancelledDate: string; cancelAtEndDate: boolean; status: string; cancelled: boolean; @@ -83,7 +83,7 @@ export class BillingSubscriptionItemResponse { } export class BillingInvoiceResponse { - date: Date; + date: string; amount: number; constructor(response: any) { @@ -93,7 +93,7 @@ export class BillingInvoiceResponse { } export class BillingChargeResponse { - createdDate: Date; + createdDate: string; amount: number; paymentSource: BillingSourceResponse; status: string; diff --git a/src/models/response/breachAccountResponse.ts b/src/models/response/breachAccountResponse.ts index cc5f748232..68cf62d4ec 100644 --- a/src/models/response/breachAccountResponse.ts +++ b/src/models/response/breachAccountResponse.ts @@ -1,13 +1,13 @@ export class BreachAccountResponse { - addedDate: Date; - breachDate: Date; + addedDate: string; + breachDate: string; dataClasses: string[]; description: string; domain: string; isActive: boolean; isVerified: boolean; logoType: string; - modifiedDate: Date; + modifiedDate: string; name: string; pwnCount: number; title: string; diff --git a/src/models/response/eventResponse.ts b/src/models/response/eventResponse.ts index db44bc1a7a..658d05baa8 100644 --- a/src/models/response/eventResponse.ts +++ b/src/models/response/eventResponse.ts @@ -10,7 +10,7 @@ export class EventResponse { groupId: string; organizationUserId: string; actingUserId: string; - date: Date; + date: string; deviceType: DeviceType; ipAddress: string; diff --git a/src/models/response/organizationBillingResponse.ts b/src/models/response/organizationBillingResponse.ts index 1cd35e8dc6..f68ef9e6c8 100644 --- a/src/models/response/organizationBillingResponse.ts +++ b/src/models/response/organizationBillingResponse.ts @@ -13,7 +13,7 @@ export class OrganizationBillingResponse extends OrganizationResponse { subscription: BillingSubscriptionResponse; upcomingInvoice: BillingInvoiceResponse; charges: BillingChargeResponse[] = []; - expiration: Date; + expiration: string; constructor(response: any) { super(response); @@ -27,6 +27,6 @@ export class OrganizationBillingResponse extends OrganizationResponse { if (response.Charges != null) { this.charges = response.Charges.map((c: any) => new BillingChargeResponse(c)); } - this.expiration = response.Expiration != null ? new Date(response.Expiration) : null; + this.expiration = response.Expiration; } } diff --git a/src/models/view/folderView.ts b/src/models/view/folderView.ts index ebe8ea157a..a0fa99b48d 100644 --- a/src/models/view/folderView.ts +++ b/src/models/view/folderView.ts @@ -5,6 +5,7 @@ import { Folder } from '../domain/folder'; export class FolderView implements View { id: string = null; name: string; + revisionDate: Date; constructor(f?: Folder) { if (!f) { @@ -12,5 +13,6 @@ export class FolderView implements View { } this.id = f.id; + this.revisionDate = f.revisionDate; } } From 7cae08a55bf48798e0a46cc4131d4b8425582bcd Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 20 Aug 2018 17:00:39 -0400 Subject: [PATCH 0500/1626] reinit notifications after setting urls --- src/services/environment.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/services/environment.service.ts b/src/services/environment.service.ts index a50909eb23..44b770673b 100644 --- a/src/services/environment.service.ts +++ b/src/services/environment.service.ts @@ -4,6 +4,7 @@ import { ConstantsService } from './constants.service'; import { ApiService } from '../abstractions/api.service'; import { EnvironmentService as EnvironmentServiceAbstraction } from '../abstractions/environment.service'; +import { NotificationsService } from '../abstractions/notifications.service'; import { StorageService } from '../abstractions/storage.service'; export class EnvironmentService implements EnvironmentServiceAbstraction { @@ -14,7 +15,8 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { iconsUrl: string; notificationsUrl: string; - constructor(private apiService: ApiService, private storageService: StorageService) {} + constructor(private apiService: ApiService, private storageService: StorageService, + private notificationsService: NotificationsService) { } getWebVaultUrl(): string { if (this.webVaultUrl != null) { @@ -85,6 +87,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { } await this.apiService.setUrls(envUrls); + this.notificationsService.init(this); return urls; } From bba52192dc9dff68cd4da80398a1127235ada06e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 20 Aug 2018 17:00:49 -0400 Subject: [PATCH 0501/1626] reload view on sync complete --- src/angular/components/view.component.ts | 28 ++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index 33403710bf..17866be231 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -1,7 +1,10 @@ import { + ChangeDetectorRef, EventEmitter, Input, + NgZone, OnDestroy, + OnInit, Output, } from '@angular/core'; @@ -23,8 +26,11 @@ import { AttachmentView } from '../../models/view/attachmentView'; import { CipherView } from '../../models/view/cipherView'; import { FieldView } from '../../models/view/fieldView'; import { LoginUriView } from '../../models/view/loginUriView'; +import { BroadcasterService } from '../services/broadcaster.service'; -export class ViewComponent implements OnDestroy { +const BroadcasterSubscriptionId = 'ViewComponent'; + +export class ViewComponent implements OnDestroy, OnInit { @Input() cipherId: string; @Output() onEditCipher = new EventEmitter(); @@ -46,9 +52,27 @@ export class ViewComponent implements OnDestroy { protected tokenService: TokenService, protected toasterService: ToasterService, protected cryptoService: CryptoService, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, protected analytics: Angulartics2, - protected auditService: AuditService, protected win: Window) { } + protected auditService: AuditService, protected win: Window, + protected broadcasterService: BroadcasterService, protected ngZone: NgZone, + protected changeDetectorRef: ChangeDetectorRef) { } + + ngOnInit() { + this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { + this.ngZone.run(async () => { + switch (message.command) { + case 'syncCompleted': + if (message.successfully) { + await this.load(); + this.changeDetectorRef.detectChanges(); + } + break; + } + }); + }); + } ngOnDestroy() { + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); this.cleanUp(); } From 21e09535896078c4716205e8141f6d3b2182a981 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 20 Aug 2018 17:08:19 -0400 Subject: [PATCH 0502/1626] null check notificationsService --- src/services/environment.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/environment.service.ts b/src/services/environment.service.ts index 44b770673b..cfc6a5e6c0 100644 --- a/src/services/environment.service.ts +++ b/src/services/environment.service.ts @@ -87,7 +87,9 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { } await this.apiService.setUrls(envUrls); - this.notificationsService.init(this); + if (this.notificationsService != null) { + this.notificationsService.init(this); + } return urls; } From 50666a761dba3d2d7d880f1faf488fd9d719ea50 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 20 Aug 2018 22:20:04 -0400 Subject: [PATCH 0503/1626] refresh token and reconnect on org key change --- src/services/notifications.service.ts | 66 +++++++++++++++------------ 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index cfbb0dc5d4..65abd9dda5 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -2,6 +2,7 @@ import * as signalR from '@aspnet/signalr'; import { NotificationType } from '../enums/notificationType'; +import { ApiService } from '../abstractions/api.service'; import { AppIdService } from '../abstractions/appId.service'; import { EnvironmentService } from '../abstractions/environment.service'; import { NotificationsService as NotificationsServiceAbstraction } from '../abstractions/notifications.service'; @@ -17,35 +18,20 @@ import { export class NotificationsService implements NotificationsServiceAbstraction { private signalrConnection: signalR.HubConnection; + private url: string; constructor(private userService: UserService, private tokenService: TokenService, - private syncService: SyncService, private appIdService: AppIdService) { } + private syncService: SyncService, private appIdService: AppIdService, + private apiService: ApiService) { } async init(environmentService: EnvironmentService): Promise { - let url = 'https://notifications.bitwarden.com'; + this.url = 'https://notifications.bitwarden.com'; if (environmentService.notificationsUrl != null) { - url = environmentService.notificationsUrl; + this.url = environmentService.notificationsUrl; } else if (environmentService.baseUrl != null) { - url = environmentService.baseUrl + '/notifications'; + this.url = environmentService.baseUrl + '/notifications'; } - - if (this.signalrConnection != null) { - await this.signalrConnection.stop(); - this.signalrConnection = null; - } - - this.signalrConnection = new signalR.HubConnectionBuilder() - .withUrl(url + '/hub', { - accessTokenFactory: () => this.tokenService.getToken(), - }) - .configureLogging(signalR.LogLevel.Information) - .build(); - - this.signalrConnection.on('ReceiveMessage', async (data: any) => { - await this.processNotification(new NotificationResponse(data)); - }); - - this.updateConnection(); + this.reconnect(); } async updateConnection(): Promise { @@ -70,27 +56,51 @@ export class NotificationsService implements NotificationsServiceAbstraction { switch (notification.type) { case NotificationType.SyncCipherCreate: case NotificationType.SyncCipherUpdate: - this.syncService.syncUpsertCipher(notification.payload as SyncCipherNotification); + await this.syncService.syncUpsertCipher(notification.payload as SyncCipherNotification); break; case NotificationType.SyncCipherDelete: case NotificationType.SyncLoginDelete: - this.syncService.syncDeleteCipher(notification.payload as SyncCipherNotification); + await this.syncService.syncDeleteCipher(notification.payload as SyncCipherNotification); break; case NotificationType.SyncFolderCreate: case NotificationType.SyncFolderUpdate: - this.syncService.syncUpsertFolder(notification.payload as SyncFolderNotification); + await this.syncService.syncUpsertFolder(notification.payload as SyncFolderNotification); break; case NotificationType.SyncFolderDelete: - this.syncService.syncDeleteFolder(notification.payload as SyncFolderNotification); + await this.syncService.syncDeleteFolder(notification.payload as SyncFolderNotification); break; case NotificationType.SyncVault: case NotificationType.SyncCiphers: - case NotificationType.SyncOrgKeys: case NotificationType.SyncSettings: - this.syncService.fullSync(false); + await this.syncService.fullSync(false); + break; + case NotificationType.SyncOrgKeys: + await this.apiService.refreshIdentityToken(); + await this.syncService.fullSync(true); + // Now reconnect to join the new org groups + await this.reconnect(); break; default: break; } } + + private async reconnect() { + if (this.signalrConnection != null) { + await this.signalrConnection.stop(); + this.signalrConnection = null; + } + + this.signalrConnection = new signalR.HubConnectionBuilder() + .withUrl(this.url + '/hub', { + accessTokenFactory: () => this.tokenService.getToken(), + }) + // .configureLogging(signalR.LogLevel.Information) + .build(); + + this.signalrConnection.on('ReceiveMessage', async (data: any) => { + await this.processNotification(new NotificationResponse(data)); + }); + await this.updateConnection(); + } } From 9cfd693576f9d901de8e3f332ae9756877ef480c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 21 Aug 2018 08:20:43 -0400 Subject: [PATCH 0504/1626] only sync on edit if they already have the item/folder --- src/abstractions/sync.service.ts | 4 +-- src/services/notifications.service.ts | 6 +++-- src/services/sync.service.ts | 36 +++++++++++++++------------ 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/abstractions/sync.service.ts b/src/abstractions/sync.service.ts index 694c0517ea..264e6ecbf9 100644 --- a/src/abstractions/sync.service.ts +++ b/src/abstractions/sync.service.ts @@ -9,8 +9,8 @@ export abstract class SyncService { getLastSync: () => Promise; setLastSync: (date: Date) => Promise; fullSync: (forceSync: boolean) => Promise; - syncUpsertFolder: (notification: SyncFolderNotification) => Promise; + syncUpsertFolder: (notification: SyncFolderNotification, isEdit: boolean) => Promise; syncDeleteFolder: (notification: SyncFolderNotification) => Promise; - syncUpsertCipher: (notification: SyncCipherNotification) => Promise; + syncUpsertCipher: (notification: SyncCipherNotification, isEdit: boolean) => Promise; syncDeleteCipher: (notification: SyncFolderNotification) => Promise; } diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index 65abd9dda5..be33b067f7 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -56,7 +56,8 @@ export class NotificationsService implements NotificationsServiceAbstraction { switch (notification.type) { case NotificationType.SyncCipherCreate: case NotificationType.SyncCipherUpdate: - await this.syncService.syncUpsertCipher(notification.payload as SyncCipherNotification); + await this.syncService.syncUpsertCipher(notification.payload as SyncCipherNotification, + notification.type === NotificationType.SyncCipherUpdate); break; case NotificationType.SyncCipherDelete: case NotificationType.SyncLoginDelete: @@ -64,7 +65,8 @@ export class NotificationsService implements NotificationsServiceAbstraction { break; case NotificationType.SyncFolderCreate: case NotificationType.SyncFolderUpdate: - await this.syncService.syncUpsertFolder(notification.payload as SyncFolderNotification); + await this.syncService.syncUpsertFolder(notification.payload as SyncFolderNotification, + notification.type === NotificationType.SyncFolderUpdate); break; case NotificationType.SyncFolderDelete: await this.syncService.syncDeleteFolder(notification.payload as SyncFolderNotification); diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index ae1a8fa7ca..a1b294a1a2 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -99,18 +99,20 @@ export class SyncService implements SyncServiceAbstraction { } } - async syncUpsertFolder(notification: SyncFolderNotification): Promise { + async syncUpsertFolder(notification: SyncFolderNotification, isEdit: boolean): Promise { this.syncStarted(); if (await this.userService.isAuthenticated()) { try { - const remoteFolder = await this.apiService.getFolder(notification.id); const localFolder = await this.folderService.get(notification.id); - if (remoteFolder != null && - (localFolder == null || localFolder.revisionDate < notification.revisionDate)) { - const userId = await this.userService.getUserId(); - await this.folderService.upsert(new FolderData(remoteFolder, userId)); - this.messagingService.send('syncedUpsertedFolder', { folderId: notification.id }); - return this.syncCompleted(true); + if ((!isEdit && localFolder == null) || + (isEdit && localFolder != null && localFolder.revisionDate < notification.revisionDate)) { + const remoteFolder = await this.apiService.getFolder(notification.id); + if (remoteFolder != null) { + const userId = await this.userService.getUserId(); + await this.folderService.upsert(new FolderData(remoteFolder, userId)); + this.messagingService.send('syncedUpsertedFolder', { folderId: notification.id }); + return this.syncCompleted(true); + } } } catch { } } @@ -128,18 +130,20 @@ export class SyncService implements SyncServiceAbstraction { return this.syncCompleted(false); } - async syncUpsertCipher(notification: SyncCipherNotification): Promise { + async syncUpsertCipher(notification: SyncCipherNotification, isEdit: boolean): Promise { this.syncStarted(); if (await this.userService.isAuthenticated()) { try { - const remoteCipher = await this.apiService.getCipher(notification.id); const localCipher = await this.cipherService.get(notification.id); - if (remoteCipher != null && - (localCipher == null || localCipher.revisionDate < notification.revisionDate)) { - const userId = await this.userService.getUserId(); - await this.cipherService.upsert(new CipherData(remoteCipher, userId)); - this.messagingService.send('syncedUpsertedCipher', { cipherId: notification.id }); - return this.syncCompleted(true); + if ((!isEdit && localCipher == null) || + (isEdit && localCipher != null && localCipher.revisionDate < notification.revisionDate)) { + const remoteCipher = await this.apiService.getCipher(notification.id); + if (remoteCipher != null) { + const userId = await this.userService.getUserId(); + await this.cipherService.upsert(new CipherData(remoteCipher, userId)); + this.messagingService.send('syncedUpsertedCipher', { cipherId: notification.id }); + return this.syncCompleted(true); + } } } catch { } } From 75d4db81f7fbdc14d18135f8218a7fe9b654cff6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 21 Aug 2018 09:04:52 -0400 Subject: [PATCH 0505/1626] trailing wildcard search on split uris --- src/services/search.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 70e78b54e6..59bcbe3f23 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -133,6 +133,7 @@ export class SearchService implements SearchServiceAbstraction { q.term(query, { fields: ['name'], wildcard: soWild }); q.term(query, { fields: ['subTitle'], wildcard: soWild }); q.term(query, { fields: ['login.uris'], wildcard: soWild }); + q.term(query, { fields: ['login.uris_split'], wildcard: soWild }); lunr.tokenizer(query).forEach((token) => { q.term(token.toString(), {}); }); From 953970498e2da454b0ee6574db2e9a4c53188c13 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 21 Aug 2018 09:25:16 -0400 Subject: [PATCH 0506/1626] update notification sync logic for collectionids --- src/models/response/notificationResponse.ts | 2 ++ src/services/sync.service.ts | 36 +++++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/models/response/notificationResponse.ts b/src/models/response/notificationResponse.ts index 2f8a2b01fa..7d84c79ce5 100644 --- a/src/models/response/notificationResponse.ts +++ b/src/models/response/notificationResponse.ts @@ -38,12 +38,14 @@ export class SyncCipherNotification { id: string; userId: string; organizationId: string; + collectionIds: string[]; revisionDate: Date; constructor(response: any) { this.id = response.Id; this.userId = response.UserId; this.organizationId = response.OrganizationId; + this.collectionIds = response.CollectionIds; this.revisionDate = new Date(response.RevisionDate); } } diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index a1b294a1a2..6a1edf3c32 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -134,9 +134,41 @@ export class SyncService implements SyncServiceAbstraction { this.syncStarted(); if (await this.userService.isAuthenticated()) { try { + let shouldUpdate = true; const localCipher = await this.cipherService.get(notification.id); - if ((!isEdit && localCipher == null) || - (isEdit && localCipher != null && localCipher.revisionDate < notification.revisionDate)) { + if (localCipher != null && localCipher.revisionDate >= notification.revisionDate) { + shouldUpdate = false; + } + + let checkCollections = false; + if (shouldUpdate) { + if (isEdit) { + shouldUpdate = localCipher != null; + checkCollections = true; + } else { + if (notification.collectionIds == null || notification.organizationId == null) { + shouldUpdate = localCipher == null; + } else { + shouldUpdate = false; + checkCollections = true; + } + } + } + + if (!shouldUpdate && checkCollections && notification.organizationId != null && + notification.collectionIds != null && notification.collectionIds.length > 0) { + const collections = await this.collectionService.getAll(); + if (collections != null) { + for (let i = 0; i < collections.length; i++) { + if (notification.collectionIds.indexOf(collections[i].id)) { + shouldUpdate = true; + break; + } + } + } + } + + if (shouldUpdate) { const remoteCipher = await this.apiService.getCipher(notification.id); if (remoteCipher != null) { const userId = await this.userService.getUserId(); From 686597ab53dcbd2933b35277724d47d2600704a2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 21 Aug 2018 15:16:05 -0400 Subject: [PATCH 0507/1626] update packages and npm audit fix --- package-lock.json | 1005 ++++++++++++++++++++------------------------- package.json | 16 +- 2 files changed, 458 insertions(+), 563 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2a9b8547ce..1af87fc835 100644 --- a/package-lock.json +++ b/package-lock.json @@ -165,12 +165,12 @@ "dev": true }, "accepts": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", - "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", "dev": true, "requires": { - "mime-types": "~2.1.11", + "mime-types": "~2.1.18", "negotiator": "0.6.1" } }, @@ -306,13 +306,13 @@ "dev": true }, "anymatch": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", - "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", "dev": true, "requires": { - "micromatch": "^2.1.5", - "normalize-path": "^2.0.0" + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" } }, "append-transform": { @@ -347,13 +347,10 @@ } }, "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "dev": true, - "requires": { - "arr-flatten": "^1.0.1" - } + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true }, "arr-flatten": { "version": "1.1.0", @@ -380,15 +377,15 @@ "dev": true }, "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, "arraybuffer.slice": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz", - "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco=", + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", "dev": true }, "asn1": { @@ -464,6 +461,12 @@ "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", "dev": true }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "dev": true + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -927,14 +930,32 @@ } }, "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "dev": true, "requires": { - "expand-range": "^1.8.1", - "preserve": "^0.2.0", - "repeat-element": "^1.1.2" + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } } }, "brorand": { @@ -1213,20 +1234,24 @@ } }, "chokidar": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", - "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", + "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", "dev": true, "requires": { - "anymatch": "^1.3.0", + "anymatch": "^2.0.0", "async-each": "^1.0.0", - "fsevents": "^1.0.0", - "glob-parent": "^2.0.0", + "braces": "^2.3.0", + "fsevents": "^1.2.2", + "glob-parent": "^3.1.0", "inherits": "^2.0.1", "is-binary-path": "^1.0.0", - "is-glob": "^2.0.0", + "is-glob": "^4.0.0", + "lodash.debounce": "^4.0.8", + "normalize-path": "^2.1.1", "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0" + "readdirp": "^2.0.0", + "upath": "^1.0.5" } }, "chownr": { @@ -1416,9 +1441,9 @@ "dev": true }, "compare-versions": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.3.0.tgz", - "integrity": "sha512-MAAAIOdi2s4Gl6rZ76PNcUa9IOYB+5ICdT41o5uMRf09aEu/F9RK+qhe8RjXNPwcTjGV7KU7h2P/fljThFVqyQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.3.1.tgz", + "integrity": "sha512-GkIcfJ9sDt4+gS+RWH3X+kR7ezuKdu3fg2oA9nRA8HZoqZwAKv3ml3TyfB9OyV2iFXxCw7q5XfV6SyPbSCT2pw==", "dev": true }, "component-bind": { @@ -1428,9 +1453,9 @@ "dev": true }, "component-emitter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz", - "integrity": "sha1-KWWU8nU9qmOZbSrwjRWpURbJrsM=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", "dev": true }, "component-inherit": { @@ -2066,91 +2091,49 @@ } }, "engine.io": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.8.3.tgz", - "integrity": "sha1-jef5eJXSDTm4X4ju7nd7K9QrE9Q=", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.0.tgz", + "integrity": "sha512-mRbgmAtQ4GAlKwuPnnAvXXwdPhEx+jkc0OBCLrXuD/CRvwNK3AxRSnqK4FSqmAMRRHryVJP8TopOvmEaA64fKw==", "dev": true, "requires": { - "accepts": "1.3.3", + "accepts": "~1.3.4", "base64id": "1.0.0", "cookie": "0.3.1", - "debug": "2.3.3", - "engine.io-parser": "1.3.2", - "ws": "1.1.2" - }, - "dependencies": { - "debug": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", - "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", - "dev": true, - "requires": { - "ms": "0.7.2" - } - }, - "ms": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", - "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", - "dev": true - } + "debug": "~3.1.0", + "engine.io-parser": "~2.1.0", + "ws": "~3.3.1" } }, "engine.io-client": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.8.3.tgz", - "integrity": "sha1-F5jtk0USRkU9TG9jXXogH+lA1as=", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", + "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", "dev": true, "requires": { "component-emitter": "1.2.1", "component-inherit": "0.0.3", - "debug": "2.3.3", - "engine.io-parser": "1.3.2", + "debug": "~3.1.0", + "engine.io-parser": "~2.1.1", "has-cors": "1.1.0", "indexof": "0.0.1", - "parsejson": "0.0.3", "parseqs": "0.0.5", "parseuri": "0.0.5", - "ws": "1.1.2", - "xmlhttprequest-ssl": "1.5.3", + "ws": "~3.3.1", + "xmlhttprequest-ssl": "~1.5.4", "yeast": "0.1.2" - }, - "dependencies": { - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "debug": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", - "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", - "dev": true, - "requires": { - "ms": "0.7.2" - } - }, - "ms": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", - "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", - "dev": true - } } }, "engine.io-parser": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.3.2.tgz", - "integrity": "sha1-k3sHnwAH0Ik+xW1GyyILjLQ1Igo=", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.2.tgz", + "integrity": "sha512-dInLFzr80RijZ1rGpx1+56/uFoH7/7InhH3kZt+Ms6hT8tNx3NGW/WNSA/f8As1WkOfkuyb3tnRyuXGxusclMw==", "dev": true, "requires": { "after": "0.8.2", - "arraybuffer.slice": "0.0.6", + "arraybuffer.slice": "~0.0.7", "base64-arraybuffer": "0.1.5", "blob": "0.0.4", - "has-binary": "0.1.7", - "wtf-8": "1.0.0" + "has-binary2": "~1.0.2" } }, "ent": { @@ -2306,6 +2289,12 @@ "braces": "^0.1.2" }, "dependencies": { + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, "braces": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz", @@ -2314,17 +2303,63 @@ "requires": { "expand-range": "^0.1.0" } - }, - "expand-range": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz", - "integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=", + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "requires": { - "is-number": "^0.1.1", - "repeat-string": "^0.2.2" + "ms": "2.0.0" } }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-range": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz", + "integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=", + "dev": true, + "requires": { + "is-number": "^0.1.1", + "repeat-string": "^0.2.2" + }, + "dependencies": { "is-number": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz", @@ -2339,24 +2374,6 @@ } } }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "dev": true, - "requires": { - "is-posix-bracket": "^0.1.0" - } - }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "dev": true, - "requires": { - "fill-range": "^2.1.0" - } - }, "expand-template": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-1.1.1.tgz", @@ -2386,12 +2403,74 @@ } }, "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", "dev": true, "requires": { - "is-extglob": "^1.0.0" + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } } }, "extract-zip": { @@ -2450,12 +2529,6 @@ "pend": "~1.2.0" } }, - "filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", - "dev": true - }, "fileset": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", @@ -2467,16 +2540,26 @@ } }, "fill-range": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", - "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "dev": true, "requires": { - "is-number": "^2.1.0", - "isobject": "^2.0.0", - "randomatic": "^3.0.0", - "repeat-element": "^1.1.2", - "repeat-string": "^1.5.2" + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } } }, "finalhandler": { @@ -2522,9 +2605,9 @@ } }, "follow-redirects": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.2.tgz", - "integrity": "sha512-kssLorP/9acIdpQ2udQVTiCS5LQmdEz9mvdIfDcl1gYX2tPKFADHSyFdvJS040XdFsPzemWtgI3q8mFVCxtX8A==", + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.6.tgz", + "integrity": "sha512-xay/eYZGgdpb3rpugZj1HunNaPcqc6fud/RW7LNEQntvKzuRO4DDLL+MnJIbTHh6t3Kda3v2RvhY2doxUddnig==", "dev": true, "requires": { "debug": "^3.1.0" @@ -2536,15 +2619,6 @@ "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", "dev": true }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } - }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -2656,12 +2730,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2676,17 +2752,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -2803,7 +2882,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -2815,6 +2895,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2829,6 +2910,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2947,7 +3029,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -2959,6 +3042,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3080,6 +3164,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3212,23 +3297,25 @@ "path-is-absolute": "^1.0.0" } }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "dev": true, - "requires": { - "glob-parent": "^2.0.0", - "is-glob": "^2.0.0" - } - }, "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", "dev": true, "requires": { - "is-glob": "^2.0.0" + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } } }, "global-dirs": { @@ -3332,19 +3419,19 @@ } } }, - "has-binary": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.7.tgz", - "integrity": "sha1-aOYesWIQyVRaClzOBqhzkS/h5ow=", + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", "dev": true, "requires": { - "isarray": "0.0.1" + "isarray": "2.0.1" }, "dependencies": { "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", "dev": true } } @@ -3679,21 +3766,6 @@ } } }, - "is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", - "dev": true - }, - "is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", - "dev": true, - "requires": { - "is-primitive": "^2.0.0" - } - }, "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -3701,9 +3773,9 @@ "dev": true }, "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, "is-finite": { @@ -3724,12 +3796,12 @@ } }, "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", "dev": true, "requires": { - "is-extglob": "^1.0.0" + "is-extglob": "^2.1.1" } }, "is-installed-globally": { @@ -3749,9 +3821,9 @@ "dev": true }, "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, "requires": { "kind-of": "^3.0.2" @@ -3789,18 +3861,6 @@ } } }, - "is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", - "dev": true - }, - "is-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", - "dev": true - }, "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", @@ -3863,13 +3923,10 @@ "dev": true }, "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true }, "isstream": { "version": "0.1.2", @@ -4031,27 +4088,19 @@ } }, "jasmine": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.1.0.tgz", - "integrity": "sha1-K9Wf1+xuwOistk4J9Fpo7SrRlSo=", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.2.0.tgz", + "integrity": "sha512-qv6TZ32r+slrQz8fbx2EhGbD9zlJo3NwPrpLK1nE8inILtZO9Fap52pyHk7mNTh4tG50a+1+tOiWVT3jO5I0Sg==", "dev": true, "requires": { "glob": "^7.0.6", - "jasmine-core": "~3.1.0" - }, - "dependencies": { - "jasmine-core": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.1.0.tgz", - "integrity": "sha1-pHheE11d9lAk38kiSVPfWFvSdmw=", - "dev": true - } + "jasmine-core": "~3.2.0" } }, "jasmine-core": { - "version": "2.99.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.99.1.tgz", - "integrity": "sha1-5kAN8ea1bhMLYcS80JPap/boyhU=", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.2.1.tgz", + "integrity": "sha512-pa9tbBWgU0EE4SWgc85T4sa886ufuQdsgruQANhECYjwqgV4z7Vw/499aCaP8ZH79JDS4vhm8doDG9HO4+e4sA==", "dev": true }, "jasmine-spec-reporter": { @@ -4129,12 +4178,6 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "dev": true }, - "json3": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", - "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", - "dev": true - }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -4156,14 +4199,14 @@ } }, "karma": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/karma/-/karma-1.7.1.tgz", - "integrity": "sha512-k5pBjHDhmkdaUccnC7gE3mBzZjcxyxYsYVaqiL2G5AqlfLyBO5nw2VdNK+O16cveEPd/gIOWULH7gkiYYwVNHg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/karma/-/karma-3.0.0.tgz", + "integrity": "sha512-ZTjyuDXVXhXsvJ1E4CnZzbCjSxD6sEdzEsFYogLuZM0yqvg/mgz+O+R1jb0J7uAQeuzdY8kJgx6hSNXLwFuHIQ==", "dev": true, "requires": { "bluebird": "^3.3.0", "body-parser": "^1.16.1", - "chokidar": "^1.4.1", + "chokidar": "^2.0.3", "colors": "^1.1.0", "combine-lists": "^1.0.0", "connect": "^3.6.0", @@ -4175,33 +4218,19 @@ "graceful-fs": "^4.1.2", "http-proxy": "^1.13.0", "isbinaryfile": "^3.0.0", - "lodash": "^3.8.0", - "log4js": "^0.6.31", - "mime": "^1.3.4", + "lodash": "^4.17.4", + "log4js": "^3.0.0", + "mime": "^2.3.1", "minimatch": "^3.0.2", "optimist": "^0.6.1", "qjobs": "^1.1.4", "range-parser": "^1.2.0", "rimraf": "^2.6.0", "safe-buffer": "^5.0.1", - "socket.io": "1.7.3", - "source-map": "^0.5.3", - "tmp": "0.0.31", - "useragent": "^2.1.12" - }, - "dependencies": { - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } + "socket.io": "2.1.1", + "source-map": "^0.6.1", + "tmp": "0.0.33", + "useragent": "2.2.1" } }, "karma-chrome-launcher": { @@ -4245,9 +4274,9 @@ } }, "karma-coverage-istanbul-reporter": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-1.4.3.tgz", - "integrity": "sha1-O13/RmT6W41RlrmInj9hwforgNk=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-2.0.1.tgz", + "integrity": "sha512-UcgrHkFehI5+ivMouD8NH/UOHiX4oCAtwaANylzPFdcAuD52fnCUuelacq2gh8tZ4ydhU3+xiXofSq7j5Ehygw==", "dev": true, "requires": { "istanbul-api": "^1.3.1", @@ -4285,13 +4314,10 @@ "dev": true }, "karma-jasmine-html-reporter": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-0.2.2.tgz", - "integrity": "sha1-SKjl7xiAdhfuK14zwRlMNbQ5Ukw=", - "dev": true, - "requires": { - "karma-jasmine": "^1.0.2" - } + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.3.1.tgz", + "integrity": "sha512-J8pUc58QeRhpHQ+sXBRZ016ZW9ZOjU4vjYA6Jah69WvBaqR7tGvXUzy7w/DoULbNrD8+hnZCpvdeEtqXtirY2g==", + "dev": true }, "karma-safari-launcher": { "version": "1.0.0", @@ -4480,44 +4506,41 @@ "dev": true }, "log4js": { - "version": "0.6.38", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-0.6.38.tgz", - "integrity": "sha1-LElBFmldb7JUgJQ9P8hy5mKlIv0=", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-3.0.5.tgz", + "integrity": "sha512-IX5c3G/7fuTtdr0JjOT2OIR12aTESVhsH6cEsijloYwKgcPRlO6DgOU72v0UFhWcoV1HN6+M3dwT89qVPLXm0w==", "dev": true, "requires": { - "readable-stream": "~1.0.2", - "semver": "~4.3.3" + "circular-json": "^0.5.5", + "date-format": "^1.2.0", + "debug": "^3.1.0", + "rfdc": "^1.1.2", + "streamroller": "0.7.0" }, "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "circular-json": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.5.5.tgz", + "integrity": "sha512-13YaR6kiz0kBNmIVM87Io8Hp7bWOo4r61vkEANy8iH9R9bc6avud/1FT0SBpqR1RpIQADOh/Q+yHZDA1iL6ysA==", "dev": true }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "date-format": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-1.2.0.tgz", + "integrity": "sha1-YV6CjiM90aubua4JUODOzPpuytg=", + "dev": true + }, + "streamroller": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-0.7.0.tgz", + "integrity": "sha512-WREzfy0r0zUqp3lGO096wRuUp7ho1X6uo/7DJfTlEi0Iv/4gT7YHqXDjKC2ioVGBZtE8QzsQD9nx1nIuoZ57jQ==", "dev": true, "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "date-format": "^1.2.0", + "debug": "^3.1.0", + "mkdirp": "^0.5.1", + "readable-stream": "^2.3.0" } - }, - "semver": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", - "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", - "dev": true - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true } } }, @@ -4615,12 +4638,6 @@ "object-visit": "^1.0.0" } }, - "math-random": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", - "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", - "dev": true - }, "md5.js": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", @@ -4656,24 +4673,51 @@ } }, "micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "dev": true, "requires": { - "arr-diff": "^2.0.0", - "array-unique": "^0.2.1", - "braces": "^1.8.2", - "expand-brackets": "^0.1.4", - "extglob": "^0.3.1", - "filename-regex": "^2.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.1", - "kind-of": "^3.0.2", - "normalize-path": "^2.0.1", - "object.omit": "^2.0.0", - "parse-glob": "^3.0.4", - "regex-cache": "^0.4.2" + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } } }, "miller-rabin": { @@ -4687,9 +4731,9 @@ } }, "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz", + "integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==", "dev": true }, "mime-db": { @@ -5424,16 +5468,6 @@ } } }, - "object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", - "dev": true, - "requires": { - "for-own": "^0.1.4", - "is-extendable": "^0.1.1" - } - }, "object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", @@ -5508,12 +5542,6 @@ } } }, - "options": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", - "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=", - "dev": true - }, "os-browserify": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", @@ -5582,18 +5610,6 @@ "pbkdf2": "^3.0.3" } }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", - "dev": true, - "requires": { - "glob-base": "^0.3.0", - "is-dotfile": "^1.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.0" - } - }, "parse-json": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", @@ -5603,15 +5619,6 @@ "error-ex": "^1.2.0" } }, - "parsejson": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz", - "integrity": "sha1-q343WfIJ7OmUN5c/fQ8fZK4OZKs=", - "dev": true, - "requires": { - "better-assert": "~1.0.0" - } - }, "parseqs": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", @@ -5831,12 +5838,6 @@ "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", "dev": true }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", - "dev": true - }, "pretty-bytes": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz", @@ -5950,31 +5951,6 @@ "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", "dev": true }, - "randomatic": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz", - "integrity": "sha512-VdxFOIEY3mNO5PtSRkkle/hPJDHvQhK21oa73K4yAc9qmp6N429gAyF1gZMOTMeS0/AYzaV/2Trcef+NaIonSA==", - "dev": true, - "requires": { - "is-number": "^4.0.0", - "kind-of": "^6.0.0", - "math-random": "^1.0.1" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, "randombytes": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", @@ -6086,15 +6062,6 @@ "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", "dev": true }, - "regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", - "dev": true, - "requires": { - "is-equal-shallow": "^0.1.3" - } - }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -6279,6 +6246,12 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true }, + "rfdc": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.2.tgz", + "integrity": "sha512-92ktAgvZhBzYTIK0Mja9uen5q5J3NRVMoDkJL2VMwq6SXjVCgqvQeVP2XAaUY6HT+XpQYeLSjb3UoitBryKmdA==", + "dev": true + }, "right-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", @@ -6585,143 +6558,62 @@ } }, "socket.io": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.7.3.tgz", - "integrity": "sha1-uK+cq6AJSeVo42nxMn6pvp6iRhs=", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz", + "integrity": "sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==", "dev": true, "requires": { - "debug": "2.3.3", - "engine.io": "1.8.3", - "has-binary": "0.1.7", - "object-assign": "4.1.0", - "socket.io-adapter": "0.5.0", - "socket.io-client": "1.7.3", - "socket.io-parser": "2.3.1" - }, - "dependencies": { - "debug": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", - "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", - "dev": true, - "requires": { - "ms": "0.7.2" - } - }, - "ms": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", - "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", - "dev": true - }, - "object-assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", - "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=", - "dev": true - } + "debug": "~3.1.0", + "engine.io": "~3.2.0", + "has-binary2": "~1.0.2", + "socket.io-adapter": "~1.1.0", + "socket.io-client": "2.1.1", + "socket.io-parser": "~3.2.0" } }, "socket.io-adapter": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz", - "integrity": "sha1-y21LuL7IHhB4uZZ3+c7QBGBmu4s=", - "dev": true, - "requires": { - "debug": "2.3.3", - "socket.io-parser": "2.3.1" - }, - "dependencies": { - "debug": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", - "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", - "dev": true, - "requires": { - "ms": "0.7.2" - } - }, - "ms": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", - "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", - "dev": true - } - } + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", + "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=", + "dev": true }, "socket.io-client": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.7.3.tgz", - "integrity": "sha1-sw6GqhDV7zVGYBwJzeR2Xjgdo3c=", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz", + "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==", "dev": true, "requires": { "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", "component-bind": "1.0.0", "component-emitter": "1.2.1", - "debug": "2.3.3", - "engine.io-client": "1.8.3", - "has-binary": "0.1.7", + "debug": "~3.1.0", + "engine.io-client": "~3.2.0", + "has-binary2": "~1.0.2", + "has-cors": "1.1.0", "indexof": "0.0.1", "object-component": "0.0.3", + "parseqs": "0.0.5", "parseuri": "0.0.5", - "socket.io-parser": "2.3.1", + "socket.io-parser": "~3.2.0", "to-array": "0.1.4" - }, - "dependencies": { - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "debug": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", - "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=", - "dev": true, - "requires": { - "ms": "0.7.2" - } - }, - "ms": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", - "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", - "dev": true - } } }, "socket.io-parser": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.3.1.tgz", - "integrity": "sha1-3VMgJRA85Clpcya+/WQAX8/ltKA=", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", + "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", "dev": true, "requires": { - "component-emitter": "1.1.2", - "debug": "2.2.0", - "isarray": "0.0.1", - "json3": "3.3.2" + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "isarray": "2.0.1" }, "dependencies": { - "debug": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", - "dev": true, - "requires": { - "ms": "0.7.1" - } - }, "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "ms": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", - "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", "dev": true } } @@ -7189,12 +7081,12 @@ } }, "tmp": { - "version": "0.0.31", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz", - "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=", + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "dev": true, "requires": { - "os-tmpdir": "~1.0.1" + "os-tmpdir": "~1.0.2" } }, "to-array": { @@ -7500,9 +7392,9 @@ "optional": true }, "ultron": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", - "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", "dev": true }, "undefsafe": { @@ -7739,13 +7631,21 @@ "dev": true }, "useragent": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", - "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.2.1.tgz", + "integrity": "sha1-z1k+9PLRdYdei7ZY6pLhik/QbY4=", "dev": true, "requires": { - "lru-cache": "4.1.x", + "lru-cache": "2.2.x", "tmp": "0.0.x" + }, + "dependencies": { + "lru-cache": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.2.4.tgz", + "integrity": "sha1-bGWGGb7PFAMdDQtZSxYELOTcBj0=", + "dev": true + } } }, "util": { @@ -7913,21 +7813,16 @@ } }, "ws": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.2.tgz", - "integrity": "sha1-iiRPoFJAHgjJiGz0SoUYnh/UBn8=", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", "dev": true, "requires": { - "options": ">=0.0.5", - "ultron": "1.0.x" + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0", + "ultron": "~1.1.0" } }, - "wtf-8": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wtf-8/-/wtf-8-1.0.0.tgz", - "integrity": "sha1-OS2LotDxw00e4tYw8V0O+2jhBIo=", - "dev": true - }, "xdg-basedir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", @@ -7935,9 +7830,9 @@ "dev": true }, "xmlhttprequest-ssl": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz", - "integrity": "sha1-GFqIjATspGw+QHDZn3tJ3jUomS0=", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=", "dev": true }, "xtend": { diff --git a/package.json b/package.json index 1ca6ee9ca0..be1e6aa5ab 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "devDependencies": { "@types/form-data": "^2.2.1", - "@types/jasmine": "^2.8.2", + "@types/jasmine": "^2.8.8", "@types/lowdb": "^1.0.1", "@types/lunr": "^2.1.6", "@types/node": "8.0.19", @@ -35,21 +35,21 @@ "@types/webcrypto": "0.0.28", "concurrently": "3.5.1", "electron": "2.0.7", - "jasmine": "^3.1.0", - "jasmine-core": "^2.8.0", + "jasmine": "^3.2.0", + "jasmine-core": "^3.2.1", "jasmine-spec-reporter": "^4.2.1", "jasmine-ts-console-reporter": "^3.1.1", - "karma": "^1.7.1", + "karma": "^3.0.0", "karma-chrome-launcher": "^2.2.0", "karma-cli": "^1.0.1", - "karma-coverage-istanbul-reporter": "^1.3.0", + "karma-coverage-istanbul-reporter": "^2.0.1", "karma-detect-browsers": "^2.3.2", "karma-edge-launcher": "^0.4.2", "karma-firefox-launcher": "^1.1.0", - "karma-jasmine": "^1.1.0", - "karma-jasmine-html-reporter": "^0.2.2", + "karma-jasmine": "^1.1.2", + "karma-jasmine-html-reporter": "^1.3.1", "karma-safari-launcher": "^1.0.0", - "karma-typescript": "^3.0.8", + "karma-typescript": "^3.0.13", "nodemon": "^1.17.3", "rimraf": "^2.6.2", "tslint": "^5.8.0", From 46e35a9b8420330fc50424af7ab8589ffa5d2af2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 21 Aug 2018 23:10:12 -0400 Subject: [PATCH 0508/1626] delete cipher on sync if it 404s --- src/services/sync.service.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index 6a1edf3c32..e440b86784 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -177,7 +177,13 @@ export class SyncService implements SyncServiceAbstraction { return this.syncCompleted(true); } } - } catch { } + } catch (e) { + if (e != null && e.statusCode === 404 && isEdit) { + await this.cipherService.delete(notification.id); + this.messagingService.send('syncedDeletedCipher', { cipherId: notification.id }); + return this.syncCompleted(true); + } + } } return this.syncCompleted(false); } From 2bc7c42733a4b969b6aaee10d74814f40ff8e9a1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 22 Aug 2018 08:37:52 -0400 Subject: [PATCH 0509/1626] catch security errors on ie when reading hostname --- src/misc/utils.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 3e3f3c0b43..ca134770b1 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -145,12 +145,20 @@ export class Utils { static getHostname(uriString: string): string { const url = Utils.getUrl(uriString); - return url != null ? url.hostname : null; + try { + return url != null ? url.hostname : null; + } catch { + return null; + } } static getHost(uriString: string): string { const url = Utils.getUrl(uriString); - return url != null ? url.host : null; + try { + return url != null ? url.host : null; + } catch { + return null; + } } static getQueryParams(uriString: string): Map { From a67ea2422f082c6b884a3c7187e17a318048f7f5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 22 Aug 2018 08:53:52 -0400 Subject: [PATCH 0510/1626] polyfill sha512 hmac on ie --- src/services/webCryptoFunction.service.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index aa8d3f47ae..1706603ef7 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -60,6 +60,15 @@ export class WebCryptoFunctionService implements CryptoFunctionService { } async hmac(value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512'): Promise { + if (this.isIE && algorithm === 'sha512') { + const hmac = (forge as any).hmac.create(); + const keyBytes = this.toByteString(key); + const valueBytes = this.toByteString(value); + hmac.start(algorithm, keyBytes); + hmac.update(valueBytes, 'raw'); + return Utils.fromByteStringToArray(hmac.digest().data).buffer; + } + const signingAlgorithm = { name: 'HMAC', hash: { name: this.toWebCryptoAlgorithm(algorithm) }, From ebf6aee542e0af1d601acebff7027b2319fd0d9c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 22 Aug 2018 13:46:35 -0400 Subject: [PATCH 0511/1626] try to reconnect when connection closed --- src/services/notifications.service.ts | 70 ++++++++++++++++++++------- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index be33b067f7..8e6fcddd16 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -19,25 +19,54 @@ import { export class NotificationsService implements NotificationsServiceAbstraction { private signalrConnection: signalR.HubConnection; private url: string; + private connected = false; + private inited = false; + private reconnectTimer: any = null; constructor(private userService: UserService, private tokenService: TokenService, private syncService: SyncService, private appIdService: AppIdService, private apiService: ApiService) { } async init(environmentService: EnvironmentService): Promise { + this.inited = false; this.url = 'https://notifications.bitwarden.com'; if (environmentService.notificationsUrl != null) { this.url = environmentService.notificationsUrl; } else if (environmentService.baseUrl != null) { this.url = environmentService.baseUrl + '/notifications'; } - this.reconnect(); + + if (this.signalrConnection != null) { + this.signalrConnection.off('ReceiveMessage'); + await this.signalrConnection.stop(); + this.connected = false; + this.signalrConnection = null; + } + + this.signalrConnection = new signalR.HubConnectionBuilder() + .withUrl(this.url + '/hub', { + accessTokenFactory: () => this.tokenService.getToken(), + }) + // .configureLogging(signalR.LogLevel.Information) + .build(); + + this.signalrConnection.on('ReceiveMessage', async (data: any) => { + await this.processNotification(new NotificationResponse(data)); + }); + this.signalrConnection.onclose(() => { + this.connected = false; + this.reconnect(); + }); + this.inited = true; + if (await this.userService.isAuthenticated()) { + await this.connect(); + } } async updateConnection(): Promise { try { if (await this.userService.isAuthenticated()) { - await this.signalrConnection.start(); + await this.connect(); } else { await this.signalrConnection.stop(); } @@ -79,30 +108,37 @@ export class NotificationsService implements NotificationsServiceAbstraction { case NotificationType.SyncOrgKeys: await this.apiService.refreshIdentityToken(); await this.syncService.fullSync(true); - // Now reconnect to join the new org groups - await this.reconnect(); + // Stop so a reconnect can be made + await this.signalrConnection.stop(); break; default: break; } } + private async connect() { + await this.signalrConnection.start(); + this.connected = true; + } + private async reconnect() { - if (this.signalrConnection != null) { - await this.signalrConnection.stop(); - this.signalrConnection = null; + if (this.reconnectTimer != null) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + const authed = await this.userService.isAuthenticated(); + if (this.connected || !this.inited || !authed) { + return; } - this.signalrConnection = new signalR.HubConnectionBuilder() - .withUrl(this.url + '/hub', { - accessTokenFactory: () => this.tokenService.getToken(), - }) - // .configureLogging(signalR.LogLevel.Information) - .build(); + try { + await this.connect(); + } catch { } - this.signalrConnection.on('ReceiveMessage', async (data: any) => { - await this.processNotification(new NotificationResponse(data)); - }); - await this.updateConnection(); + if (!this.connected) { + this.reconnectTimer = setTimeout(() => { + this.reconnect(); + }, 120000); + } } } From 74b31daf1463502f72fa311d91ea68b11e109423 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 22 Aug 2018 13:48:51 -0400 Subject: [PATCH 0512/1626] short arrow functions --- src/services/notifications.service.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index 8e6fcddd16..fe5bf96c38 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -50,9 +50,8 @@ export class NotificationsService implements NotificationsServiceAbstraction { // .configureLogging(signalR.LogLevel.Information) .build(); - this.signalrConnection.on('ReceiveMessage', async (data: any) => { - await this.processNotification(new NotificationResponse(data)); - }); + this.signalrConnection.on('ReceiveMessage', + (data: any) => this.processNotification(new NotificationResponse(data))); this.signalrConnection.onclose(() => { this.connected = false; this.reconnect(); @@ -136,9 +135,7 @@ export class NotificationsService implements NotificationsServiceAbstraction { } catch { } if (!this.connected) { - this.reconnectTimer = setTimeout(() => { - this.reconnect(); - }, 120000); + this.reconnectTimer = setTimeout(() => this.reconnect(), 120000); } } } From d37fa836da523ae69413881f4af929c6808a688b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 22 Aug 2018 21:09:58 -0400 Subject: [PATCH 0513/1626] activity connect/disconnect events --- src/abstractions/notifications.service.ts | 2 ++ src/services/notifications.service.ts | 22 +++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/abstractions/notifications.service.ts b/src/abstractions/notifications.service.ts index 540b9885fc..1908c436bc 100644 --- a/src/abstractions/notifications.service.ts +++ b/src/abstractions/notifications.service.ts @@ -3,4 +3,6 @@ import { EnvironmentService } from './environment.service'; export abstract class NotificationsService { init: (environmentService: EnvironmentService) => Promise; updateConnection: () => Promise; + reconnectFromActivity: () => Promise; + disconnectFromInactivity: () => Promise; } diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index fe5bf96c38..f9af37ed40 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -21,6 +21,7 @@ export class NotificationsService implements NotificationsServiceAbstraction { private url: string; private connected = false; private inited = false; + private inactive = false; private reconnectTimer: any = null; constructor(private userService: UserService, private tokenService: TokenService, @@ -75,6 +76,22 @@ export class NotificationsService implements NotificationsServiceAbstraction { } } + async reconnectFromActivity(): Promise { + this.inactive = false; + if (!this.connected) { + if (await this.userService.isAuthenticated()) { + return this.reconnect().then(() => this.syncService.fullSync(false)); + } + } + } + + async disconnectFromInactivity(): Promise { + this.inactive = true; + if (this.connected) { + await this.signalrConnection.stop(); + } + } + private async processNotification(notification: NotificationResponse) { const appId = await this.appIdService.getAppId(); if (notification == null || notification.contextId === appId) { @@ -125,8 +142,11 @@ export class NotificationsService implements NotificationsServiceAbstraction { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } + if (this.connected || !this.inited || this.inactive) { + return; + } const authed = await this.userService.isAuthenticated(); - if (this.connected || !this.inited || !authed) { + if (!authed) { return; } From a360cd8e61d9228b14de86ca5469c694492c4a87 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 22 Aug 2018 21:46:34 -0400 Subject: [PATCH 0514/1626] refresh notification token if needed. authed and unlocked required --- src/abstractions/api.service.ts | 1 + src/abstractions/notifications.service.ts | 4 +-- src/services/api.service.ts | 23 ++++++++------- src/services/notifications.service.ts | 34 ++++++++++++++--------- 4 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 3ee640286a..3c8775eaf8 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -234,5 +234,6 @@ export abstract class ApiService { getUserPublicKey: (id: string) => Promise; + getActiveBearerToken: () => Promise; fetch: (request: Request) => Promise; } diff --git a/src/abstractions/notifications.service.ts b/src/abstractions/notifications.service.ts index 1908c436bc..e89896550d 100644 --- a/src/abstractions/notifications.service.ts +++ b/src/abstractions/notifications.service.ts @@ -3,6 +3,6 @@ import { EnvironmentService } from './environment.service'; export abstract class NotificationsService { init: (environmentService: EnvironmentService) => Promise; updateConnection: () => Promise; - reconnectFromActivity: () => Promise; - disconnectFromInactivity: () => Promise; + reconnectFromActivity: () => Promise; + disconnectFromInactivity: () => Promise; } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 20d28c4241..8d3669213b 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -776,6 +776,15 @@ export class ApiService implements ApiServiceAbstraction { // Helpers + async getActiveBearerToken(): Promise { + let accessToken = await this.tokenService.getToken(); + if (this.tokenService.tokenNeedsRefresh()) { + const tokenResponse = await this.doRefreshToken(); + accessToken = tokenResponse.accessToken; + } + return accessToken; + } + fetch(request: Request): Promise { if (request.method === 'GET') { request.headers.set('Cache-Control', 'no-cache'); @@ -797,8 +806,8 @@ export class ApiService implements ApiServiceAbstraction { }; if (authed) { - const authHeader = await this.handleTokenState(); - headers.set('Authorization', authHeader); + const authHeader = await this.getActiveBearerToken(); + headers.set('Authorization', 'Bearer ' + authHeader); } if (body != null) { if (typeof body === 'string') { @@ -844,16 +853,6 @@ export class ApiService implements ApiServiceAbstraction { return new ErrorResponse(responseJson, response.status, tokenError); } - private async handleTokenState(): Promise { - let accessToken = await this.tokenService.getToken(); - if (this.tokenService.tokenNeedsRefresh()) { - const tokenResponse = await this.doRefreshToken(); - accessToken = tokenResponse.accessToken; - } - - return 'Bearer ' + accessToken; - } - private async doRefreshToken(): Promise { const refreshToken = await this.tokenService.getRefreshToken(); if (refreshToken == null || refreshToken === '') { diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index f9af37ed40..896e7b9d3a 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -4,10 +4,10 @@ import { NotificationType } from '../enums/notificationType'; import { ApiService } from '../abstractions/api.service'; import { AppIdService } from '../abstractions/appId.service'; +import { CryptoService } from '../abstractions/crypto.service'; import { EnvironmentService } from '../abstractions/environment.service'; import { NotificationsService as NotificationsServiceAbstraction } from '../abstractions/notifications.service'; import { SyncService } from '../abstractions/sync.service'; -import { TokenService } from '../abstractions/token.service'; import { UserService } from '../abstractions/user.service'; import { @@ -24,9 +24,9 @@ export class NotificationsService implements NotificationsServiceAbstraction { private inactive = false; private reconnectTimer: any = null; - constructor(private userService: UserService, private tokenService: TokenService, - private syncService: SyncService, private appIdService: AppIdService, - private apiService: ApiService) { } + constructor(private userService: UserService, private syncService: SyncService, + private appIdService: AppIdService, private apiService: ApiService, + private cryptoService: CryptoService) { } async init(environmentService: EnvironmentService): Promise { this.inited = false; @@ -46,7 +46,7 @@ export class NotificationsService implements NotificationsServiceAbstraction { this.signalrConnection = new signalR.HubConnectionBuilder() .withUrl(this.url + '/hub', { - accessTokenFactory: () => this.tokenService.getToken(), + accessTokenFactory: () => this.apiService.getActiveBearerToken(), }) // .configureLogging(signalR.LogLevel.Information) .build(); @@ -58,14 +58,14 @@ export class NotificationsService implements NotificationsServiceAbstraction { this.reconnect(); }); this.inited = true; - if (await this.userService.isAuthenticated()) { + if (await this.isAuthedAndUnlocked()) { await this.connect(); } } async updateConnection(): Promise { try { - if (await this.userService.isAuthenticated()) { + if (await this.isAuthedAndUnlocked()) { await this.connect(); } else { await this.signalrConnection.stop(); @@ -76,16 +76,17 @@ export class NotificationsService implements NotificationsServiceAbstraction { } } - async reconnectFromActivity(): Promise { + async reconnectFromActivity(): Promise { this.inactive = false; if (!this.connected) { - if (await this.userService.isAuthenticated()) { - return this.reconnect().then(() => this.syncService.fullSync(false)); + if (await this.isAuthedAndUnlocked()) { + await this.reconnect(); + await this.syncService.fullSync(false); } } } - async disconnectFromInactivity(): Promise { + async disconnectFromInactivity(): Promise { this.inactive = true; if (this.connected) { await this.signalrConnection.stop(); @@ -145,8 +146,8 @@ export class NotificationsService implements NotificationsServiceAbstraction { if (this.connected || !this.inited || this.inactive) { return; } - const authed = await this.userService.isAuthenticated(); - if (!authed) { + const authedAndUnlocked = await this.isAuthedAndUnlocked(); + if (!authedAndUnlocked) { return; } @@ -158,4 +159,11 @@ export class NotificationsService implements NotificationsServiceAbstraction { this.reconnectTimer = setTimeout(() => this.reconnect(), 120000); } } + + private async isAuthedAndUnlocked() { + if (await this.userService.isAuthenticated()) { + return this.cryptoService.hasKey(); + } + return false; + } } From 3d02a1ecb8cdb945f3bee2fbb6921b40286575a1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 22 Aug 2018 22:35:18 -0400 Subject: [PATCH 0515/1626] make filter public --- src/angular/components/ciphers.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/ciphers.component.ts b/src/angular/components/ciphers.component.ts index a7968e7b5c..bd9c07b0d8 100644 --- a/src/angular/components/ciphers.component.ts +++ b/src/angular/components/ciphers.component.ts @@ -19,8 +19,8 @@ export class CiphersComponent { ciphers: CipherView[] = []; searchText: string; searchPlaceholder: string = null; + filter: (cipher: CipherView) => boolean = null; - protected filter: (cipher: CipherView) => boolean = null; protected searchPending = false; private searchTimeout: any = null; From db1a632c652cffe78346c4f8dbc6f361dbc9ee1d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 23 Aug 2018 08:56:23 -0400 Subject: [PATCH 0516/1626] sync on reconnect --- src/abstractions/notifications.service.ts | 2 +- src/services/notifications.service.ts | 34 ++++++++++++----------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/abstractions/notifications.service.ts b/src/abstractions/notifications.service.ts index e89896550d..91db76bdd6 100644 --- a/src/abstractions/notifications.service.ts +++ b/src/abstractions/notifications.service.ts @@ -2,7 +2,7 @@ import { EnvironmentService } from './environment.service'; export abstract class NotificationsService { init: (environmentService: EnvironmentService) => Promise; - updateConnection: () => Promise; + updateConnection: (sync?: boolean) => Promise; reconnectFromActivity: () => Promise; disconnectFromInactivity: () => Promise; } diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index 896e7b9d3a..5071b94ccd 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -55,18 +55,18 @@ export class NotificationsService implements NotificationsServiceAbstraction { (data: any) => this.processNotification(new NotificationResponse(data))); this.signalrConnection.onclose(() => { this.connected = false; - this.reconnect(); + this.reconnect(true); }); this.inited = true; if (await this.isAuthedAndUnlocked()) { - await this.connect(); + await this.reconnect(false); } } - async updateConnection(): Promise { + async updateConnection(sync = false): Promise { try { if (await this.isAuthedAndUnlocked()) { - await this.connect(); + await this.reconnect(sync); } else { await this.signalrConnection.stop(); } @@ -77,16 +77,19 @@ export class NotificationsService implements NotificationsServiceAbstraction { } async reconnectFromActivity(): Promise { + if (!this.inited) { + return; + } this.inactive = false; if (!this.connected) { - if (await this.isAuthedAndUnlocked()) { - await this.reconnect(); - await this.syncService.fullSync(false); - } + await this.reconnect(true); } } async disconnectFromInactivity(): Promise { + if (!this.inited) { + return; + } this.inactive = true; if (this.connected) { await this.signalrConnection.stop(); @@ -133,12 +136,7 @@ export class NotificationsService implements NotificationsServiceAbstraction { } } - private async connect() { - await this.signalrConnection.start(); - this.connected = true; - } - - private async reconnect() { + private async reconnect(sync: boolean) { if (this.reconnectTimer != null) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; @@ -152,11 +150,15 @@ export class NotificationsService implements NotificationsServiceAbstraction { } try { - await this.connect(); + await this.signalrConnection.start(); + this.connected = true; + if (sync) { + await this.syncService.fullSync(false); + } } catch { } if (!this.connected) { - this.reconnectTimer = setTimeout(() => this.reconnect(), 120000); + this.reconnectTimer = setTimeout(() => this.reconnect(sync), 120000); } } From 5d95fc733c7b5aa922c38b5e4d92b1a755526719 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 23 Aug 2018 08:56:27 -0400 Subject: [PATCH 0517/1626] null checks --- src/models/domain/cipher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/domain/cipher.ts b/src/models/domain/cipher.ts index 1c3040edc8..1bbdd7675d 100644 --- a/src/models/domain/cipher.ts +++ b/src/models/domain/cipher.ts @@ -178,7 +178,7 @@ export class Cipher extends Domain { c.edit = this.edit; c.organizationUseTotp = this.organizationUseTotp; c.favorite = this.favorite; - c.revisionDate = this.revisionDate.toISOString(); + c.revisionDate = this.revisionDate != null ? this.revisionDate.toISOString() : null; c.type = this.type; c.collectionIds = this.collectionIds; From 2dc77b6143f59d45fef762a64c353db14d12bb1d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 23 Aug 2018 09:25:39 -0400 Subject: [PATCH 0518/1626] disable notifications server url with https://- --- src/services/notifications.service.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index 5071b94ccd..3c395904b4 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -37,6 +37,12 @@ export class NotificationsService implements NotificationsServiceAbstraction { this.url = environmentService.baseUrl + '/notifications'; } + // Set notifications server URL to `https://-` to effectively disable communication + // with the notifications server from the client app + if (this.url === 'https://-') { + return; + } + if (this.signalrConnection != null) { this.signalrConnection.off('ReceiveMessage'); await this.signalrConnection.stop(); @@ -64,6 +70,9 @@ export class NotificationsService implements NotificationsServiceAbstraction { } async updateConnection(sync = false): Promise { + if (!this.inited) { + return; + } try { if (await this.isAuthedAndUnlocked()) { await this.reconnect(sync); From bc198c60d1916600f3bfb6ec82dd900dc6bd0642 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 23 Aug 2018 21:43:40 -0400 Subject: [PATCH 0519/1626] message pack protocol for signalr --- package-lock.json | 59 ++++++++++++++------- package.json | 3 +- src/models/response/notificationResponse.ts | 20 +++---- src/services/notifications.service.ts | 4 +- 4 files changed, 54 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1af87fc835..49ae4dd155 100644 --- a/package-lock.json +++ b/package-lock.json @@ -85,9 +85,17 @@ } }, "@aspnet/signalr": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@aspnet/signalr/-/signalr-1.0.2.tgz", - "integrity": "sha512-sXleqUCCbodCOqUA8MjLSvtAgDTvDhEq6j3JyAq/w4RMJhpZ+dXK9+6xEMbzag2hisq5e/8vDC82JYutkcOISQ==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@aspnet/signalr/-/signalr-1.0.3.tgz", + "integrity": "sha512-8nPSarp4k+oP2M6P7tw2FZMXOMR86wH9GPb/4wiqA18c4Ds88SUmE0pSpnNQPDOoWGMj6y9F2Xz5JyoynCPXWQ==" + }, + "@aspnet/signalr-protocol-msgpack": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@aspnet/signalr-protocol-msgpack/-/signalr-protocol-msgpack-1.0.3.tgz", + "integrity": "sha512-R5CRpxXGICi0Tgbd86uA8bC75Rm0y7Yp6oD87INfKxsav/m/xbz33rkGOI1OZIfsc/J6/WU5z0Bqpc2QgYfaJQ==", + "requires": { + "msgpack5": "^4.0.2" + } }, "@types/form-data": { "version": "2.2.1", @@ -2730,14 +2738,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2752,20 +2758,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -2882,8 +2885,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -2895,7 +2897,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2910,7 +2911,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3029,8 +3029,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -3042,7 +3041,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -3164,7 +3162,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4821,6 +4818,28 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "msgpack5": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/msgpack5/-/msgpack5-4.2.0.tgz", + "integrity": "sha512-tQkRlwO4f3/E8Kq5qm6PcVw+J+K4+U/XNqeD9Ebo1qVsrjkcKb2FfmdtuuIslw42CGT+K3ZVKAvKfSPp3QRplQ==", + "requires": { + "bl": "^2.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.3.6", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bl": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.0.1.tgz", + "integrity": "sha512-FrMgLukB9jujvJ92p5TA0hcKIHtInVXXhxD7qgAuV7k0cbPt9USZmOYnhDXH6IsnGeIUglX42TSBV7Gn4q5sbQ==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + } + } + }, "nan": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", diff --git a/package.json b/package.json index be1e6aa5ab..97b1a7f307 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,8 @@ "@angular/platform-browser-dynamic": "5.2.0", "@angular/router": "5.2.0", "@angular/upgrade": "5.2.0", - "@aspnet/signalr": "1.0.2", + "@aspnet/signalr": "1.0.3", + "@aspnet/signalr-protocol-msgpack": "1.0.3", "angular2-toaster": "4.0.2", "angulartics2": "5.0.1", "core-js": "2.4.1", diff --git a/src/models/response/notificationResponse.ts b/src/models/response/notificationResponse.ts index 7d84c79ce5..f796bff63e 100644 --- a/src/models/response/notificationResponse.ts +++ b/src/models/response/notificationResponse.ts @@ -42,11 +42,11 @@ export class SyncCipherNotification { revisionDate: Date; constructor(response: any) { - this.id = response.Id; - this.userId = response.UserId; - this.organizationId = response.OrganizationId; - this.collectionIds = response.CollectionIds; - this.revisionDate = new Date(response.RevisionDate); + this.id = response.id || response.Id; + this.userId = response.userId || response.UserId; + this.organizationId = response.organizationId || response.OrganizationId; + this.collectionIds = response.collectionIds || response.CollectionIds; + this.revisionDate = new Date(response.revisionDate || response.RevisionDate); } } @@ -56,9 +56,9 @@ export class SyncFolderNotification { revisionDate: Date; constructor(response: any) { - this.id = response.Id; - this.userId = response.UserId; - this.revisionDate = new Date(response.RevisionDate); + this.id = response.id || response.Id; + this.userId = response.userId || response.UserId; + this.revisionDate = new Date(response.revisionDate || response.RevisionDate); } } @@ -67,7 +67,7 @@ export class SyncUserNotification { date: Date; constructor(response: any) { - this.userId = response.UserId; - this.date = new Date(response.Date); + this.userId = response.userId || response.UserId; + this.date = new Date(response.date || response.Date); } } diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index 3c395904b4..d2aad1b632 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -1,4 +1,5 @@ import * as signalR from '@aspnet/signalr'; +import * as signalRMsgPack from '@aspnet/signalr-protocol-msgpack'; import { NotificationType } from '../enums/notificationType'; @@ -54,7 +55,8 @@ export class NotificationsService implements NotificationsServiceAbstraction { .withUrl(this.url + '/hub', { accessTokenFactory: () => this.apiService.getActiveBearerToken(), }) - // .configureLogging(signalR.LogLevel.Information) + .withHubProtocol(new signalRMsgPack.MessagePackHubProtocol()) + // .configureLogging(signalR.LogLevel.Trace) .build(); this.signalrConnection.on('ReceiveMessage', From c0fd5f71f8e2cf36165c565b4967ac0e379f166f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 24 Aug 2018 15:21:28 -0400 Subject: [PATCH 0520/1626] always set inactive state first --- src/services/notifications.service.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index d2aad1b632..04be09e021 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -88,21 +88,15 @@ export class NotificationsService implements NotificationsServiceAbstraction { } async reconnectFromActivity(): Promise { - if (!this.inited) { - return; - } this.inactive = false; - if (!this.connected) { + if (this.inited && !this.connected) { await this.reconnect(true); } } async disconnectFromInactivity(): Promise { - if (!this.inited) { - return; - } this.inactive = true; - if (this.connected) { + if (this.inited && this.connected) { await this.signalrConnection.stop(); } } From 7d0583f47b5359037a7ef212a902a72fe4c48211 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 25 Aug 2018 08:30:06 -0400 Subject: [PATCH 0521/1626] ELECTRON_NO_UPDATER env variable to disable auto-updates --- src/electron/updater.main.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/electron/updater.main.ts b/src/electron/updater.main.ts index 012f169f49..5130159cf4 100644 --- a/src/electron/updater.main.ts +++ b/src/electron/updater.main.ts @@ -34,7 +34,8 @@ export class UpdaterMain { const linuxCanUpdate = process.platform === 'linux' && isAppImage(); const windowsCanUpdate = process.platform === 'win32' && !isWindowsStore() && !isWindowsPortable(); const macCanUpdate = process.platform === 'darwin' && !isMacAppStore(); - this.canUpdate = linuxCanUpdate || windowsCanUpdate || macCanUpdate; + this.canUpdate = process.env.ELECTRON_NO_UPDATER !== '1' && + (linuxCanUpdate || windowsCanUpdate || macCanUpdate); } async init() { From 6f43b73237fc64394441d5821bd88090b26cbe5b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 25 Aug 2018 08:47:38 -0400 Subject: [PATCH 0522/1626] don't re-throw exception on 2fa failure --- src/angular/components/two-factor.component.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index b638796908..705340681e 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -175,11 +175,9 @@ export class TwoFactorComponent implements OnInit, OnDestroy { } else { this.router.navigate([this.successRoute]); } - } catch (e) { + } catch { if (this.selectedProviderType === TwoFactorProviderType.U2f && this.u2f != null) { this.u2f.start(); - } else { - throw e; } } } From 1454aff46cbab6fb737faaeed5aeb886b46e06d1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 27 Aug 2018 16:47:19 -0400 Subject: [PATCH 0523/1626] enable key stretching on makeEncKey --- src/services/crypto.service.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 815a152e6d..21e9d07852 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -319,9 +319,6 @@ export class CryptoService implements CryptoServiceAbstraction { async makeEncKey(key: SymmetricCryptoKey): Promise<[SymmetricCryptoKey, CipherString]> { const encKey = await this.cryptoFunctionService.randomBytes(64); let encKeyEnc: CipherString = null; - // TODO: Uncomment when we're ready to enable key stretching - encKeyEnc = await this.encrypt(encKey, key); - /* if (key.key.byteLength === 32) { const newKey = await this.stretchKey(key); encKeyEnc = await this.encrypt(encKey, newKey); @@ -330,7 +327,6 @@ export class CryptoService implements CryptoServiceAbstraction { } else { throw new Error('Invalid key size.'); } - */ return [new SymmetricCryptoKey(encKey), encKeyEnc]; } From 00562d083b687fb566b09c0df8a05e90b86f9e5a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 27 Aug 2018 19:06:36 -0400 Subject: [PATCH 0524/1626] remakeEncKey --- src/abstractions/crypto.service.ts | 1 + src/services/crypto.service.ts | 30 ++++++++++++++++++++---------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 8181ccf587..e333e5791b 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -32,6 +32,7 @@ export abstract class CryptoService { makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, CipherString]>; hashPassword: (password: string, key: SymmetricCryptoKey) => Promise; makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; + remakeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]> encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise; encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise; rsaEncrypt: (data: ArrayBuffer, publicKey?: ArrayBuffer, key?: SymmetricCryptoKey) => Promise; diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 21e9d07852..dc0890eb31 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -318,16 +318,12 @@ export class CryptoService implements CryptoServiceAbstraction { async makeEncKey(key: SymmetricCryptoKey): Promise<[SymmetricCryptoKey, CipherString]> { const encKey = await this.cryptoFunctionService.randomBytes(64); - let encKeyEnc: CipherString = null; - if (key.key.byteLength === 32) { - const newKey = await this.stretchKey(key); - encKeyEnc = await this.encrypt(encKey, newKey); - } else if (key.key.byteLength === 64) { - encKeyEnc = await this.encrypt(encKey, key); - } else { - throw new Error('Invalid key size.'); - } - return [new SymmetricCryptoKey(encKey), encKeyEnc]; + return this.buildEncKey(key, encKey); + } + + async remakeEncKey(key: SymmetricCryptoKey): Promise<[SymmetricCryptoKey, CipherString]> { + const encKey = await this.getEncKey(); + return this.buildEncKey(key, encKey.key); } async encrypt(plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey): Promise { @@ -677,4 +673,18 @@ export class CryptoService implements CryptoServiceAbstraction { } return okm; } + + private async buildEncKey(key: SymmetricCryptoKey, encKey: ArrayBuffer = null) + : Promise<[SymmetricCryptoKey, CipherString]> { + let encKeyEnc: CipherString = null; + if (key.key.byteLength === 32) { + const newKey = await this.stretchKey(key); + encKeyEnc = await this.encrypt(encKey, newKey); + } else if (key.key.byteLength === 64) { + encKeyEnc = await this.encrypt(encKey, key); + } else { + throw new Error('Invalid key size.'); + } + return [new SymmetricCryptoKey(encKey), encKeyEnc]; + } } From ae54094fcdea3aa6466eb744f4656eb7455d71c9 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 27 Aug 2018 19:55:05 -0400 Subject: [PATCH 0525/1626] kdf apis --- src/abstractions/api.service.ts | 2 ++ src/enums/kdfType.ts | 2 +- src/models/request/kdfRequest.ts | 8 ++++++++ src/services/api.service.ts | 5 +++++ 4 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 src/models/request/kdfRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 3c8775eaf8..81ba242d77 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -16,6 +16,7 @@ import { ImportCiphersRequest } from '../models/request/importCiphersRequest'; import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest'; import { KeysRequest } from '../models/request/keysRequest'; +import { KdfRequest } from '../models/request/kdfRequest'; import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest'; import { OrganizationUpdateRequest } from '../models/request/organizationUpdateRequest'; import { OrganizationUserAcceptRequest } from '../models/request/organizationUserAcceptRequest'; @@ -116,6 +117,7 @@ export abstract class ApiService { postAccountVerifyEmailToken: (request: VerifyEmailRequest) => Promise; postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise; postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise; + postAccountKdf: (request: KdfRequest) => Promise; getFolder: (id: string) => Promise; postFolder: (request: FolderRequest) => Promise; diff --git a/src/enums/kdfType.ts b/src/enums/kdfType.ts index 53eb59c4ce..b23ef8e6da 100644 --- a/src/enums/kdfType.ts +++ b/src/enums/kdfType.ts @@ -1,3 +1,3 @@ export enum KdfType { - PBKDF2 = 0, + PBKDF2_SHA256 = 0, } diff --git a/src/models/request/kdfRequest.ts b/src/models/request/kdfRequest.ts new file mode 100644 index 0000000000..996aab0fa1 --- /dev/null +++ b/src/models/request/kdfRequest.ts @@ -0,0 +1,8 @@ +import { PasswordRequest } from './passwordRequest'; + +import { KdfType } from '../../enums/kdfType'; + +export class KdfRequest extends PasswordRequest { + kdf: KdfType; + kdfIterations: number; +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 8d3669213b..a5291f9e91 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -21,6 +21,7 @@ import { GroupRequest } from '../models/request/groupRequest'; import { ImportCiphersRequest } from '../models/request/importCiphersRequest'; import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest'; +import { KdfRequest } from '../models/request/kdfRequest'; import { KeysRequest } from '../models/request/keysRequest'; import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest'; import { OrganizationUpdateRequest } from '../models/request/organizationUpdateRequest'; @@ -284,6 +285,10 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/accounts/delete-recover-token', request, false, false); } + postAccountKdf(request: KdfRequest): Promise { + return this.send('POST', '/accounts/kdf', request, true, false); + } + // Folder APIs async getFolder(id: string): Promise { From 13e0cf0c47430ea009c9cebd41854e51a36f88d6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 27 Aug 2018 19:58:49 -0400 Subject: [PATCH 0526/1626] rename all to PBKDF2_SHA256 --- src/angular/components/register.component.ts | 2 +- src/services/crypto.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index 75df514fe4..d8aedf0e0b 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -59,7 +59,7 @@ export class RegisterComponent { this.name = this.name === '' ? null : this.name; this.email = this.email.toLowerCase(); - const kdf = KdfType.PBKDF2; + const kdf = KdfType.PBKDF2_SHA256; const kdfIterations = 5000; const key = await this.cryptoService.makeKey(this.masterPassword, this.email, kdf, kdfIterations); const encKey = await this.cryptoService.makeEncKey(key); diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index dc0890eb31..c66db4c364 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -276,7 +276,7 @@ export class CryptoService implements CryptoServiceAbstraction { async makeKey(password: string, salt: string, kdf: KdfType, kdfIterations: number): Promise { let key: ArrayBuffer = null; - if (kdf == null || kdf === KdfType.PBKDF2) { + if (kdf == null || kdf === KdfType.PBKDF2_SHA256) { if (kdfIterations == null) { kdfIterations = 5000; } else if (kdfIterations < 5000) { From 0d15ae8615771842ec18567da001cf6e8b40e3e1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 27 Aug 2018 20:00:41 -0400 Subject: [PATCH 0527/1626] lint fixes --- src/abstractions/api.service.ts | 2 +- src/abstractions/crypto.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 81ba242d77..04fd907edf 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -15,8 +15,8 @@ import { GroupRequest } from '../models/request/groupRequest'; import { ImportCiphersRequest } from '../models/request/importCiphersRequest'; import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest'; -import { KeysRequest } from '../models/request/keysRequest'; import { KdfRequest } from '../models/request/kdfRequest'; +import { KeysRequest } from '../models/request/keysRequest'; import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest'; import { OrganizationUpdateRequest } from '../models/request/organizationUpdateRequest'; import { OrganizationUserAcceptRequest } from '../models/request/organizationUserAcceptRequest'; diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index e333e5791b..d8adfca748 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -32,7 +32,7 @@ export abstract class CryptoService { makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, CipherString]>; hashPassword: (password: string, key: SymmetricCryptoKey) => Promise; makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; - remakeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]> + remakeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise; encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise; rsaEncrypt: (data: ArrayBuffer, publicKey?: ArrayBuffer, key?: SymmetricCryptoKey) => Promise; From 0f68f0507e691d1f0fdc42ee858ac4f4eaf233fe Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 27 Aug 2018 22:54:51 -0400 Subject: [PATCH 0528/1626] use string mode --- src/misc/nodeUtils.ts | 2 +- src/services/lowdbStorage.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/misc/nodeUtils.ts b/src/misc/nodeUtils.ts index e4a79276ca..d42db57a82 100644 --- a/src/misc/nodeUtils.ts +++ b/src/misc/nodeUtils.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; export class NodeUtils { - static mkdirpSync(targetDir: string, mode = 755, relative = false, relativeDir: string = null) { + static mkdirpSync(targetDir: string, mode = '755', relative = false, relativeDir: string = null) { const initialDir = path.isAbsolute(targetDir) ? path.sep : ''; const baseDir = relative ? (relativeDir != null ? relativeDir : __dirname) : '.'; targetDir.split(path.sep).reduce((parentDir, childDir) => { diff --git a/src/services/lowdbStorage.service.ts b/src/services/lowdbStorage.service.ts index 90943b34ff..1f6861a5af 100644 --- a/src/services/lowdbStorage.service.ts +++ b/src/services/lowdbStorage.service.ts @@ -18,7 +18,7 @@ export class LowdbStorageService implements StorageService { let adapter: lowdb.AdapterSync; if (Utils.isNode && dir != null) { if (!fs.existsSync(dir)) { - NodeUtils.mkdirpSync(dir, 755); + NodeUtils.mkdirpSync(dir, '755'); } const p = path.join(dir, 'data.json'); adapter = new FileSync(p); From 3c43265878892adab985f6fc9b1b4270ae27aaa2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 27 Aug 2018 22:59:50 -0400 Subject: [PATCH 0529/1626] change to 700 perms --- src/misc/nodeUtils.ts | 2 +- src/services/lowdbStorage.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/misc/nodeUtils.ts b/src/misc/nodeUtils.ts index d42db57a82..ee12c74459 100644 --- a/src/misc/nodeUtils.ts +++ b/src/misc/nodeUtils.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; export class NodeUtils { - static mkdirpSync(targetDir: string, mode = '755', relative = false, relativeDir: string = null) { + static mkdirpSync(targetDir: string, mode = '700', relative = false, relativeDir: string = null) { const initialDir = path.isAbsolute(targetDir) ? path.sep : ''; const baseDir = relative ? (relativeDir != null ? relativeDir : __dirname) : '.'; targetDir.split(path.sep).reduce((parentDir, childDir) => { diff --git a/src/services/lowdbStorage.service.ts b/src/services/lowdbStorage.service.ts index 1f6861a5af..581c48291a 100644 --- a/src/services/lowdbStorage.service.ts +++ b/src/services/lowdbStorage.service.ts @@ -18,7 +18,7 @@ export class LowdbStorageService implements StorageService { let adapter: lowdb.AdapterSync; if (Utils.isNode && dir != null) { if (!fs.existsSync(dir)) { - NodeUtils.mkdirpSync(dir, '755'); + NodeUtils.mkdirpSync(dir, '700'); } const p = path.join(dir, 'data.json'); adapter = new FileSync(p); From 45da8aa9eb4dd7e12c9fa67ed09189bc4d5ed2f1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 28 Aug 2018 08:38:19 -0400 Subject: [PATCH 0530/1626] support for logout notification --- src/enums/notificationType.ts | 2 ++ src/services/notifications.service.ts | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/enums/notificationType.ts b/src/enums/notificationType.ts index 2ceddadd5d..4b655404e7 100644 --- a/src/enums/notificationType.ts +++ b/src/enums/notificationType.ts @@ -11,4 +11,6 @@ export enum NotificationType { SyncFolderUpdate = 8, SyncCipherDelete = 9, SyncSettings = 10, + + LogOut = 11, } diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index 04be09e021..ff832c4136 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -27,7 +27,7 @@ export class NotificationsService implements NotificationsServiceAbstraction { constructor(private userService: UserService, private syncService: SyncService, private appIdService: AppIdService, private apiService: ApiService, - private cryptoService: CryptoService) { } + private cryptoService: CryptoService, private logoutCallback: () => Promise) { } async init(environmentService: EnvironmentService): Promise { this.inited = false; @@ -136,6 +136,9 @@ export class NotificationsService implements NotificationsServiceAbstraction { // Stop so a reconnect can be made await this.signalrConnection.stop(); break; + case NotificationType.LogOut: + this.logoutCallback(); + break; default: break; } From a72843af3ea1e745c2f56ba0fabf425389d486cf Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 28 Aug 2018 08:47:06 -0400 Subject: [PATCH 0531/1626] ensure that message is for proper logged in user --- src/models/response/notificationResponse.ts | 5 +++-- src/services/notifications.service.ts | 25 ++++++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/models/response/notificationResponse.ts b/src/models/response/notificationResponse.ts index f796bff63e..b768a0c6c0 100644 --- a/src/models/response/notificationResponse.ts +++ b/src/models/response/notificationResponse.ts @@ -26,7 +26,8 @@ export class NotificationResponse { case NotificationType.SyncCiphers: case NotificationType.SyncOrgKeys: case NotificationType.SyncSettings: - this.payload = new SyncUserNotification(payload); + case NotificationType.LogOut: + this.payload = new UserNotification(payload); break; default: break; @@ -62,7 +63,7 @@ export class SyncFolderNotification { } } -export class SyncUserNotification { +export class UserNotification { userId: string; date: Date; diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index ff832c4136..d6f2300b6c 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -107,6 +107,13 @@ export class NotificationsService implements NotificationsServiceAbstraction { return; } + const isAuthenticated = await this.userService.isAuthenticated(); + const payloadUserId = notification.payload.userId || notification.payload.UserId; + const myUserId = await this.userService.getUserId(); + if (isAuthenticated && payloadUserId != null && payloadUserId !== myUserId) { + return; + } + switch (notification.type) { case NotificationType.SyncCipherCreate: case NotificationType.SyncCipherUpdate: @@ -128,16 +135,22 @@ export class NotificationsService implements NotificationsServiceAbstraction { case NotificationType.SyncVault: case NotificationType.SyncCiphers: case NotificationType.SyncSettings: - await this.syncService.fullSync(false); + if (isAuthenticated) { + await this.syncService.fullSync(false); + } break; case NotificationType.SyncOrgKeys: - await this.apiService.refreshIdentityToken(); - await this.syncService.fullSync(true); - // Stop so a reconnect can be made - await this.signalrConnection.stop(); + if (isAuthenticated) { + await this.apiService.refreshIdentityToken(); + await this.syncService.fullSync(true); + // Stop so a reconnect can be made + await this.signalrConnection.stop(); + } break; case NotificationType.LogOut: - this.logoutCallback(); + if (isAuthenticated) { + this.logoutCallback(); + } break; default: break; From 81c21418ec965221b4d322008f9da0ab7b9037d0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 28 Aug 2018 23:17:30 -0400 Subject: [PATCH 0532/1626] user canAccessPremium checks --- src/abstractions/user.service.ts | 1 + src/angular/components/view.component.ts | 7 +++++-- src/services/user.service.ts | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/abstractions/user.service.ts b/src/abstractions/user.service.ts index ab2239a845..29eae7b08c 100644 --- a/src/abstractions/user.service.ts +++ b/src/abstractions/user.service.ts @@ -13,6 +13,7 @@ export abstract class UserService { getKdfIterations: () => Promise; clear: () => Promise; isAuthenticated: () => Promise; + canAccessPremium: () => Promise; getOrganization: (id: string) => Promise; getAllOrganizations: () => Promise; replaceOrganizations: (organizations: { [id: string]: OrganizationData; }) => Promise; diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index 17866be231..fbb1b16053 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -21,6 +21,7 @@ import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { TokenService } from '../../abstractions/token.service'; import { TotpService } from '../../abstractions/totp.service'; +import { UserService } from '../../abstractions/user.service'; import { AttachmentView } from '../../models/view/attachmentView'; import { CipherView } from '../../models/view/cipherView'; @@ -38,6 +39,7 @@ export class ViewComponent implements OnDestroy, OnInit { showPassword: boolean; showCardCode: boolean; isPremium: boolean; + canAccessPremium: boolean; totpCode: string; totpCodeFormatted: string; totpDash: number; @@ -54,7 +56,7 @@ export class ViewComponent implements OnDestroy, OnInit { protected i18nService: I18nService, protected analytics: Angulartics2, protected auditService: AuditService, protected win: Window, protected broadcasterService: BroadcasterService, protected ngZone: NgZone, - protected changeDetectorRef: ChangeDetectorRef) { } + protected changeDetectorRef: ChangeDetectorRef, protected userService: UserService) { } ngOnInit() { this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { @@ -83,9 +85,10 @@ export class ViewComponent implements OnDestroy, OnInit { this.cipher = await cipher.decrypt(); this.isPremium = this.tokenService.getPremium(); + this.canAccessPremium = await this.userService.canAccessPremium(); if (this.cipher.type === CipherType.Login && this.cipher.login.totp && - (cipher.organizationUseTotp || this.isPremium)) { + (cipher.organizationUseTotp || this.canAccessPremium)) { await this.totpUpdateCode(); const interval = this.totpService.getTimeInterval(this.cipher.login.totp); await this.totpTick(interval); diff --git a/src/services/user.service.ts b/src/services/user.service.ts index df80de16a3..6d68bbd0be 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -116,6 +116,21 @@ export class UserService implements UserServiceAbstraction { return userId != null; } + async canAccessPremium(): Promise { + const tokenPremium = await this.tokenService.getPremium(); + if (tokenPremium) { + return true; + } + + const orgs = await this.getAllOrganizations(); + for (let i = 0; i < orgs.length; i++) { + if (orgs[i].usersGetPremium) { + return true; + } + } + return false; + } + async getOrganization(id: string): Promise { const userId = await this.getUserId(); const organizations = await this.storageService.get<{ [id: string]: OrganizationData; }>( From 42dbdb0043842810fb084370f886bd2ea9406b9f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 29 Aug 2018 09:21:27 -0400 Subject: [PATCH 0533/1626] allow attachments if can access premium --- src/angular/components/attachments.component.ts | 8 ++++---- src/angular/components/view.component.ts | 5 +---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/angular/components/attachments.component.ts b/src/angular/components/attachments.component.ts index c4b287c94c..b24c7fd11e 100644 --- a/src/angular/components/attachments.component.ts +++ b/src/angular/components/attachments.component.ts @@ -12,7 +12,7 @@ import { CipherService } from '../../abstractions/cipher.service'; import { CryptoService } from '../../abstractions/crypto.service'; import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; -import { TokenService } from '../../abstractions/token.service'; +import { UserService } from '../../abstractions/user.service'; import { Cipher } from '../../models/domain/cipher'; @@ -33,7 +33,7 @@ export class AttachmentsComponent implements OnInit { constructor(protected cipherService: CipherService, protected analytics: Angulartics2, protected toasterService: ToasterService, protected i18nService: I18nService, - protected cryptoService: CryptoService, protected tokenService: TokenService, + protected cryptoService: CryptoService, protected userService: UserService, protected platformUtilsService: PlatformUtilsService, protected win: Window) { } async ngOnInit() { @@ -41,8 +41,8 @@ export class AttachmentsComponent implements OnInit { this.cipher = await this.cipherDomain.decrypt(); this.hasUpdatedKey = await this.cryptoService.hasEncKey(); - const isPremium = this.tokenService.getPremium(); - this.canAccessAttachments = isPremium || this.cipher.organizationId != null; + const canAccessPremium = await this.userService.canAccessPremium(); + this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null; if (!this.canAccessAttachments) { const confirmed = await this.platformUtilsService.showDialog( diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index fbb1b16053..240384d8f5 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -38,7 +38,6 @@ export class ViewComponent implements OnDestroy, OnInit { cipher: CipherView; showPassword: boolean; showCardCode: boolean; - isPremium: boolean; canAccessPremium: boolean; totpCode: string; totpCodeFormatted: string; @@ -83,8 +82,6 @@ export class ViewComponent implements OnDestroy, OnInit { const cipher = await this.cipherService.get(this.cipherId); this.cipher = await cipher.decrypt(); - - this.isPremium = this.tokenService.getPremium(); this.canAccessPremium = await this.userService.canAccessPremium(); if (this.cipher.type === CipherType.Login && this.cipher.login.totp && @@ -161,7 +158,7 @@ export class ViewComponent implements OnDestroy, OnInit { return; } - if (this.cipher.organizationId == null && !this.isPremium) { + if (this.cipher.organizationId == null && !this.canAccessPremium) { this.toasterService.popAsync('error', this.i18nService.t('premiumRequired'), this.i18nService.t('premiumRequiredDesc')); return; From 5db115ae43018a34c87f00207498f2e768e4f9e5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 29 Aug 2018 09:35:10 -0400 Subject: [PATCH 0534/1626] check for null path --- src/electron/services/electronPlatformUtils.service.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index b39046e2b1..b4c0ed84a7 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -117,10 +117,12 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { remote.dialog.showSaveDialog(remote.getCurrentWindow(), { defaultPath: fileName, showsTagField: false, - }, (filename) => { - fs.writeFile(filename, Buffer.from(blobData), (err) => { - // error check? - }); + }, (path) => { + if (path != null) { + fs.writeFile(path, Buffer.from(blobData), (err) => { + // error check? + }); + } }); } From 82bf646a779f9075acdb801a582c80e8b3d43a40 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 29 Aug 2018 13:44:55 -0400 Subject: [PATCH 0535/1626] check enabled too --- src/services/user.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 6d68bbd0be..429c6f2169 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -124,7 +124,7 @@ export class UserService implements UserServiceAbstraction { const orgs = await this.getAllOrganizations(); for (let i = 0; i < orgs.length; i++) { - if (orgs[i].usersGetPremium) { + if (orgs[i].usersGetPremium && orgs[i].enabled) { return true; } } From 38c26d9649f613e4f8987e4e4727a0e90a21233b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 30 Aug 2018 21:47:41 -0400 Subject: [PATCH 0536/1626] Fix !== null checks --- src/models/request/tokenRequest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/request/tokenRequest.ts b/src/models/request/tokenRequest.ts index 85d0477b8c..c9bcd5ad36 100644 --- a/src/models/request/tokenRequest.ts +++ b/src/models/request/tokenRequest.ts @@ -37,7 +37,7 @@ export class TokenRequest { // obj.devicePushToken = this.device.pushToken; } - if (this.token && this.provider !== null && (typeof this.provider !== 'undefined')) { + if (this.token && this.provider != null) { obj.twoFactorToken = this.token; obj.twoFactorProvider = this.provider; obj.twoFactorRemember = this.remember ? '1' : '0'; From 5571fbe8bfc50d1ab56118607a9ad0d8d10d59d5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 31 Aug 2018 15:38:40 -0400 Subject: [PATCH 0537/1626] use currency pipe on premium price --- src/angular/components/premium.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/premium.component.ts b/src/angular/components/premium.component.ts index fb90fa50f2..4c3adeb536 100644 --- a/src/angular/components/premium.component.ts +++ b/src/angular/components/premium.component.ts @@ -10,7 +10,7 @@ import { TokenService } from '../../abstractions/token.service'; export class PremiumComponent implements OnInit { isPremium: boolean = false; - price: string = '$10'; + price: number = 10; refreshPromise: Promise; constructor(protected analytics: Angulartics2, protected toasterService: ToasterService, From 852b4571b34d60845f37956e9468f845d9dd627e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 31 Aug 2018 17:22:38 -0400 Subject: [PATCH 0538/1626] check last sync before revision date --- src/services/sync.service.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index e440b86784..405322228b 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -216,14 +216,16 @@ export class SyncService implements SyncServiceAbstraction { return [true, false]; } + const lastSync = await this.getLastSync(); + if (lastSync == null || lastSync.getTime() === 0) { + return [true, false]; + } + try { const response = await this.apiService.getAccountRevisionDate(); - const accountRevisionDate = new Date(response); - const lastSync = await this.getLastSync(); - if (lastSync != null && accountRevisionDate <= lastSync) { + if (new Date(response) <= lastSync) { return [false, false]; } - return [true, false]; } catch (e) { return [false, true]; From 26625a58d0831f023cf682b11116a02ed724d9f7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 31 Aug 2018 23:24:43 -0400 Subject: [PATCH 0539/1626] spread out reconnects between 2 and 5 min --- src/services/notifications.service.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index d6f2300b6c..9ad374bf65 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -179,7 +179,7 @@ export class NotificationsService implements NotificationsServiceAbstraction { } catch { } if (!this.connected) { - this.reconnectTimer = setTimeout(() => this.reconnect(sync), 120000); + this.reconnectTimer = setTimeout(() => this.reconnect(sync), this.random(120000, 300000)); } } @@ -189,4 +189,10 @@ export class NotificationsService implements NotificationsServiceAbstraction { } return false; } + + private random(min: number, max: number) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1)) + min; + } } From eb48abbcf659ae20b2d0627467bccb56c272132e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 3 Sep 2018 21:51:19 -0400 Subject: [PATCH 0540/1626] update lunr for leading wildcard searches --- package-lock.json | 6 ++-- package.json | 2 +- src/services/search.service.ts | 66 +++++++++++++++++----------------- 3 files changed, 36 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index 49ae4dd155..d310286166 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4595,9 +4595,9 @@ } }, "lunr": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.1.tgz", - "integrity": "sha1-ETYWorYC3cEJMqe/ik5uV+v+zfI=" + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.3.tgz", + "integrity": "sha512-rlAEsgU9Bnavca2w1WJ6+6cdeHMXNyadcersyk3ZpuhgWb5HBNj8l4WwJz9PjksAhYDlpQffCVXPctOn+wCIVA==" }, "make-dir": { "version": "1.3.0", diff --git a/package.json b/package.json index 97b1a7f307..7a35e61349 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "form-data": "2.3.2", "keytar": "4.2.1", "lowdb": "1.0.0", - "lunr": "2.3.1", + "lunr": "2.3.3", "node-fetch": "2.1.2", "node-forge": "0.7.1", "papaparse": "4.3.5", diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 59bcbe3f23..606375ce58 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -8,8 +8,7 @@ import { SearchService as SearchServiceAbstraction } from '../abstractions/searc import { DeviceType } from '../enums/deviceType'; import { FieldType } from '../enums/fieldType'; - -const IgnoredTlds = ['com', 'net', 'org', 'io', 'co', 'uk', 'au', 'nz', 'fr', 'de', 'eu', 'me', 'jp', 'cn']; +import { UriMatchType } from '../enums/uriMatchType'; export class SearchService implements SearchServiceAbstraction { private indexing = false; @@ -47,36 +46,7 @@ export class SearchService implements SearchServiceAbstraction { (builder as any).field('login.username', { extractor: (c: CipherView) => c.login != null ? c.login.username : null, }); - (builder as any).field('login.uris', { - boost: 2, - extractor: (c: CipherView) => c.login == null || !c.login.hasUris ? null : - c.login.uris.filter((u) => u.hostname != null).map((u) => u.hostname), - }); - (builder as any).field('login.uris_split', { - boost: 2, - extractor: (c: CipherView) => { - if (c.login == null || !c.login.hasUris) { - return null; - } - let uriParts: string[] = []; - c.login.uris.forEach((u) => { - if (u.hostname == null) { - return; - } - const parts = u.hostname.split('.'); - if (parts.length > 0 && parts.length <= 2) { - uriParts.push(parts[0]); - } else if (parts.length > 2) { - uriParts = uriParts.concat(parts.slice(0, parts.length - 2)); - const lastBit = parts[parts.length - 2]; - if (IgnoredTlds.indexOf(lastBit) === -1) { - uriParts.push(lastBit); - } - } - }); - return uriParts.length === 0 ? null : uriParts; - }, - }); + (builder as any).field('login.uris', { boost: 2, extractor: (c: CipherView) => this.uriExtractor(c) }); (builder as any).field('fields', { extractor: (c: CipherView) => this.fieldExtractor(c, false) }); (builder as any).field('fields_joined', { extractor: (c: CipherView) => this.fieldExtractor(c, true) }); (builder as any).field('attachments', { extractor: (c: CipherView) => this.attachmentExtractor(c, false) }); @@ -128,12 +98,11 @@ export class SearchService implements SearchServiceAbstraction { } catch { } } else { // tslint:disable-next-line - const soWild = lunr.Query.wildcard.TRAILING; + const soWild = lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING; searchResults = index.query((q) => { q.term(query, { fields: ['name'], wildcard: soWild }); q.term(query, { fields: ['subTitle'], wildcard: soWild }); q.term(query, { fields: ['login.uris'], wildcard: soWild }); - q.term(query, { fields: ['login.uris_split'], wildcard: soWild }); lunr.tokenizer(query).forEach((token) => { q.term(token.toString(), {}); }); @@ -216,4 +185,33 @@ export class SearchService implements SearchServiceAbstraction { } return joined ? attachments.join(' ') : attachments; } + + private uriExtractor(c: CipherView) { + if (c.login == null || !c.login.hasUris) { + return null; + } + const uris: string[] = []; + c.login.uris.forEach((u) => { + if (u.uri == null || u.uri === '') { + return; + } + if (u.hostname != null) { + uris.push(u.hostname); + return; + } + let uri = u.uri; + if (u.match !== UriMatchType.RegularExpression) { + const protocolIndex = uri.indexOf('://'); + if (protocolIndex > -1) { + uri = uri.substr(protocolIndex + 3); + } + const queryIndex = uri.search(/\?|&|#/); + if (queryIndex > -1) { + uri = uri.substring(0, queryIndex); + } + } + uris.push(uri); + }); + return uris.length > 0 ? uris : null; + } } From 773c51901fe77165c40cfb018bf5b99a740b5e4a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 8 Sep 2018 08:13:33 -0400 Subject: [PATCH 0541/1626] trim email also --- src/angular/components/register.component.ts | 2 +- src/services/auth.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index d8aedf0e0b..7ed96d4144 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -58,7 +58,7 @@ export class RegisterComponent { } this.name = this.name === '' ? null : this.name; - this.email = this.email.toLowerCase(); + this.email = this.email.trim().toLowerCase(); const kdf = KdfType.PBKDF2_SHA256; const kdfIterations = 5000; const key = await this.cryptoService.makeKey(this.masterPassword, this.email, kdf, kdfIterations); diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 5a4227f8b4..d1336ed421 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -199,7 +199,7 @@ export class AuthService { } async makePreloginKey(masterPassword: string, email: string): Promise { - email = email.toLowerCase(); + email = email.trim().toLowerCase(); this.kdf = null; this.kdfIterations = null; try { From 2d612b07de588a52e0e5895c4a4593f670442bb9 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 10 Sep 2018 08:22:10 -0400 Subject: [PATCH 0542/1626] no key safety check for orgs --- src/services/cipher.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 3ade53634c..c6c6a7aa2b 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -134,6 +134,9 @@ export class CipherService implements CipherServiceAbstraction { if (key == null && cipher.organizationId != null) { key = await this.cryptoService.getOrgKey(cipher.organizationId); + if (key == null) { + throw new Error('Cannot encrypt cipher for organization. No key.'); + } } await Promise.all([ this.encryptObjProperty(model, cipher, { From 3bc81ca4500de62a3f505e6fc6dcc7f38702698b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 10 Sep 2018 09:41:53 -0400 Subject: [PATCH 0543/1626] clear org key cache when setting --- src/services/crypto.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index c66db4c364..5f0e779629 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -76,6 +76,7 @@ export class CryptoService implements CryptoServiceAbstraction { orgKeys[org.id] = org.key; }); + this.orgKeys = null; return this.storageService.save(Keys.encOrgKeys, orgKeys); } From 4927d0d9077964ec1f047667ec3a2132af594527 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 10 Sep 2018 10:38:21 -0400 Subject: [PATCH 0544/1626] make enpass checks ignore case --- src/importers/enpassCsvImporter.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/importers/enpassCsvImporter.ts b/src/importers/enpassCsvImporter.ts index 5ce49d6d2e..bf5bd5fb57 100644 --- a/src/importers/enpassCsvImporter.ts +++ b/src/importers/enpassCsvImporter.ts @@ -20,7 +20,7 @@ export class EnpassCsvImporter extends BaseImporter implements Importer { let firstRow = true; results.forEach((value) => { - if (value.length < 2 || (firstRow && value[0] === 'Title')) { + if (value.length < 2 || (firstRow && (value[0] === 'Title' || value[0] === 'title'))) { firstRow = false; return; } @@ -29,14 +29,16 @@ export class EnpassCsvImporter extends BaseImporter implements Importer { cipher.notes = this.getValueOrDefault(value[value.length - 1]); cipher.name = this.getValueOrDefault(value[0], '--'); - if (value.length === 2 || (value.indexOf('Username') < 0 && value.indexOf('Password') < 0 && - value.indexOf('Email') && value.indexOf('URL') < 0)) { + if (value.length === 2 || (!this.containsField(value, 'username') && + !this.containsField(value, 'password') && !this.containsField(value, 'email') && + !this.containsField(value, 'url'))) { cipher.type = CipherType.SecureNote; cipher.secureNote = new SecureNoteView(); cipher.secureNote.type = SecureNoteType.Generic; } - if (value.indexOf('Cardholder') > -1 && value.indexOf('Number') > -1 && value.indexOf('Expiry date') > -1) { + if (this.containsField(value, 'cardholder') && this.containsField(value, 'number') && + this.containsField(value, 'expiry date')) { cipher.type = CipherType.Card; cipher.card = new CardView(); } @@ -115,4 +117,12 @@ export class EnpassCsvImporter extends BaseImporter implements Importer { result.success = true; return result; } + + private containsField(fields: any[], name: string) { + if (fields == null || name == null) { + return false; + } + return fields.filter((f) => !this.isNullOrWhitespace(f) && + f.toLowerCase() === name.toLowerCase()).length > 0; + } } From e240085351dbada5b1580fdadff95f269d885c40 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 10 Sep 2018 12:13:30 -0400 Subject: [PATCH 0545/1626] expose decrypt to bytes --- src/abstractions/crypto.service.ts | 1 + src/services/crypto.service.ts | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index d8adfca748..3dc13669fa 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -36,6 +36,7 @@ export abstract class CryptoService { encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise; encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise; rsaEncrypt: (data: ArrayBuffer, publicKey?: ArrayBuffer, key?: SymmetricCryptoKey) => Promise; + decryptToBytes: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise; decryptToUtf8: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise; decryptFromBytes: (encBuf: ArrayBuffer, key: SymmetricCryptoKey) => Promise; randomNumber: (min: number, max: number) => Promise; diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 5f0e779629..b6a9097884 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -120,10 +120,10 @@ export class CryptoService implements CryptoServiceAbstraction { let decEncKey: ArrayBuffer; const encKeyCipher = new CipherString(encKey); if (encKeyCipher.encryptionType === EncryptionType.AesCbc256_B64) { - decEncKey = await this.decrypt(encKeyCipher, key); + decEncKey = await this.decryptToBytes(encKeyCipher, key); } else if (encKeyCipher.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) { const newKey = await this.stretchKey(key); - decEncKey = await this.decrypt(encKeyCipher, newKey); + decEncKey = await this.decryptToBytes(encKeyCipher, newKey); } else { throw new Error('Unsupported encKey type.'); } @@ -159,7 +159,7 @@ export class CryptoService implements CryptoServiceAbstraction { return null; } - this.privateKey = await this.decrypt(new CipherString(encPrivateKey), null); + this.privateKey = await this.decryptToBytes(new CipherString(encPrivateKey), null); return this.privateKey; } @@ -383,7 +383,7 @@ export class CryptoService implements CryptoServiceAbstraction { return new CipherString(type, Utils.fromBufferToB64(encBytes), null, mac); } - async decrypt(cipherString: CipherString, key?: SymmetricCryptoKey): Promise { + async decryptToBytes(cipherString: CipherString, key?: SymmetricCryptoKey): Promise { const iv = Utils.fromB64ToArray(cipherString.iv).buffer; const data = Utils.fromB64ToArray(cipherString.data).buffer; const mac = cipherString.mac ? Utils.fromB64ToArray(cipherString.mac).buffer : null; From 46f9e17056e6248abc4601aede3c2a4c596382c9 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 10 Sep 2018 15:57:59 -0400 Subject: [PATCH 0546/1626] add nativescript support to utils --- src/misc/utils.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/misc/utils.ts b/src/misc/utils.ts index ca134770b1..6ff46f15cd 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -1,14 +1,15 @@ import { I18nService } from '../abstractions/i18n.service'; // tslint:disable-next-line -const nodeURL = typeof window === 'undefined' ? require('url').URL : null; +const nodeURL = typeof window === 'undefined' ? require('url') : null; export class Utils { static inited = false; + static isNativeScript = false; static isNode = false; static isBrowser = true; static isMobileBrowser = false; - static global: NodeJS.Global | Window = null; + static global: any = null; static init() { if (Utils.inited) { @@ -19,12 +20,13 @@ export class Utils { Utils.isNode = typeof process !== 'undefined' && (process as any).release != null && (process as any).release.name === 'node'; Utils.isBrowser = typeof window !== 'undefined'; + Utils.isNativeScript = !Utils.isNode && !Utils.isBrowser; Utils.isMobileBrowser = Utils.isBrowser && this.isMobile(window); - Utils.global = Utils.isNode && !Utils.isBrowser ? global : window; + Utils.global = Utils.isNativeScript ? new Object() : (Utils.isNode && !Utils.isBrowser ? global : window); } static fromB64ToArray(str: string): Uint8Array { - if (Utils.isNode) { + if (Utils.isNode || Utils.isNativeScript) { return new Uint8Array(Buffer.from(str, 'base64')); } else { const binaryString = window.atob(str); @@ -37,7 +39,7 @@ export class Utils { } static fromHexToArray(str: string): Uint8Array { - if (Utils.isNode) { + if (Utils.isNode || Utils.isNativeScript) { return new Uint8Array(Buffer.from(str, 'hex')); } else { const bytes = new Uint8Array(str.length / 2); @@ -49,7 +51,7 @@ export class Utils { } static fromUtf8ToArray(str: string): Uint8Array { - if (Utils.isNode) { + if (Utils.isNode || Utils.isNativeScript) { return new Uint8Array(Buffer.from(str, 'utf8')); } else { const strUtf8 = unescape(encodeURIComponent(str)); @@ -70,7 +72,7 @@ export class Utils { } static fromBufferToB64(buffer: ArrayBuffer): string { - if (Utils.isNode) { + if (Utils.isNode || Utils.isNativeScript) { return Buffer.from(buffer).toString('base64'); } else { let binary = ''; @@ -83,7 +85,7 @@ export class Utils { } static fromBufferToUtf8(buffer: ArrayBuffer): string { - if (Utils.isNode) { + if (Utils.isNode || Utils.isNativeScript) { return Buffer.from(buffer).toString('utf8'); } else { const bytes = new Uint8Array(buffer); @@ -98,7 +100,7 @@ export class Utils { // ref: https://stackoverflow.com/a/40031979/1090359 static fromBufferToHex(buffer: ArrayBuffer): string { - if (Utils.isNode) { + if (Utils.isNode || Utils.isNativeScript) { return Buffer.from(buffer).toString('hex'); } else { const bytes = new Uint8Array(buffer); @@ -125,7 +127,7 @@ export class Utils { } static fromB64ToUtf8(b64Str: string): string { - if (Utils.isNode) { + if (Utils.isNode || Utils.isNativeScript) { return Buffer.from(b64Str, 'base64').toString('utf8'); } else { return decodeURIComponent(escape(window.atob(b64Str))); @@ -225,7 +227,7 @@ export class Utils { try { if (nodeURL != null) { - return new nodeURL(uriString); + return nodeURL.URL ? new nodeURL.URL(uriString) : nodeURL.parse(uriString); } else if (typeof URL === 'function') { return new URL(uriString); } else if (window != null) { From ffa7b3549448e0f39b37b5df36e8cefc3200dff2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 10 Sep 2018 16:28:35 -0400 Subject: [PATCH 0547/1626] is old safari check on pbkdf2 --- src/services/webCryptoFunction.service.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index 1706603ef7..a7fc99638e 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -5,25 +5,31 @@ import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { Utils } from '../misc/utils'; -import { SymmetricCryptoKey } from '../models/domain'; import { DecryptParameters } from '../models/domain/decryptParameters'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; + +import { DeviceType } from '../enums/deviceType'; export class WebCryptoFunctionService implements CryptoFunctionService { private crypto: Crypto; private subtle: SubtleCrypto; private isEdge: boolean; private isIE: boolean; + private isOldSafari: boolean; constructor(private win: Window, private platformUtilsService: PlatformUtilsService) { this.crypto = typeof win.crypto !== 'undefined' ? win.crypto : null; this.subtle = (!!this.crypto && typeof win.crypto.subtle !== 'undefined') ? win.crypto.subtle : null; this.isEdge = platformUtilsService.isEdge(); this.isIE = platformUtilsService.isIE(); + const ua = win.navigator.userAgent; + this.isOldSafari = platformUtilsService.getDevice() === DeviceType.SafariBrowser && + (ua.indexOf(' Version/10.') > -1 || ua.indexOf(' Version/9.') > -1); } async pbkdf2(password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', iterations: number): Promise { - if (this.isEdge || this.isIE) { + if (this.isEdge || this.isIE || this.isOldSafari) { const forgeLen = algorithm === 'sha256' ? 32 : 64; const passwordBytes = this.toByteString(password); const saltBytes = this.toByteString(salt); From d0ad8650605ec506704ed76f13f40fd4d33cffcd Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 10 Sep 2018 16:44:50 -0400 Subject: [PATCH 0548/1626] add getDevice to mocked utils service --- spec/web/services/webCryptoFunction.service.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index f3ad003bf8..de4c8012fa 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -399,4 +399,5 @@ function makeStaticByteArray(length: number) { class PlatformUtilsServiceMock extends PlatformUtilsService { isEdge = () => false; isIE = () => false; + getDevice = () => super.getDevice(); } From 04f6b44d5414a1e1a35d019f58a5166c1a3bb01b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 11 Sep 2018 15:54:18 -0400 Subject: [PATCH 0549/1626] use regualar fetch for hibp apis --- src/services/audit.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/audit.service.ts b/src/services/audit.service.ts index 057be1646e..ff7a90b996 100644 --- a/src/services/audit.service.ts +++ b/src/services/audit.service.ts @@ -28,7 +28,7 @@ export class AuditService implements AuditServiceAbstraction { } async breachedAccounts(username: string): Promise { - const response = await this.apiService.fetch(new Request(HibpBreachApi + username)); + const response = await fetch(new Request(HibpBreachApi + username)); if (response.status === 404) { return []; } else if (response.status !== 200) { From b6ee31879f07a0115099001afd7e7ac367edad2c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 11 Sep 2018 17:29:47 -0400 Subject: [PATCH 0550/1626] update libs --- package-lock.json | 161 ++++++++++++++++++++++++---------------------- package.json | 40 ++++++------ 2 files changed, 103 insertions(+), 98 deletions(-) diff --git a/package-lock.json b/package-lock.json index d310286166..d2ab33408e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,83 +5,83 @@ "requires": true, "dependencies": { "@angular/animations": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-5.2.0.tgz", - "integrity": "sha512-JLR42YHiJppO4ruAkFxgbzghUDtHkXHkKPM8udd2qyt16T7e1OX7EEOrrmldUu59CC56tZnJ/32p4SrYmxyBSA==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-6.1.7.tgz", + "integrity": "sha512-bjX3VEVEh5scGDDmxEKPzYI8DWUbqOFA34aYDY2cHPnDkLM0I7pEtO44qb72FSbWwXn77sYlby/dx2gtRayOOA==", "requires": { - "tslib": "^1.7.1" + "tslib": "^1.9.0" } }, "@angular/common": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-5.2.0.tgz", - "integrity": "sha512-yMFn2isC7/XOs56/2Kzzbb1AASHiwipAPOVFtKe7TdZQClO8fJXwCnk326rzr615+CG0eSBNQWeiFGyWN2riBA==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-6.1.7.tgz", + "integrity": "sha512-zFK2xM0hqR2ZWIfUsn+06jg+0K5PolzTxPjfUtVQDCZo+JHHKTVHEwtfORUaMTMfH9EqKrvfB3t6fCwK0523ag==", "requires": { - "tslib": "^1.7.1" + "tslib": "^1.9.0" } }, "@angular/compiler": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-5.2.0.tgz", - "integrity": "sha512-RfYa4ESgjGX0T0ob/Xz00IF7nd2xZkoyRy6oKgL82q42uzB3xZUDMrFNgeGxAUs3H22IkL46/5SSPOMOTMZ0NA==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-6.1.7.tgz", + "integrity": "sha512-JKuK/fzKNCF+mNPmPmGQjr0uHVpfxmrOqXBriJMklCtdsKeQW94BLUoNjn8h1H10rFbUqYuD5v9AAKdH77FgnA==", "requires": { - "tslib": "^1.7.1" + "tslib": "^1.9.0" } }, "@angular/core": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-5.2.0.tgz", - "integrity": "sha512-s2ne45DguNUubhC1YgybGECC4Tyx3G4EZCntUiRMDWWkmKXSK+6dgHMesyDo8R5Oat8VfN4Anf8l3JHS1He8kg==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-6.1.7.tgz", + "integrity": "sha512-3MtS8EQy+saNcImDWghphOr/h3l5CpFnZW6aaHiL8T5CpTBNdB86uEmAwtiNQkJ0UeO+cztF1zNCzhm9R93/3w==", "requires": { - "tslib": "^1.7.1" + "tslib": "^1.9.0" } }, "@angular/forms": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-5.2.0.tgz", - "integrity": "sha512-g1/SF9lY0ZwzJ0w4NXbFsTGGEuUdgtaZny8DmkaqtmA7idby3FW398X0tv25KQfVYKtL+p9Jp1Y8EI0CvrIsvw==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-6.1.7.tgz", + "integrity": "sha512-McCElnn6Abr+HAjwxa1ldvIMs101TT0NGq8EHXLyF9QcKG24dU7425+MdLuW0OrtgBql2+RjlqnSiKuxDQHxJA==", "requires": { - "tslib": "^1.7.1" + "tslib": "^1.9.0" } }, "@angular/http": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@angular/http/-/http-5.2.0.tgz", - "integrity": "sha512-V5Cl24dP3rCXTTQvDc0TIKoWqBRAa0DWAQbtr7iuDAt5a1vPGdKz5K1sEiiV6ziwX6gzjiwHjUvL+B+WbIUrQA==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/@angular/http/-/http-6.1.7.tgz", + "integrity": "sha512-N0wXHpEL/CsNM4l44Z+dU51Y994mBEHjt9yb0SeKf02mdrsTJK+cEvfZ0JkVDjGddqdWHvWFn3zSmkR79qLrSQ==", "requires": { - "tslib": "^1.7.1" + "tslib": "^1.9.0" } }, "@angular/platform-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-5.2.0.tgz", - "integrity": "sha512-c6cR15MfopPwGZ097HdRuAi9+R9BhA3bRRFpP2HmrSSB/BW4ZNovUYwB2QUMSYbd9s0lYTtnavqGm6DKcyF2QA==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-6.1.7.tgz", + "integrity": "sha512-YOYg944aefCWElJhnma8X+3wJDb6nHf6aBAVN+YPg0bUplEFacR4y6PeM9QR8vjh5Y0kbGG9ZPGDT/WwP2t4sQ==", "requires": { - "tslib": "^1.7.1" + "tslib": "^1.9.0" } }, "@angular/platform-browser-dynamic": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-5.2.0.tgz", - "integrity": "sha512-xG1eNoi8sm4Jcly2y98r5mqYVe3XV8sUJCtOhvGBYtvt4dKEQ5tOns6fWQ0nUbl6Vv3Y0xgGUS1JCtfut3DuaQ==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-6.1.7.tgz", + "integrity": "sha512-sSF7n4SpwPiP1fMwocu/RUegpp/45jHK/+r9biXUXUBD12zO5QMcLHU393sjoNi7e6+meuXEH0pnWa66dTznjw==", "requires": { - "tslib": "^1.7.1" + "tslib": "^1.9.0" } }, "@angular/router": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-5.2.0.tgz", - "integrity": "sha512-VXDXtp2A1GQEUEhXg0ZzqHdTUERLgDSo3/Mmpzt+dgLMKlXDSCykcm4gINwE5VQLGD1zQvDFCCRv3seGRNfrqA==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-6.1.7.tgz", + "integrity": "sha512-YaOTq2icKAd9FDls2qo2Qp8FrmLGke3eA+bZ3FvOhFydxyUAvlU96N9Y9Gb05tXTtBaQNzAInov2bbp2YMFEFA==", "requires": { - "tslib": "^1.7.1" + "tslib": "^1.9.0" } }, "@angular/upgrade": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@angular/upgrade/-/upgrade-5.2.0.tgz", - "integrity": "sha512-ezWfhBCiP7RX+59scxfYfjDMRw+qq0BVbm/EfOXdYFU0NHWo7lXJ3v+cUi18G+5GVjzwRiJDIKWhw1QEyq2nug==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/@angular/upgrade/-/upgrade-6.1.7.tgz", + "integrity": "sha512-vPXK2lweiGXCigrels43QlEfMvvVnUcN0lhZq+Mjb3gmyEByRcsATH2DgxxTVIXj5zVytMOXR89lC/sSEaQHAA==", "requires": { - "tslib": "^1.7.1" + "tslib": "^1.9.0" } }, "@aspnet/signalr": { @@ -149,16 +149,22 @@ } }, "@types/node-forge": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.7.1.tgz", - "integrity": "sha512-sXCLq42I8Evd/qnrSluSKwxuBc2ioPvNCvb5hl+VL3d2zlh45n26b3rPf8DuJiAuJSv5Z5cqcF1KL7X77tXG4Q==", - "dev": true + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.7.5.tgz", + "integrity": "sha512-1J0FVHFUUOovMuCd2nLmyZVlG+LkX49B1MpA6DGWmTX/tTU2HRmE5yB82DCOuQBjIMCDQQHx5v+nqUt0cSfWAw==", + "dev": true, + "requires": { + "@types/node": "*" + } }, "@types/papaparse": { - "version": "4.1.31", - "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-4.1.31.tgz", - "integrity": "sha512-8+d1hk3GgF+NJ6mMZZ5zKimqIOc+8OTzpLw4RQ8wnS1NkJh/dMH3NEhSud4Ituq2SGXJjOG6wIczCBAKsSsBdQ==", - "dev": true + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-4.5.3.tgz", + "integrity": "sha512-xQtDs1aIh5NORAKmUI4ei+qLqHb1z1Zgq+GITrI2q7lWGU5pR55EEu8m0oGqIQKIwjlpXn7quqbyy2+PHjXBjA==", + "dev": true, + "requires": { + "@types/node": "*" + } }, "@types/webcrypto": { "version": "0.0.28", @@ -224,16 +230,16 @@ "dev": true }, "angular2-toaster": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/angular2-toaster/-/angular2-toaster-4.0.2.tgz", - "integrity": "sha512-/ndYYbV/7WZx6ujm6avFUqfb+FKbrx7oT+3mYj8i0o9N26Ug+BseFjy6oRnlVVedl39yRP6hhea81QgKmoYbbQ==" + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/angular2-toaster/-/angular2-toaster-6.1.0.tgz", + "integrity": "sha512-++CtP7Xx3zNG1U+1nlop8rfA0nqkKqEhKGCtdcT2dB+CTQKkSOod5A6TMdTOZbdm/edaAzT8E0fHt9RrPRTDbA==" }, "angulartics2": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/angulartics2/-/angulartics2-5.0.1.tgz", - "integrity": "sha512-QYBp7km7xTf/57zKKnYreM0OQ1Pq0kd4L9HJTC79vy7+RG1XqrkA944jTGKDERLWtjEAlQuSyZMS9J5IZZ56sw==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/angulartics2/-/angulartics2-6.3.0.tgz", + "integrity": "sha512-5BwRYCLF6ypBl7rlarW403v4AdotIldJhN+2rQeTIw/rTtngJ4SewNhf4zlRnKBSItlhbZRrDJBl9uR2TUuCdw==", "requires": { - "tslib": "^1.7.1" + "tslib": "^1.9.0" } }, "ansi-align": { @@ -1588,9 +1594,9 @@ "dev": true }, "core-js": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", - "integrity": "sha1-TekR5mew6ukSTjQlS1OupvxhjT4=" + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", + "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" }, "core-util-is": { "version": "1.0.2", @@ -2738,12 +2744,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2763,7 +2771,8 @@ "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", @@ -2911,6 +2920,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4923,9 +4933,9 @@ "integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=" }, "node-forge": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz", - "integrity": "sha1-naYR6giYL0uUIGs760zJZl8gwwA=" + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", + "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==" }, "nodemon": { "version": "1.18.3", @@ -5612,9 +5622,9 @@ "dev": true }, "papaparse": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-4.3.5.tgz", - "integrity": "sha1-ts31yub+nsYDsb5m8RSmOsZFoDY=" + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-4.6.0.tgz", + "integrity": "sha512-ylm8pmgyz9rkS3Ng/ru5tHUF3JxWwKYP0aZZWZ8eCGdSxoqgYiDUXLNQei73mUJOjHw8QNu5ZNCsLoDpkMA6sg==" }, "parse-asn1": { "version": "5.1.1", @@ -6307,11 +6317,11 @@ "dev": true }, "rxjs": { - "version": "5.5.6", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.6.tgz", - "integrity": "sha512-v4Q5HDC0FHAQ7zcBX7T2IL6O5ltl1a2GX4ENjPXg6SjDY69Cmx9v4113C99a4wGF16ClPv5Z8mghuYorVkg/kg==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.2.tgz", + "integrity": "sha512-hV7criqbR0pe7EeL3O66UYVg92IR0XsA97+9y+BWTePK9SKmEI5Qd3Zj6uPnGkNzXsBywBQWTvujPl+1Kn9Zjw==", "requires": { - "symbol-observable": "1.0.1" + "tslib": "^1.9.0" } }, "safe-buffer": { @@ -6977,11 +6987,6 @@ "has-flag": "^1.0.0" } }, - "symbol-observable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", - "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" - }, "tar-fs": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", @@ -7903,9 +7908,9 @@ "dev": true }, "zone.js": { - "version": "0.8.19", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.19.tgz", - "integrity": "sha512-l9rofaOs6a4y1W8zt4pDmnCUCnYG377dG+5SZlXNWrTWYUuXFqcJZiOarhYiRVR0NI9sH/8ooPJiz4uprB/Mkg==" + "version": "0.8.26", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.26.tgz", + "integrity": "sha512-W9Nj+UmBJG251wkCacIkETgra4QgBo/vgoEkb4a2uoLzpQG7qF9nzwoLXWU5xj3Fg2mxGvEDh47mg24vXccYjA==" } } } diff --git a/package.json b/package.json index 7a35e61349..b337b920bb 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,8 @@ "@types/lunr": "^2.1.6", "@types/node": "8.0.19", "@types/node-fetch": "^1.6.9", - "@types/node-forge": "0.7.1", - "@types/papaparse": "4.1.31", + "@types/node-forge": "^0.7.5", + "@types/papaparse": "^4.5.3", "@types/webcrypto": "0.0.28", "concurrently": "3.5.1", "electron": "2.0.7", @@ -54,24 +54,24 @@ "rimraf": "^2.6.2", "tslint": "^5.8.0", "typemoq": "^2.1.0", - "typescript": "^2.7.1" + "typescript": "^2.7.2" }, "dependencies": { - "@angular/animations": "5.2.0", - "@angular/common": "5.2.0", - "@angular/compiler": "5.2.0", - "@angular/core": "5.2.0", - "@angular/forms": "5.2.0", - "@angular/http": "5.2.0", - "@angular/platform-browser": "5.2.0", - "@angular/platform-browser-dynamic": "5.2.0", - "@angular/router": "5.2.0", - "@angular/upgrade": "5.2.0", + "@angular/animations": "6.1.7", + "@angular/common": "6.1.7", + "@angular/compiler": "6.1.7", + "@angular/core": "6.1.7", + "@angular/forms": "6.1.7", + "@angular/http": "6.1.7", + "@angular/platform-browser": "6.1.7", + "@angular/platform-browser-dynamic": "6.1.7", + "@angular/router": "6.1.7", + "@angular/upgrade": "6.1.7", "@aspnet/signalr": "1.0.3", "@aspnet/signalr-protocol-msgpack": "1.0.3", - "angular2-toaster": "4.0.2", - "angulartics2": "5.0.1", - "core-js": "2.4.1", + "angular2-toaster": "6.1.0", + "angulartics2": "6.3.0", + "core-js": "2.5.7", "electron-log": "2.2.14", "electron-updater": "3.0.3", "form-data": "2.3.2", @@ -79,9 +79,9 @@ "lowdb": "1.0.0", "lunr": "2.3.3", "node-fetch": "2.1.2", - "node-forge": "0.7.1", - "papaparse": "4.3.5", - "rxjs": "5.5.6", - "zone.js": "0.8.19" + "node-forge": "0.7.6", + "papaparse": "4.6.0", + "rxjs": "6.3.2", + "zone.js": "0.8.26" } } From 930ee9f3c0e350fa3ffc7f623452ae7bb7898d3a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 11 Sep 2018 22:47:19 -0400 Subject: [PATCH 0551/1626] update node types --- package-lock.json | 20 ++++++++------------ package.json | 2 +- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index d2ab33408e..83fccdc2e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -134,9 +134,9 @@ "dev": true }, "@types/node": { - "version": "8.0.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.19.tgz", - "integrity": "sha512-VRQB+Q0L3YZWs45uRdpN9oWr82meL/8TrJ6faoKT5tp0uub2l/aRMhtm5fo68h7kjYKH60f9/bay1nF7ZpTW5g==", + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.9.4.tgz", + "integrity": "sha512-fCHV45gS+m3hH17zgkgADUSi2RR1Vht6wOZ0jyHP8rjiQra9f+mIcgwPQHllmDocYOstIEbKlxbFDYlgrTPYqw==", "dev": true }, "@types/node-fetch": { @@ -2744,14 +2744,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2771,8 +2769,7 @@ "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", @@ -2920,7 +2917,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2963,9 +2959,9 @@ "optional": true }, "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz", + "integrity": "sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw==", "dev": true, "optional": true }, diff --git a/package.json b/package.json index b337b920bb..f384f97838 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@types/jasmine": "^2.8.8", "@types/lowdb": "^1.0.1", "@types/lunr": "^2.1.6", - "@types/node": "8.0.19", + "@types/node": "^10.9.4", "@types/node-fetch": "^1.6.9", "@types/node-forge": "^0.7.5", "@types/papaparse": "^4.5.3", From 832babf704d590bca586454cd1a087f8c48503d6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 11 Sep 2018 23:13:04 -0400 Subject: [PATCH 0552/1626] update node-fetch --- package-lock.json | 12 ++++++------ package.json | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 83fccdc2e3..55c82348cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -140,9 +140,9 @@ "dev": true }, "@types/node-fetch": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-1.6.9.tgz", - "integrity": "sha512-n2r6WLoY7+uuPT7pnEtKJCmPUGyJ+cbyBR8Avnu4+m1nzz7DwBVuyIvvlBzCZ/nrpC7rIgb3D6pNavL7rFEa9g==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.1.2.tgz", + "integrity": "sha512-XroxUzLpKuL+CVkQqXlffRkEPi4Gh3Oui/mWyS7ztKiyqVxiU+h3imCW5I2NQmde5jK+3q++36/Q96cyRWsweg==", "dev": true, "requires": { "@types/node": "*" @@ -4924,9 +4924,9 @@ } }, "node-fetch": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz", - "integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.2.0.tgz", + "integrity": "sha512-OayFWziIxiHY8bCUyLX6sTpDH8Jsbp4FfYd1j1f7vZyfgkcOnAyM4oQR16f8a0s7Gl/viMGRey8eScYk4V4EZA==" }, "node-forge": { "version": "0.7.6", diff --git a/package.json b/package.json index f384f97838..61e070e276 100644 --- a/package.json +++ b/package.json @@ -26,10 +26,10 @@ "devDependencies": { "@types/form-data": "^2.2.1", "@types/jasmine": "^2.8.8", - "@types/lowdb": "^1.0.1", + "@types/lowdb": "^1.0.5", "@types/lunr": "^2.1.6", "@types/node": "^10.9.4", - "@types/node-fetch": "^1.6.9", + "@types/node-fetch": "^2.1.2", "@types/node-forge": "^0.7.5", "@types/papaparse": "^4.5.3", "@types/webcrypto": "0.0.28", @@ -78,7 +78,7 @@ "keytar": "4.2.1", "lowdb": "1.0.0", "lunr": "2.3.3", - "node-fetch": "2.1.2", + "node-fetch": "2.2.0", "node-forge": "0.7.6", "papaparse": "4.6.0", "rxjs": "6.3.2", From cf795bc39c729844c8c0900a3ec0befc95348de3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 12 Sep 2018 10:22:46 -0400 Subject: [PATCH 0553/1626] dont index asterisk on card last4 --- src/services/search.service.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 606375ce58..94518ca739 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -6,6 +6,7 @@ import { CipherService } from '../abstractions/cipher.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { SearchService as SearchServiceAbstraction } from '../abstractions/search.service'; +import { CipherType } from '../enums/cipherType'; import { DeviceType } from '../enums/deviceType'; import { FieldType } from '../enums/fieldType'; import { UriMatchType } from '../enums/uriMatchType'; @@ -41,7 +42,15 @@ export class SearchService implements SearchServiceAbstraction { builder.ref('id'); (builder as any).field('shortId', { boost: 100, extractor: (c: CipherView) => c.id.substr(0, 8) }); (builder as any).field('name', { boost: 10 }); - (builder as any).field('subTitle', { boost: 5 }); + (builder as any).field('subTitle', { + boost: 5, + extractor: (c: CipherView) => { + if (c.type === CipherType.Card && c.subTitle.indexOf('*') === 0) { + return c.subTitle.substr(1); + } + return c.subTitle; + }, + }); builder.field('notes'); (builder as any).field('login.username', { extractor: (c: CipherView) => c.login != null ? c.login.username : null, From 7dc14a0d18b09c05dfd2bc7f7791b68a3b204601 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 12 Sep 2018 10:27:21 -0400 Subject: [PATCH 0554/1626] strip asterisk from subtitle of cards --- src/services/search.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 94518ca739..60b72853ab 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -45,8 +45,8 @@ export class SearchService implements SearchServiceAbstraction { (builder as any).field('subTitle', { boost: 5, extractor: (c: CipherView) => { - if (c.type === CipherType.Card && c.subTitle.indexOf('*') === 0) { - return c.subTitle.substr(1); + if (c.subTitle != null && c.type === CipherType.Card) { + return c.subTitle.replace(/\*/g, ''); } return c.subTitle; }, From 2fadcb8d041983c0f5b13d77f45236ebdbcdfa63 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 14 Sep 2018 08:07:55 -0400 Subject: [PATCH 0555/1626] is safari check --- src/services/webCryptoFunction.service.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index a7fc99638e..fc37e3ae3e 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -8,8 +8,6 @@ import { Utils } from '../misc/utils'; import { DecryptParameters } from '../models/domain/decryptParameters'; import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; -import { DeviceType } from '../enums/deviceType'; - export class WebCryptoFunctionService implements CryptoFunctionService { private crypto: Crypto; private subtle: SubtleCrypto; @@ -23,7 +21,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { this.isEdge = platformUtilsService.isEdge(); this.isIE = platformUtilsService.isIE(); const ua = win.navigator.userAgent; - this.isOldSafari = platformUtilsService.getDevice() === DeviceType.SafariBrowser && + this.isOldSafari = platformUtilsService.isSafari() && (ua.indexOf(' Version/10.') > -1 || ua.indexOf(' Version/9.') > -1); } From d9808250c652bcbe539c73621fd53ce8616c5302 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 14 Sep 2018 09:22:35 -0400 Subject: [PATCH 0556/1626] lowercase fields --- src/services/search.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 60b72853ab..84e73bcaa4 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -40,9 +40,9 @@ export class SearchService implements SearchServiceAbstraction { this.index = null; const builder = new lunr.Builder(); builder.ref('id'); - (builder as any).field('shortId', { boost: 100, extractor: (c: CipherView) => c.id.substr(0, 8) }); + (builder as any).field('shortid', { boost: 100, extractor: (c: CipherView) => c.id.substr(0, 8) }); (builder as any).field('name', { boost: 10 }); - (builder as any).field('subTitle', { + (builder as any).field('subtitle', { boost: 5, extractor: (c: CipherView) => { if (c.subTitle != null && c.type === CipherType.Card) { @@ -110,7 +110,7 @@ export class SearchService implements SearchServiceAbstraction { const soWild = lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING; searchResults = index.query((q) => { q.term(query, { fields: ['name'], wildcard: soWild }); - q.term(query, { fields: ['subTitle'], wildcard: soWild }); + q.term(query, { fields: ['subtitle'], wildcard: soWild }); q.term(query, { fields: ['login.uris'], wildcard: soWild }); lunr.tokenizer(query).forEach((token) => { q.term(token.toString(), {}); From 85587e06728aabd118f3cb6b06110be92202049b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 14 Sep 2018 13:36:03 -0400 Subject: [PATCH 0557/1626] isSafari mock --- spec/web/services/webCryptoFunction.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index de4c8012fa..f5f0fb1940 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -399,5 +399,5 @@ function makeStaticByteArray(length: number) { class PlatformUtilsServiceMock extends PlatformUtilsService { isEdge = () => false; isIE = () => false; - getDevice = () => super.getDevice(); + isSafari = () => super.isSafari(); } From 64be82d5ccab9e24a27ea840acb4693967e3ed56 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 19 Sep 2018 09:57:17 -0400 Subject: [PATCH 0558/1626] remove unused import --- spec/web/services/webCryptoFunction.service.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index f5f0fb1940..2e31fa1525 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -1,7 +1,5 @@ import * as TypeMoq from 'typemoq'; -import { DeviceType } from '../../../src/enums/deviceType'; - import { PlatformUtilsService } from '../../../src/abstractions/platformUtils.service'; import { WebCryptoFunctionService } from '../../../src/services/webCryptoFunction.service'; From d81273c44f97ff709f21623c36b5a4661fea6627 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 21 Sep 2018 13:54:06 -0400 Subject: [PATCH 0559/1626] passpack importer --- src/importers/passpackCsvImporter.ts | 93 ++++++++++++++++++++++++++++ src/services/import.service.ts | 4 ++ 2 files changed, 97 insertions(+) create mode 100644 src/importers/passpackCsvImporter.ts diff --git a/src/importers/passpackCsvImporter.ts b/src/importers/passpackCsvImporter.ts new file mode 100644 index 0000000000..9b26d45c2b --- /dev/null +++ b/src/importers/passpackCsvImporter.ts @@ -0,0 +1,93 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CollectionView } from '../models/view/collectionView'; + +export class PasspackCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + const tagsJson = !this.isNullOrWhitespace(value.Tags) ? JSON.parse(value.Tags) : null; + const tags: string[] = tagsJson != null && tagsJson.tags != null && tagsJson.tags.length > 0 ? + tagsJson.tags.map((tagJson: string) => { + try { + const t = JSON.parse(tagJson); + return this.getValueOrDefault(t.tag); + } catch { } + return null; + }).filter((t: string) => !this.isNullOrWhitespace(t)) : null; + + if (this.organization && tags != null && tags.length > 0) { + tags.forEach((tag) => { + let addCollection = true; + let collectionIndex = result.collections.length; + + for (let i = 0; i < result.collections.length; i++) { + if (result.collections[i].name === tag) { + addCollection = false; + collectionIndex = i; + break; + } + } + + if (addCollection) { + const collection = new CollectionView(); + collection.name = tag; + result.collections.push(collection); + } + + result.collectionRelationships.push([result.ciphers.length, collectionIndex]); + }); + } else if (!this.organization && tags != null && tags.length > 0) { + this.processFolder(result, tags[0]); + } + + const cipher = this.initLoginCipher(); + cipher.notes = this.getValueOrDefault(value.Notes, ''); + cipher.notes += ('\n\n' + this.getValueOrDefault(value['Shared Notes'], '') + '\n'); + cipher.name = this.getValueOrDefault(value['Entry Name'], '--'); + cipher.login.username = this.getValueOrDefault(value['User ID']); + cipher.login.password = this.getValueOrDefault(value.Password); + cipher.login.uris = this.makeUriArray(value.URL); + + if (value.__parsed_extra != null && value.__parsed_extra.length > 0) { + value.__parsed_extra.forEach((extra: string) => { + if (!this.isNullOrWhitespace(extra)) { + cipher.notes += ('\n' + extra); + } + }); + } + + const fieldsJson = !this.isNullOrWhitespace(value['Extra Fields']) ? + JSON.parse(value['Extra Fields']) : null; + const fields = fieldsJson != null && fieldsJson.extraFields != null && + fieldsJson.extraFields.length > 0 ? fieldsJson.extraFields.map((fieldJson: string) => { + try { + return JSON.parse(fieldJson); + } catch { } + return null; + }) : null; + if (fields != null) { + fields.forEach((f: any) => { + if (f != null) { + this.processKvp(cipher, f.name, f.data); + } + }); + } + + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} diff --git a/src/services/import.service.ts b/src/services/import.service.ts index 40f744231e..f6548989e5 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -40,6 +40,7 @@ import { OnePassword1PifImporter } from '../importers/onepassword1PifImporter'; import { OnePasswordWinCsvImporter } from '../importers/onepasswordWinCsvImporter'; import { PadlockCsvImporter } from '../importers/padlockCsvImporter'; import { PassKeepCsvImporter } from '../importers/passkeepCsvImporter'; +import { PasspackCsvImporter } from '../importers/passpackCsvImporter'; import { PasswordAgentCsvImporter } from '../importers/passwordAgentCsvImporter'; import { PasswordBossJsonImporter } from '../importers/passwordBossJsonImporter'; import { PasswordDragonXmlImporter } from '../importers/passwordDragonXmlImporter'; @@ -93,6 +94,7 @@ export class ImportService implements ImportServiceAbstraction { { id: 'gnomejson', name: 'GNOME Passwords and Keys/Seahorse (json)' }, { id: 'blurcsv', name: 'Blur (csv)' }, { id: 'passwordagentcsv', name: 'Password Agent (csv)' }, + { id: 'passpackcsv', name: 'Passpack (csv)' }, ]; constructor(private cipherService: CipherService, private folderService: FolderService, @@ -207,6 +209,8 @@ export class ImportService implements ImportServiceAbstraction { return new GnomeJsonImporter(); case 'passwordagentcsv': return new PasswordAgentCsvImporter(); + case 'passpackcsv': + return new PasspackCsvImporter(); default: return null; } From d1847690f260cf06ace8ed98b7308edabc3b62c0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 25 Sep 2018 09:12:11 -0400 Subject: [PATCH 0560/1626] purge org vault apis --- src/abstractions/api.service.ts | 2 +- src/enums/eventType.ts | 2 +- src/services/api.service.ts | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 04fd907edf..d931034139 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -139,7 +139,7 @@ export abstract class ApiService { putShareCiphers: (request: CipherBulkShareRequest) => Promise; putCipherCollections: (id: string, request: CipherCollectionsRequest) => Promise; putCipherCollectionsAdmin: (id: string, request: CipherCollectionsRequest) => Promise; - postPurgeCiphers: (request: PasswordVerificationRequest) => Promise; + postPurgeCiphers: (request: PasswordVerificationRequest, organizationId?: string) => Promise; postImportCiphers: (request: ImportCiphersRequest) => Promise; postImportOrganizationCiphers: (organizationId: string, request: ImportOrganizationCiphersRequest) => Promise; diff --git a/src/enums/eventType.ts b/src/enums/eventType.ts index acaae31d4d..103a6b6873 100644 --- a/src/enums/eventType.ts +++ b/src/enums/eventType.ts @@ -30,5 +30,5 @@ export enum EventType { OrganizationUser_UpdatedGroups = 1504, Organization_Updated = 1600, - + Organization_PurgedVault = 1601, } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index a5291f9e91..5acfe6f99a 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -380,8 +380,12 @@ export class ApiService implements ApiServiceAbstraction { return this.send('PUT', '/ciphers/' + id + '/collections-admin', request, true, false); } - postPurgeCiphers(request: PasswordVerificationRequest): Promise { - return this.send('POST', '/ciphers/purge', request, true, false); + postPurgeCiphers(request: PasswordVerificationRequest, organizationId: string = null): Promise { + let path = '/ciphers/purge'; + if (organizationId != null) { + path += '?organizationId=' + organizationId; + } + return this.send('POST', path, request, true, false); } postImportCiphers(request: ImportCiphersRequest): Promise { From fc1bcb34a0164c9674370833aa09498d573fd121 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 27 Sep 2018 08:32:48 -0400 Subject: [PATCH 0561/1626] null check --- src/services/search.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 84e73bcaa4..ff9fa07789 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -17,7 +17,8 @@ export class SearchService implements SearchServiceAbstraction { private onlySearchName = false; constructor(private cipherService: CipherService, platformUtilsService: PlatformUtilsService) { - this.onlySearchName = platformUtilsService.getDevice() === DeviceType.EdgeExtension; + this.onlySearchName = platformUtilsService == null || + platformUtilsService.getDevice() === DeviceType.EdgeExtension; } clearIndex(): void { From c3a2d3a536c12f3648e8945d8cd4090c5ad33a65 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 2 Oct 2018 09:06:03 -0400 Subject: [PATCH 0562/1626] default iterations when registering is now 100k --- src/angular/components/register.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index 7ed96d4144..6a6dc96e6d 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -60,7 +60,7 @@ export class RegisterComponent { this.name = this.name === '' ? null : this.name; this.email = this.email.trim().toLowerCase(); const kdf = KdfType.PBKDF2_SHA256; - const kdfIterations = 5000; + const kdfIterations = 100000; const key = await this.cryptoService.makeKey(this.masterPassword, this.email, kdf, kdfIterations); const encKey = await this.cryptoService.makeEncKey(key); const hashedPassword = await this.cryptoService.hashPassword(this.masterPassword, key); From 45341ec408319cbf69a4772e503a3991ae770a49 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 2 Oct 2018 09:20:32 -0400 Subject: [PATCH 0563/1626] lower kdf iterations for edge/ie since they use less-performant polyfill --- src/angular/components/register.component.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index 6a6dc96e6d..f30e8bff55 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -10,6 +10,7 @@ import { ApiService } from '../../abstractions/api.service'; import { AuthService } from '../../abstractions/auth.service'; import { CryptoService } from '../../abstractions/crypto.service'; import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { StateService } from '../../abstractions/state.service'; import { KdfType } from '../../enums/kdfType'; @@ -28,7 +29,8 @@ export class RegisterComponent { constructor(protected authService: AuthService, protected router: Router, protected analytics: Angulartics2, protected toasterService: ToasterService, protected i18nService: I18nService, protected cryptoService: CryptoService, - protected apiService: ApiService, protected stateService: StateService) { } + protected apiService: ApiService, protected stateService: StateService, + protected platformUtilsService: PlatformUtilsService) { } async submit() { if (this.email == null || this.email === '') { @@ -60,7 +62,8 @@ export class RegisterComponent { this.name = this.name === '' ? null : this.name; this.email = this.email.trim().toLowerCase(); const kdf = KdfType.PBKDF2_SHA256; - const kdfIterations = 100000; + const useLowerKdf = this.platformUtilsService.isEdge() || this.platformUtilsService.isIE(); + const kdfIterations = useLowerKdf ? 10000 : 100000; const key = await this.cryptoService.makeKey(this.masterPassword, this.email, kdf, kdfIterations); const encKey = await this.cryptoService.makeEncKey(key); const hashedPassword = await this.cryptoService.hashPassword(this.masterPassword, key); From f793ff0aa57ef9ffcf09323d1f557c7c24509c1e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 2 Oct 2018 23:09:19 -0400 Subject: [PATCH 0564/1626] refactor toaster to platform showToast --- package-lock.json | 5 ---- package.json | 1 - src/abstractions/platformUtils.service.ts | 3 +- src/angular/components/add-edit.component.ts | 14 ++++----- .../components/attachments.component.ts | 19 ++++++------ .../components/environment.component.ts | 6 ++-- src/angular/components/export.component.ts | 7 ++--- .../components/folder-add-edit.component.ts | 10 +++---- src/angular/components/hint.component.ts | 12 ++++---- src/angular/components/lock.component.ts | 7 ++--- src/angular/components/login.component.ts | 10 +++---- .../password-generator-history.component.ts | 6 ++-- .../password-generator.component.ts | 6 ++-- .../components/password-history.component.ts | 6 ++-- src/angular/components/premium.component.ts | 5 ++-- src/angular/components/register.component.ts | 15 +++++----- .../two-factor-options.component.ts | 5 +--- .../components/two-factor.component.ts | 9 +++--- src/angular/components/view.component.ts | 16 +++++----- src/angular/services/validation.service.ts | 29 ++++--------------- .../services/electronPlatformUtils.service.ts | 11 +++---- 21 files changed, 85 insertions(+), 117 deletions(-) diff --git a/package-lock.json b/package-lock.json index 55c82348cc..4668dfc380 100644 --- a/package-lock.json +++ b/package-lock.json @@ -229,11 +229,6 @@ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", "dev": true }, - "angular2-toaster": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/angular2-toaster/-/angular2-toaster-6.1.0.tgz", - "integrity": "sha512-++CtP7Xx3zNG1U+1nlop8rfA0nqkKqEhKGCtdcT2dB+CTQKkSOod5A6TMdTOZbdm/edaAzT8E0fHt9RrPRTDbA==" - }, "angulartics2": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/angulartics2/-/angulartics2-6.3.0.tgz", diff --git a/package.json b/package.json index 61e070e276..09029ed887 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,6 @@ "@angular/upgrade": "6.1.7", "@aspnet/signalr": "1.0.3", "@aspnet/signalr-protocol-msgpack": "1.0.3", - "angular2-toaster": "6.1.0", "angulartics2": "6.3.0", "core-js": "2.5.7", "electron-log": "2.2.14", diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index debb19f6ae..f24b0f5706 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -21,7 +21,8 @@ export abstract class PlatformUtilsService { getApplicationVersion: () => string; supportsU2f: (win: Window) => boolean; supportsDuo: () => boolean; - showToast: (type: 'error' | 'success' | 'warning' | 'info', title: string, text: string) => void; + showToast: (type: 'error' | 'success' | 'warning' | 'info', title: string, text: string | string[], + options?: any) => void; showDialog: (text: string, title?: string, confirmText?: string, cancelText?: string, type?: string) => Promise; isDev: () => boolean; diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 0e9f2cdbbc..9642c8afb3 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -4,7 +4,6 @@ import { Output, } from '@angular/core'; -import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; import { CipherType } from '../../enums/cipherType'; @@ -61,7 +60,7 @@ export class AddEditComponent { constructor(protected cipherService: CipherService, protected folderService: FolderService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, - protected analytics: Angulartics2, protected toasterService: ToasterService, + protected analytics: Angulartics2, protected auditService: AuditService, protected stateService: StateService) { this.typeOptions = [ { name: i18nService.t('typeLogin'), value: CipherType.Login }, @@ -152,7 +151,7 @@ export class AddEditComponent { async submit(): Promise { if (this.cipher.name == null || this.cipher.name === '') { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('nameRequired')); return false; } @@ -169,7 +168,7 @@ export class AddEditComponent { await this.formPromise; this.cipher.id = cipher.id; this.analytics.eventTrack.next({ action: this.editMode ? 'Edited Cipher' : 'Added Cipher' }); - this.toasterService.popAsync('success', null, + this.platformUtilsService.showToast('success', null, this.i18nService.t(this.editMode ? 'editedItem' : 'addedItem')); this.onSavedCipher.emit(this.cipher); return true; @@ -238,7 +237,7 @@ export class AddEditComponent { this.deletePromise = this.deleteCipher(); await this.deletePromise; this.analytics.eventTrack.next({ action: 'Deleted Cipher' }); - this.toasterService.popAsync('success', null, this.i18nService.t('deletedItem')); + this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedItem')); this.onDeletedCipher.emit(this.cipher); } catch { } @@ -301,9 +300,10 @@ export class AddEditComponent { this.checkPasswordPromise = null; if (matches > 0) { - this.toasterService.popAsync('warning', null, this.i18nService.t('passwordExposed', matches.toString())); + this.platformUtilsService.showToast('warning', null, + this.i18nService.t('passwordExposed', matches.toString())); } else { - this.toasterService.popAsync('success', null, this.i18nService.t('passwordSafe')); + this.platformUtilsService.showToast('success', null, this.i18nService.t('passwordSafe')); } } diff --git a/src/angular/components/attachments.component.ts b/src/angular/components/attachments.component.ts index b24c7fd11e..05f6f241d6 100644 --- a/src/angular/components/attachments.component.ts +++ b/src/angular/components/attachments.component.ts @@ -5,7 +5,6 @@ import { Output, } from '@angular/core'; -import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; import { CipherService } from '../../abstractions/cipher.service'; @@ -32,7 +31,7 @@ export class AttachmentsComponent implements OnInit { deletePromises: { [id: string]: Promise; } = {}; constructor(protected cipherService: CipherService, protected analytics: Angulartics2, - protected toasterService: ToasterService, protected i18nService: I18nService, + protected i18nService: I18nService, protected cryptoService: CryptoService, protected userService: UserService, protected platformUtilsService: PlatformUtilsService, protected win: Window) { } @@ -63,7 +62,7 @@ export class AttachmentsComponent implements OnInit { async submit() { if (!this.hasUpdatedKey) { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('updateKey')); return; } @@ -71,13 +70,13 @@ export class AttachmentsComponent implements OnInit { const fileEl = document.getElementById('file') as HTMLInputElement; const files = fileEl.files; if (files == null || files.length === 0) { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('selectFile')); return; } if (files[0].size > 104857600) { // 100 MB - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('maxFileSize')); return; } @@ -87,7 +86,7 @@ export class AttachmentsComponent implements OnInit { this.cipherDomain = await this.formPromise; this.cipher = await this.cipherDomain.decrypt(); this.analytics.eventTrack.next({ action: 'Added Attachment' }); - this.toasterService.popAsync('success', null, this.i18nService.t('attachmentSaved')); + this.platformUtilsService.showToast('success', null, this.i18nService.t('attachmentSaved')); this.onUploadedAttachment.emit(); } catch { } @@ -114,7 +113,7 @@ export class AttachmentsComponent implements OnInit { this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id); await this.deletePromises[attachment.id]; this.analytics.eventTrack.next({ action: 'Deleted Attachment' }); - this.toasterService.popAsync('success', null, this.i18nService.t('deletedAttachment')); + this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedAttachment')); const i = this.cipher.attachments.indexOf(attachment); if (i > -1) { this.cipher.attachments.splice(i, 1); @@ -132,7 +131,7 @@ export class AttachmentsComponent implements OnInit { } if (!this.canAccessAttachments) { - this.toasterService.popAsync('error', this.i18nService.t('premiumRequired'), + this.platformUtilsService.showToast('error', this.i18nService.t('premiumRequired'), this.i18nService.t('premiumRequiredDesc')); return; } @@ -140,7 +139,7 @@ export class AttachmentsComponent implements OnInit { a.downloading = true; const response = await fetch(new Request(attachment.url, { cache: 'no-cache' })); if (response.status !== 200) { - this.toasterService.popAsync('error', null, this.i18nService.t('errorOccurred')); + this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred')); a.downloading = false; return; } @@ -151,7 +150,7 @@ export class AttachmentsComponent implements OnInit { const decBuf = await this.cryptoService.decryptFromBytes(buf, key); this.platformUtilsService.saveFile(this.win, decBuf, null, attachment.fileName); } catch (e) { - this.toasterService.popAsync('error', null, this.i18nService.t('errorOccurred')); + this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred')); } a.downloading = false; diff --git a/src/angular/components/environment.component.ts b/src/angular/components/environment.component.ts index d7d4226d9b..8c2456ad69 100644 --- a/src/angular/components/environment.component.ts +++ b/src/angular/components/environment.component.ts @@ -3,11 +3,11 @@ import { Output, } from '@angular/core'; -import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; import { EnvironmentService } from '../../abstractions/environment.service'; import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; export class EnvironmentComponent { @Output() onSaved = new EventEmitter(); @@ -20,7 +20,7 @@ export class EnvironmentComponent { baseUrl: string; showCustom = false; - constructor(protected analytics: Angulartics2, protected toasterService: ToasterService, + constructor(protected analytics: Angulartics2, protected platformUtilsService: PlatformUtilsService, protected environmentService: EnvironmentService, protected i18nService: I18nService) { this.baseUrl = environmentService.baseUrl || ''; this.webVaultUrl = environmentService.webVaultUrl || ''; @@ -49,7 +49,7 @@ export class EnvironmentComponent { this.notificationsUrl = resUrls.notifications; this.analytics.eventTrack.next({ action: 'Set Environment URLs' }); - this.toasterService.popAsync('success', null, this.i18nService.t('environmentSaved')); + this.platformUtilsService.showToast('success', null, this.i18nService.t('environmentSaved')); this.saved(); } diff --git a/src/angular/components/export.component.ts b/src/angular/components/export.component.ts index 128a36d9ab..6980210ffe 100644 --- a/src/angular/components/export.component.ts +++ b/src/angular/components/export.component.ts @@ -1,4 +1,3 @@ -import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; import { @@ -18,14 +17,14 @@ export class ExportComponent { masterPassword: string; showPassword = false; - constructor(protected analytics: Angulartics2, protected toasterService: ToasterService, + constructor(protected analytics: Angulartics2, protected cryptoService: CryptoService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected exportService: ExportService, protected win: Window) { } async submit() { if (this.masterPassword == null || this.masterPassword === '') { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('invalidMasterPassword')); return; } @@ -41,7 +40,7 @@ export class ExportComponent { this.saved(); } catch { } } else { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('invalidMasterPassword')); } } diff --git a/src/angular/components/folder-add-edit.component.ts b/src/angular/components/folder-add-edit.component.ts index c84be649c1..ac20449c8f 100644 --- a/src/angular/components/folder-add-edit.component.ts +++ b/src/angular/components/folder-add-edit.component.ts @@ -5,7 +5,6 @@ import { Output, } from '@angular/core'; -import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; import { FolderService } from '../../abstractions/folder.service'; @@ -26,8 +25,7 @@ export class FolderAddEditComponent implements OnInit { deletePromise: Promise; constructor(protected folderService: FolderService, protected i18nService: I18nService, - protected analytics: Angulartics2, protected toasterService: ToasterService, - protected platformUtilsService: PlatformUtilsService) { } + protected analytics: Angulartics2, protected platformUtilsService: PlatformUtilsService) { } async ngOnInit() { this.editMode = this.folderId != null; @@ -44,7 +42,7 @@ export class FolderAddEditComponent implements OnInit { async submit(): Promise { if (this.folder.name == null || this.folder.name === '') { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('nameRequired')); return false; } @@ -54,7 +52,7 @@ export class FolderAddEditComponent implements OnInit { this.formPromise = this.folderService.saveWithServer(folder); await this.formPromise; this.analytics.eventTrack.next({ action: this.editMode ? 'Edited Folder' : 'Added Folder' }); - this.toasterService.popAsync('success', null, + this.platformUtilsService.showToast('success', null, this.i18nService.t(this.editMode ? 'editedFolder' : 'addedFolder')); this.onSavedFolder.emit(this.folder); return true; @@ -75,7 +73,7 @@ export class FolderAddEditComponent implements OnInit { this.deletePromise = this.folderService.deleteWithServer(this.folder.id); await this.deletePromise; this.analytics.eventTrack.next({ action: 'Deleted Folder' }); - this.toasterService.popAsync('success', null, this.i18nService.t('deletedFolder')); + this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedFolder')); this.onDeletedFolder.emit(this.folder); } catch { } diff --git a/src/angular/components/hint.component.ts b/src/angular/components/hint.component.ts index be8a8ec8e4..3726d48566 100644 --- a/src/angular/components/hint.component.ts +++ b/src/angular/components/hint.component.ts @@ -1,12 +1,12 @@ import { Router } from '@angular/router'; -import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; import { PasswordHintRequest } from '../../models/request/passwordHintRequest'; import { ApiService } from '../../abstractions/api.service'; import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; export class HintComponent { email: string = ''; @@ -15,17 +15,17 @@ export class HintComponent { protected successRoute = 'login'; constructor(protected router: Router, protected analytics: Angulartics2, - protected toasterService: ToasterService, protected i18nService: I18nService, - protected apiService: ApiService) { } + protected i18nService: I18nService, protected apiService: ApiService, + protected platformUtilsService: PlatformUtilsService) { } async submit() { if (this.email == null || this.email === '') { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('emailRequired')); return; } if (this.email.indexOf('@') === -1) { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('invalidEmail')); return; } @@ -34,7 +34,7 @@ export class HintComponent { this.formPromise = this.apiService.postPasswordHint(new PasswordHintRequest(this.email)); await this.formPromise; this.analytics.eventTrack.next({ action: 'Requested Hint' }); - this.toasterService.popAsync('success', null, this.i18nService.t('masterPassSent')); + this.platformUtilsService.showToast('success', null, this.i18nService.t('masterPassSent')); this.router.navigate([this.successRoute]); } catch { } } diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 695fa3679b..31f7c86cee 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -1,6 +1,5 @@ import { Router } from '@angular/router'; -import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; import { CryptoService } from '../../abstractions/crypto.service'; @@ -16,13 +15,13 @@ export class LockComponent { protected successRoute: string = 'vault'; constructor(protected router: Router, protected analytics: Angulartics2, - protected toasterService: ToasterService, protected i18nService: I18nService, + protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected messagingService: MessagingService, protected userService: UserService, protected cryptoService: CryptoService) { } async submit() { if (this.masterPassword == null || this.masterPassword === '') { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('masterPassRequired')); return; } @@ -39,7 +38,7 @@ export class LockComponent { this.messagingService.send('unlocked'); this.router.navigate([this.successRoute]); } else { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('invalidMasterPassword')); } } diff --git a/src/angular/components/login.component.ts b/src/angular/components/login.component.ts index 9e5557f9c3..105d24d7de 100644 --- a/src/angular/components/login.component.ts +++ b/src/angular/components/login.component.ts @@ -4,13 +4,13 @@ import { } from '@angular/core'; import { Router } from '@angular/router'; -import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; import { AuthResult } from '../../models/domain/authResult'; import { AuthService } from '../../abstractions/auth.service'; import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { StorageService } from '../../abstractions/storage.service'; import { Utils } from '../../misc/utils'; @@ -34,7 +34,7 @@ export class LoginComponent implements OnInit { protected successRoute = 'vault'; constructor(protected authService: AuthService, protected router: Router, - protected analytics: Angulartics2, protected toasterService: ToasterService, + protected analytics: Angulartics2, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, private storageService: StorageService) { } async ngOnInit() { @@ -55,17 +55,17 @@ export class LoginComponent implements OnInit { async submit() { if (this.email == null || this.email === '') { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('emailRequired')); return; } if (this.email.indexOf('@') === -1) { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('invalidEmail')); return; } if (this.masterPassword == null || this.masterPassword === '') { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('masterPassRequired')); return; } diff --git a/src/angular/components/password-generator-history.component.ts b/src/angular/components/password-generator-history.component.ts index 45c1006992..ae3915269a 100644 --- a/src/angular/components/password-generator-history.component.ts +++ b/src/angular/components/password-generator-history.component.ts @@ -1,4 +1,3 @@ -import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; import { OnInit } from '@angular/core'; @@ -14,7 +13,7 @@ export class PasswordGeneratorHistoryComponent implements OnInit { constructor(protected passwordGenerationService: PasswordGenerationService, protected analytics: Angulartics2, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, - protected toasterService: ToasterService, private win: Window) { } + private win: Window) { } async ngOnInit() { this.history = await this.passwordGenerationService.getHistory(); @@ -29,6 +28,7 @@ export class PasswordGeneratorHistoryComponent implements OnInit { this.analytics.eventTrack.next({ action: 'Copied Historical Password' }); const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(password, copyOptions); - this.toasterService.popAsync('info', null, this.i18nService.t('valueCopied', this.i18nService.t('password'))); + this.platformUtilsService.showToast('info', null, + this.i18nService.t('valueCopied', this.i18nService.t('password'))); } } diff --git a/src/angular/components/password-generator.component.ts b/src/angular/components/password-generator.component.ts index c6cd72fb6d..600791c84a 100644 --- a/src/angular/components/password-generator.component.ts +++ b/src/angular/components/password-generator.component.ts @@ -1,4 +1,3 @@ -import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; import { @@ -23,7 +22,7 @@ export class PasswordGeneratorComponent implements OnInit { constructor(protected passwordGenerationService: PasswordGenerationService, protected analytics: Angulartics2, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, - protected toasterService: ToasterService, private win: Window) { } + private win: Window) { } async ngOnInit() { this.options = await this.passwordGenerationService.getOptions(); @@ -63,7 +62,8 @@ export class PasswordGeneratorComponent implements OnInit { this.analytics.eventTrack.next({ action: 'Copied Generated Password' }); const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(this.password, copyOptions); - this.toasterService.popAsync('info', null, this.i18nService.t('valueCopied', this.i18nService.t('password'))); + this.platformUtilsService.showToast('info', null, + this.i18nService.t('valueCopied', this.i18nService.t('password'))); } select() { diff --git a/src/angular/components/password-history.component.ts b/src/angular/components/password-history.component.ts index b847f9ea33..e46e4ce0d9 100644 --- a/src/angular/components/password-history.component.ts +++ b/src/angular/components/password-history.component.ts @@ -1,4 +1,3 @@ -import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; import { OnInit } from '@angular/core'; @@ -15,7 +14,7 @@ export class PasswordHistoryComponent implements OnInit { constructor(protected cipherService: CipherService, protected analytics: Angulartics2, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, - protected toasterService: ToasterService, private win: Window) { } + private win: Window) { } async ngOnInit() { const cipher = await this.cipherService.get(this.cipherId); @@ -27,6 +26,7 @@ export class PasswordHistoryComponent implements OnInit { this.analytics.eventTrack.next({ action: 'Copied Password History' }); const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(password, copyOptions); - this.toasterService.popAsync('info', null, this.i18nService.t('valueCopied', this.i18nService.t('password'))); + this.platformUtilsService.showToast('info', null, + this.i18nService.t('valueCopied', this.i18nService.t('password'))); } } diff --git a/src/angular/components/premium.component.ts b/src/angular/components/premium.component.ts index 4c3adeb536..a6981c80d9 100644 --- a/src/angular/components/premium.component.ts +++ b/src/angular/components/premium.component.ts @@ -1,6 +1,5 @@ import { OnInit } from '@angular/core'; -import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; import { ApiService } from '../../abstractions/api.service'; @@ -13,7 +12,7 @@ export class PremiumComponent implements OnInit { price: number = 10; refreshPromise: Promise; - constructor(protected analytics: Angulartics2, protected toasterService: ToasterService, + constructor(protected analytics: Angulartics2, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected tokenService: TokenService, protected apiService: ApiService) { } @@ -25,7 +24,7 @@ export class PremiumComponent implements OnInit { try { this.refreshPromise = this.apiService.refreshIdentityToken(); await this.refreshPromise; - this.toasterService.popAsync('success', null, this.i18nService.t('refreshComplete')); + this.platformUtilsService.showToast('success', null, this.i18nService.t('refreshComplete')); this.isPremium = this.tokenService.getPremium(); } catch { } } diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index f30e8bff55..fe9a747981 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -1,6 +1,5 @@ import { Router } from '@angular/router'; -import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; import { KeysRequest } from '../../models/request/keysRequest'; @@ -27,34 +26,34 @@ export class RegisterComponent { protected successRoute = 'login'; constructor(protected authService: AuthService, protected router: Router, - protected analytics: Angulartics2, protected toasterService: ToasterService, + protected analytics: Angulartics2, protected i18nService: I18nService, protected cryptoService: CryptoService, protected apiService: ApiService, protected stateService: StateService, protected platformUtilsService: PlatformUtilsService) { } async submit() { if (this.email == null || this.email === '') { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('emailRequired')); return; } if (this.email.indexOf('@') === -1) { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('invalidEmail')); return; } if (this.masterPassword == null || this.masterPassword === '') { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('masterPassRequired')); return; } if (this.masterPassword.length < 8) { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('masterPassLength')); return; } if (this.masterPassword !== this.confirmMasterPassword) { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('masterPassDoesntMatch')); return; } @@ -81,7 +80,7 @@ export class RegisterComponent { this.formPromise = this.apiService.postRegister(request); await this.formPromise; this.analytics.eventTrack.next({ action: 'Registered' }); - this.toasterService.popAsync('success', null, this.i18nService.t('newAccountCreated')); + this.platformUtilsService.showToast('success', null, this.i18nService.t('newAccountCreated')); this.router.navigate([this.successRoute], { queryParams: { email: this.email } }); } catch { } } diff --git a/src/angular/components/two-factor-options.component.ts b/src/angular/components/two-factor-options.component.ts index 998ad76743..d930b5e9fb 100644 --- a/src/angular/components/two-factor-options.component.ts +++ b/src/angular/components/two-factor-options.component.ts @@ -6,7 +6,6 @@ import { } from '@angular/core'; import { Router } from '@angular/router'; -import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; @@ -15,8 +14,6 @@ import { AuthService } from '../../abstractions/auth.service'; import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; -import { TwoFactorProviders } from '../../services/auth.service'; - export class TwoFactorOptionsComponent implements OnInit { @Output() onProviderSelected = new EventEmitter(); @Output() onRecoverSelected = new EventEmitter(); @@ -24,7 +21,7 @@ export class TwoFactorOptionsComponent implements OnInit { providers: any[] = []; constructor(protected authService: AuthService, protected router: Router, - protected analytics: Angulartics2, protected toasterService: ToasterService, + protected analytics: Angulartics2, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected win: Window) { } diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 705340681e..dc08df0e9a 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -4,7 +4,6 @@ import { } from '@angular/core'; import { Router } from '@angular/router'; -import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; import { DeviceType } from '../../enums/deviceType'; @@ -43,7 +42,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { protected successRoute = 'vault'; constructor(protected authService: AuthService, protected router: Router, - protected analytics: Angulartics2, protected toasterService: ToasterService, + protected analytics: Angulartics2, protected i18nService: I18nService, protected apiService: ApiService, protected platformUtilsService: PlatformUtilsService, protected win: Window, protected environmentService: EnvironmentService) { @@ -69,7 +68,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { this.token = token; this.submit(); }, (error: string) => { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), error); + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), error); }, (info: string) => { if (info === 'ready') { this.u2fReady = true; @@ -147,7 +146,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { async submit() { if (this.token == null || this.token === '') { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('verificationCodeRequired')); return; } @@ -196,7 +195,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { this.emailPromise = this.apiService.postTwoFactorEmail(request); await this.emailPromise; if (doToast) { - this.toasterService.popAsync('success', null, + this.platformUtilsService.showToast('success', null, this.i18nService.t('verificationCodeEmailSent', this.twoFactorEmail)); } } catch { } diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index 240384d8f5..11c5c16ca6 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -8,7 +8,6 @@ import { Output, } from '@angular/core'; -import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; import { CipherType } from '../../enums/cipherType'; @@ -50,7 +49,7 @@ export class ViewComponent implements OnDestroy, OnInit { private totpInterval: any; constructor(protected cipherService: CipherService, protected totpService: TotpService, - protected tokenService: TokenService, protected toasterService: ToasterService, + protected tokenService: TokenService, protected cryptoService: CryptoService, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, protected analytics: Angulartics2, protected auditService: AuditService, protected win: Window, @@ -120,9 +119,10 @@ export class ViewComponent implements OnDestroy, OnInit { const matches = await this.checkPasswordPromise; if (matches > 0) { - this.toasterService.popAsync('warning', null, this.i18nService.t('passwordExposed', matches.toString())); + this.platformUtilsService.showToast('warning', null, + this.i18nService.t('passwordExposed', matches.toString())); } else { - this.toasterService.popAsync('success', null, this.i18nService.t('passwordSafe')); + this.platformUtilsService.showToast('success', null, this.i18nService.t('passwordSafe')); } } @@ -148,7 +148,7 @@ export class ViewComponent implements OnDestroy, OnInit { this.analytics.eventTrack.next({ action: 'Copied ' + aType }); const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(value, copyOptions); - this.toasterService.popAsync('info', null, + this.platformUtilsService.showToast('info', null, this.i18nService.t('valueCopied', this.i18nService.t(typeI18nKey))); } @@ -159,7 +159,7 @@ export class ViewComponent implements OnDestroy, OnInit { } if (this.cipher.organizationId == null && !this.canAccessPremium) { - this.toasterService.popAsync('error', this.i18nService.t('premiumRequired'), + this.platformUtilsService.showToast('error', this.i18nService.t('premiumRequired'), this.i18nService.t('premiumRequiredDesc')); return; } @@ -167,7 +167,7 @@ export class ViewComponent implements OnDestroy, OnInit { a.downloading = true; const response = await fetch(new Request(attachment.url, { cache: 'no-cache' })); if (response.status !== 200) { - this.toasterService.popAsync('error', null, this.i18nService.t('errorOccurred')); + this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred')); a.downloading = false; return; } @@ -178,7 +178,7 @@ export class ViewComponent implements OnDestroy, OnInit { const decBuf = await this.cryptoService.decryptFromBytes(buf, key); this.platformUtilsService.saveFile(this.win, decBuf, null, attachment.fileName); } catch (e) { - this.toasterService.popAsync('error', null, this.i18nService.t('errorOccurred')); + this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred')); } a.downloading = false; diff --git a/src/angular/services/validation.service.ts b/src/angular/services/validation.service.ts index dbef4e9a7e..c5c3e293ae 100644 --- a/src/angular/services/validation.service.ts +++ b/src/angular/services/validation.service.ts @@ -1,21 +1,11 @@ -import { - Injectable, - SecurityContext, -} from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; - -import { - BodyOutputType, - Toast, - ToasterService, -} from 'angular2-toaster'; +import { Injectable } from '@angular/core'; import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; @Injectable() export class ValidationService { - constructor(private toasterService: ToasterService, private i18nService: I18nService, - private sanitizer: DomSanitizer) { } + constructor(private i18nService: I18nService, private platformUtilsService: PlatformUtilsService) { } showError(data: any): string[] { const defaultErrorMessage = this.i18nService.t('unexpectedError'); @@ -45,18 +35,11 @@ export class ValidationService { } if (errors.length === 1) { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), errors[0]); + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), errors[0]); } else if (errors.length > 1) { - let errorMessage = ''; - errors.forEach((e) => errorMessage += ('

' + this.sanitizer.sanitize(SecurityContext.HTML, e) + '

')); - const toast: Toast = { - type: 'error', - title: this.i18nService.t('errorOccurred'), - body: errorMessage, - bodyOutputType: BodyOutputType.TrustedHtml, + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), errors, { timeout: 5000 * errors.length, - }; - this.toasterService.popAsync(toast); + }); } return errors; diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index b4c0ed84a7..acf2845fcd 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -140,14 +140,15 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { return true; } - showToast(type: 'error' | 'success' | 'warning' | 'info', title: string, text: string, global?: any): void { - if (global == null && Utils.isBrowser) { - global = window; + showToast(type: 'error' | 'success' | 'warning' | 'info', title: string, text: string | string[], + options?: any): void { + if ((options == null || options.global == null) && Utils.isBrowser) { + options.global = window; } - if (global == null || global.BitwardenToasterService == null) { + if (options.global == null || options.global.BitwardenToasterService == null) { throw new Error('BitwardenToasterService not available on global.'); } - global.BitwardenToasterService.popAsync(type, title, text); + options.global.BitwardenToasterService.popAsync(type, title, text); } showDialog(text: string, title?: string, confirmText?: string, cancelText?: string, type?: string): From ad31527b8dd156d74be7dd03e7d4b6f4dc34fc97 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 3 Oct 2018 00:03:49 -0400 Subject: [PATCH 0565/1626] move eventTrack analytics to platform utils --- package-lock.json | 8 -------- package.json | 1 - src/abstractions/platformUtils.service.ts | 1 + src/angular/components/add-edit.component.ts | 13 +++++-------- src/angular/components/attachments.component.ts | 9 +++------ src/angular/components/environment.component.ts | 8 +++----- src/angular/components/export.component.ts | 9 +++------ .../components/folder-add-edit.component.ts | 8 +++----- src/angular/components/groupings.component.ts | 1 - src/angular/components/hint.component.ts | 9 +++------ src/angular/components/lock.component.ts | 7 ++----- src/angular/components/login.component.ts | 12 +++++------- .../password-generator-history.component.ts | 6 ++---- .../components/password-generator.component.ts | 14 ++++++-------- .../components/password-history.component.ts | 9 +++------ src/angular/components/premium.component.ts | 9 +++------ src/angular/components/register.component.ts | 7 ++----- .../components/two-factor-options.component.ts | 5 +---- src/angular/components/two-factor.component.ts | 5 +---- src/angular/components/view.component.ts | 15 ++++++--------- 20 files changed, 52 insertions(+), 104 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4668dfc380..b02d5a7110 100644 --- a/package-lock.json +++ b/package-lock.json @@ -229,14 +229,6 @@ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", "dev": true }, - "angulartics2": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/angulartics2/-/angulartics2-6.3.0.tgz", - "integrity": "sha512-5BwRYCLF6ypBl7rlarW403v4AdotIldJhN+2rQeTIw/rTtngJ4SewNhf4zlRnKBSItlhbZRrDJBl9uR2TUuCdw==", - "requires": { - "tslib": "^1.9.0" - } - }, "ansi-align": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", diff --git a/package.json b/package.json index 09029ed887..90406232d4 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,6 @@ "@angular/upgrade": "6.1.7", "@aspnet/signalr": "1.0.3", "@aspnet/signalr-protocol-msgpack": "1.0.3", - "angulartics2": "6.3.0", "core-js": "2.5.7", "electron-log": "2.2.14", "electron-updater": "3.0.3", diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index f24b0f5706..e33b97db54 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -25,6 +25,7 @@ export abstract class PlatformUtilsService { options?: any) => void; showDialog: (text: string, title?: string, confirmText?: string, cancelText?: string, type?: string) => Promise; + eventTrack: (action: string, label?: string, options?: any) => void; isDev: () => boolean; isSelfHost: () => boolean; copyToClipboard: (text: string, options?: any) => void; diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 9642c8afb3..345a22740a 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -4,8 +4,6 @@ import { Output, } from '@angular/core'; -import { Angulartics2 } from 'angulartics2'; - import { CipherType } from '../../enums/cipherType'; import { FieldType } from '../../enums/fieldType'; import { SecureNoteType } from '../../enums/secureNoteType'; @@ -60,7 +58,6 @@ export class AddEditComponent { constructor(protected cipherService: CipherService, protected folderService: FolderService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, - protected analytics: Angulartics2, protected auditService: AuditService, protected stateService: StateService) { this.typeOptions = [ { name: i18nService.t('typeLogin'), value: CipherType.Login }, @@ -167,7 +164,7 @@ export class AddEditComponent { this.formPromise = this.saveCipher(cipher); await this.formPromise; this.cipher.id = cipher.id; - this.analytics.eventTrack.next({ action: this.editMode ? 'Edited Cipher' : 'Added Cipher' }); + this.platformUtilsService.eventTrack(this.editMode ? 'Edited Cipher' : 'Added Cipher'); this.platformUtilsService.showToast('success', null, this.i18nService.t(this.editMode ? 'editedItem' : 'addedItem')); this.onSavedCipher.emit(this.cipher); @@ -236,7 +233,7 @@ export class AddEditComponent { try { this.deletePromise = this.deleteCipher(); await this.deletePromise; - this.analytics.eventTrack.next({ action: 'Deleted Cipher' }); + this.platformUtilsService.eventTrack('Deleted Cipher'); this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedItem')); this.onDeletedCipher.emit(this.cipher); } catch { } @@ -259,13 +256,13 @@ export class AddEditComponent { } togglePassword() { - this.analytics.eventTrack.next({ action: 'Toggled Password on Edit' }); + this.platformUtilsService.eventTrack('Toggled Password on Edit'); this.showPassword = !this.showPassword; document.getElementById('loginPassword').focus(); } toggleCardCode() { - this.analytics.eventTrack.next({ action: 'Toggled CardCode on Edit' }); + this.platformUtilsService.eventTrack('Toggled CardCode on Edit'); this.showCardCode = !this.showCardCode; document.getElementById('cardCode').focus(); } @@ -294,7 +291,7 @@ export class AddEditComponent { return; } - this.analytics.eventTrack.next({ action: 'Check Password' }); + this.platformUtilsService.eventTrack('Check Password'); this.checkPasswordPromise = this.auditService.passwordLeaked(this.cipher.login.password); const matches = await this.checkPasswordPromise; this.checkPasswordPromise = null; diff --git a/src/angular/components/attachments.component.ts b/src/angular/components/attachments.component.ts index 05f6f241d6..5ef23b12bb 100644 --- a/src/angular/components/attachments.component.ts +++ b/src/angular/components/attachments.component.ts @@ -5,8 +5,6 @@ import { Output, } from '@angular/core'; -import { Angulartics2 } from 'angulartics2'; - import { CipherService } from '../../abstractions/cipher.service'; import { CryptoService } from '../../abstractions/crypto.service'; import { I18nService } from '../../abstractions/i18n.service'; @@ -30,8 +28,7 @@ export class AttachmentsComponent implements OnInit { formPromise: Promise; deletePromises: { [id: string]: Promise; } = {}; - constructor(protected cipherService: CipherService, protected analytics: Angulartics2, - protected i18nService: I18nService, + constructor(protected cipherService: CipherService, protected i18nService: I18nService, protected cryptoService: CryptoService, protected userService: UserService, protected platformUtilsService: PlatformUtilsService, protected win: Window) { } @@ -85,7 +82,7 @@ export class AttachmentsComponent implements OnInit { this.formPromise = this.saveCipherAttachment(files[0]); this.cipherDomain = await this.formPromise; this.cipher = await this.cipherDomain.decrypt(); - this.analytics.eventTrack.next({ action: 'Added Attachment' }); + this.platformUtilsService.eventTrack('Added Attachment'); this.platformUtilsService.showToast('success', null, this.i18nService.t('attachmentSaved')); this.onUploadedAttachment.emit(); } catch { } @@ -112,7 +109,7 @@ export class AttachmentsComponent implements OnInit { try { this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id); await this.deletePromises[attachment.id]; - this.analytics.eventTrack.next({ action: 'Deleted Attachment' }); + this.platformUtilsService.eventTrack('Deleted Attachment'); this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedAttachment')); const i = this.cipher.attachments.indexOf(attachment); if (i > -1) { diff --git a/src/angular/components/environment.component.ts b/src/angular/components/environment.component.ts index 8c2456ad69..70db95d2ec 100644 --- a/src/angular/components/environment.component.ts +++ b/src/angular/components/environment.component.ts @@ -3,8 +3,6 @@ import { Output, } from '@angular/core'; -import { Angulartics2 } from 'angulartics2'; - import { EnvironmentService } from '../../abstractions/environment.service'; import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; @@ -20,8 +18,8 @@ export class EnvironmentComponent { baseUrl: string; showCustom = false; - constructor(protected analytics: Angulartics2, protected platformUtilsService: PlatformUtilsService, - protected environmentService: EnvironmentService, protected i18nService: I18nService) { + constructor(protected platformUtilsService: PlatformUtilsService, protected environmentService: EnvironmentService, + protected i18nService: I18nService) { this.baseUrl = environmentService.baseUrl || ''; this.webVaultUrl = environmentService.webVaultUrl || ''; this.apiUrl = environmentService.apiUrl || ''; @@ -48,7 +46,7 @@ export class EnvironmentComponent { this.iconsUrl = resUrls.icons; this.notificationsUrl = resUrls.notifications; - this.analytics.eventTrack.next({ action: 'Set Environment URLs' }); + this.platformUtilsService.eventTrack('Set Environment URLs'); this.platformUtilsService.showToast('success', null, this.i18nService.t('environmentSaved')); this.saved(); } diff --git a/src/angular/components/export.component.ts b/src/angular/components/export.component.ts index 6980210ffe..d4192fed26 100644 --- a/src/angular/components/export.component.ts +++ b/src/angular/components/export.component.ts @@ -1,5 +1,3 @@ -import { Angulartics2 } from 'angulartics2'; - import { EventEmitter, Output, @@ -17,8 +15,7 @@ export class ExportComponent { masterPassword: string; showPassword = false; - constructor(protected analytics: Angulartics2, - protected cryptoService: CryptoService, protected i18nService: I18nService, + constructor(protected cryptoService: CryptoService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected exportService: ExportService, protected win: Window) { } @@ -35,7 +32,7 @@ export class ExportComponent { try { this.formPromise = this.getExportData(); const data = await this.formPromise; - this.analytics.eventTrack.next({ action: 'Exported Data' }); + this.platformUtilsService.eventTrack('Exported Data'); this.downloadFile(data); this.saved(); } catch { } @@ -46,7 +43,7 @@ export class ExportComponent { } togglePassword() { - this.analytics.eventTrack.next({ action: 'Toggled Master Password on Export' }); + this.platformUtilsService.eventTrack('Toggled Master Password on Export'); this.showPassword = !this.showPassword; document.getElementById('masterPassword').focus(); } diff --git a/src/angular/components/folder-add-edit.component.ts b/src/angular/components/folder-add-edit.component.ts index ac20449c8f..5e6314431a 100644 --- a/src/angular/components/folder-add-edit.component.ts +++ b/src/angular/components/folder-add-edit.component.ts @@ -5,8 +5,6 @@ import { Output, } from '@angular/core'; -import { Angulartics2 } from 'angulartics2'; - import { FolderService } from '../../abstractions/folder.service'; import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; @@ -25,7 +23,7 @@ export class FolderAddEditComponent implements OnInit { deletePromise: Promise; constructor(protected folderService: FolderService, protected i18nService: I18nService, - protected analytics: Angulartics2, protected platformUtilsService: PlatformUtilsService) { } + protected platformUtilsService: PlatformUtilsService) { } async ngOnInit() { this.editMode = this.folderId != null; @@ -51,7 +49,7 @@ export class FolderAddEditComponent implements OnInit { const folder = await this.folderService.encrypt(this.folder); this.formPromise = this.folderService.saveWithServer(folder); await this.formPromise; - this.analytics.eventTrack.next({ action: this.editMode ? 'Edited Folder' : 'Added Folder' }); + this.platformUtilsService.eventTrack(this.editMode ? 'Edited Folder' : 'Added Folder'); this.platformUtilsService.showToast('success', null, this.i18nService.t(this.editMode ? 'editedFolder' : 'addedFolder')); this.onSavedFolder.emit(this.folder); @@ -72,7 +70,7 @@ export class FolderAddEditComponent implements OnInit { try { this.deletePromise = this.folderService.deleteWithServer(this.folder.id); await this.deletePromise; - this.analytics.eventTrack.next({ action: 'Deleted Folder' }); + this.platformUtilsService.eventTrack('Deleted Folder'); this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedFolder')); this.onDeletedFolder.emit(this.folder); } catch { } diff --git a/src/angular/components/groupings.component.ts b/src/angular/components/groupings.component.ts index 605ce4bc58..88ea88d88e 100644 --- a/src/angular/components/groupings.component.ts +++ b/src/angular/components/groupings.component.ts @@ -1,5 +1,4 @@ import { - Component, EventEmitter, Input, Output, diff --git a/src/angular/components/hint.component.ts b/src/angular/components/hint.component.ts index 3726d48566..e1052a58f4 100644 --- a/src/angular/components/hint.component.ts +++ b/src/angular/components/hint.component.ts @@ -1,7 +1,5 @@ import { Router } from '@angular/router'; -import { Angulartics2 } from 'angulartics2'; - import { PasswordHintRequest } from '../../models/request/passwordHintRequest'; import { ApiService } from '../../abstractions/api.service'; @@ -14,9 +12,8 @@ export class HintComponent { protected successRoute = 'login'; - constructor(protected router: Router, protected analytics: Angulartics2, - protected i18nService: I18nService, protected apiService: ApiService, - protected platformUtilsService: PlatformUtilsService) { } + constructor(protected router: Router, protected i18nService: I18nService, + protected apiService: ApiService, protected platformUtilsService: PlatformUtilsService) { } async submit() { if (this.email == null || this.email === '') { @@ -33,7 +30,7 @@ export class HintComponent { try { this.formPromise = this.apiService.postPasswordHint(new PasswordHintRequest(this.email)); await this.formPromise; - this.analytics.eventTrack.next({ action: 'Requested Hint' }); + this.platformUtilsService.eventTrack('Requested Hint'); this.platformUtilsService.showToast('success', null, this.i18nService.t('masterPassSent')); this.router.navigate([this.successRoute]); } catch { } diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 31f7c86cee..1d2ba6f147 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -1,7 +1,5 @@ import { Router } from '@angular/router'; -import { Angulartics2 } from 'angulartics2'; - import { CryptoService } from '../../abstractions/crypto.service'; import { I18nService } from '../../abstractions/i18n.service'; import { MessagingService } from '../../abstractions/messaging.service'; @@ -14,8 +12,7 @@ export class LockComponent { protected successRoute: string = 'vault'; - constructor(protected router: Router, protected analytics: Angulartics2, - protected i18nService: I18nService, + constructor(protected router: Router, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected messagingService: MessagingService, protected userService: UserService, protected cryptoService: CryptoService) { } @@ -52,7 +49,7 @@ export class LockComponent { } togglePassword() { - this.analytics.eventTrack.next({ action: 'Toggled Master Password on Unlock' }); + this.platformUtilsService.eventTrack('Toggled Master Password on Unlock'); this.showPassword = !this.showPassword; document.getElementById('masterPassword').focus(); } diff --git a/src/angular/components/login.component.ts b/src/angular/components/login.component.ts index 105d24d7de..d5a19526b1 100644 --- a/src/angular/components/login.component.ts +++ b/src/angular/components/login.component.ts @@ -4,8 +4,6 @@ import { } from '@angular/core'; import { Router } from '@angular/router'; -import { Angulartics2 } from 'angulartics2'; - import { AuthResult } from '../../models/domain/authResult'; import { AuthService } from '../../abstractions/auth.service'; @@ -34,8 +32,8 @@ export class LoginComponent implements OnInit { protected successRoute = 'vault'; constructor(protected authService: AuthService, protected router: Router, - protected analytics: Angulartics2, protected platformUtilsService: PlatformUtilsService, - protected i18nService: I18nService, private storageService: StorageService) { } + protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, + private storageService: StorageService) { } async ngOnInit() { if (this.email == null || this.email === '') { @@ -80,13 +78,13 @@ export class LoginComponent implements OnInit { await this.storageService.remove(Keys.rememberedEmail); } if (response.twoFactor) { - this.analytics.eventTrack.next({ action: 'Logged In To Two-step' }); + this.platformUtilsService.eventTrack('Logged In To Two-step'); this.router.navigate([this.twoFactorRoute]); } else { if (this.onSuccessfulLogin != null) { this.onSuccessfulLogin(); } - this.analytics.eventTrack.next({ action: 'Logged In' }); + this.platformUtilsService.eventTrack('Logged In'); if (this.onSuccessfulLoginNavigate != null) { this.onSuccessfulLoginNavigate(); } else { @@ -97,7 +95,7 @@ export class LoginComponent implements OnInit { } togglePassword() { - this.analytics.eventTrack.next({ action: 'Toggled Master Password on Login' }); + this.platformUtilsService.eventTrack('Toggled Master Password on Login'); this.showPassword = !this.showPassword; document.getElementById('masterPassword').focus(); } diff --git a/src/angular/components/password-generator-history.component.ts b/src/angular/components/password-generator-history.component.ts index ae3915269a..94ad6b1b05 100644 --- a/src/angular/components/password-generator-history.component.ts +++ b/src/angular/components/password-generator-history.component.ts @@ -1,5 +1,3 @@ -import { Angulartics2 } from 'angulartics2'; - import { OnInit } from '@angular/core'; import { I18nService } from '../../abstractions/i18n.service'; @@ -11,7 +9,7 @@ import { GeneratedPasswordHistory } from '../../models/domain/generatedPasswordH export class PasswordGeneratorHistoryComponent implements OnInit { history: GeneratedPasswordHistory[] = []; - constructor(protected passwordGenerationService: PasswordGenerationService, protected analytics: Angulartics2, + constructor(protected passwordGenerationService: PasswordGenerationService, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, private win: Window) { } @@ -25,7 +23,7 @@ export class PasswordGeneratorHistoryComponent implements OnInit { } copy(password: string) { - this.analytics.eventTrack.next({ action: 'Copied Historical Password' }); + this.platformUtilsService.eventTrack('Copied Historical Password'); const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(password, copyOptions); this.platformUtilsService.showToast('info', null, diff --git a/src/angular/components/password-generator.component.ts b/src/angular/components/password-generator.component.ts index 600791c84a..f5fb6461b2 100644 --- a/src/angular/components/password-generator.component.ts +++ b/src/angular/components/password-generator.component.ts @@ -1,5 +1,3 @@ -import { Angulartics2 } from 'angulartics2'; - import { EventEmitter, Input, @@ -20,7 +18,7 @@ export class PasswordGeneratorComponent implements OnInit { showOptions = false; avoidAmbiguous = false; - constructor(protected passwordGenerationService: PasswordGenerationService, protected analytics: Angulartics2, + constructor(protected passwordGenerationService: PasswordGenerationService, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, private win: Window) { } @@ -28,14 +26,14 @@ export class PasswordGeneratorComponent implements OnInit { this.options = await this.passwordGenerationService.getOptions(); this.avoidAmbiguous = !this.options.ambiguous; this.password = await this.passwordGenerationService.generatePassword(this.options); - this.analytics.eventTrack.next({ action: 'Generated Password' }); + this.platformUtilsService.eventTrack('Generated Password'); await this.passwordGenerationService.addHistory(this.password); } async sliderChanged() { this.saveOptions(false); await this.passwordGenerationService.addHistory(this.password); - this.analytics.eventTrack.next({ action: 'Regenerated Password' }); + this.platformUtilsService.eventTrack('Regenerated Password'); } async sliderInput() { @@ -55,11 +53,11 @@ export class PasswordGeneratorComponent implements OnInit { async regenerate() { this.password = await this.passwordGenerationService.generatePassword(this.options); await this.passwordGenerationService.addHistory(this.password); - this.analytics.eventTrack.next({ action: 'Regenerated Password' }); + this.platformUtilsService.eventTrack('Regenerated Password'); } copy() { - this.analytics.eventTrack.next({ action: 'Copied Generated Password' }); + this.platformUtilsService.eventTrack('Copied Generated Password'); const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(this.password, copyOptions); this.platformUtilsService.showToast('info', null, @@ -67,7 +65,7 @@ export class PasswordGeneratorComponent implements OnInit { } select() { - this.analytics.eventTrack.next({ action: 'Selected Generated Password' }); + this.platformUtilsService.eventTrack('Selected Generated Password'); this.onSelected.emit(this.password); } diff --git a/src/angular/components/password-history.component.ts b/src/angular/components/password-history.component.ts index e46e4ce0d9..d567d73d6c 100644 --- a/src/angular/components/password-history.component.ts +++ b/src/angular/components/password-history.component.ts @@ -1,5 +1,3 @@ -import { Angulartics2 } from 'angulartics2'; - import { OnInit } from '@angular/core'; import { CipherService } from '../../abstractions/cipher.service'; @@ -12,9 +10,8 @@ export class PasswordHistoryComponent implements OnInit { cipherId: string; history: PasswordHistoryView[] = []; - constructor(protected cipherService: CipherService, protected analytics: Angulartics2, - protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, - private win: Window) { } + constructor(protected cipherService: CipherService, protected platformUtilsService: PlatformUtilsService, + protected i18nService: I18nService, private win: Window) { } async ngOnInit() { const cipher = await this.cipherService.get(this.cipherId); @@ -23,7 +20,7 @@ export class PasswordHistoryComponent implements OnInit { } copy(password: string) { - this.analytics.eventTrack.next({ action: 'Copied Password History' }); + this.platformUtilsService.eventTrack('Copied Password History'); const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(password, copyOptions); this.platformUtilsService.showToast('info', null, diff --git a/src/angular/components/premium.component.ts b/src/angular/components/premium.component.ts index a6981c80d9..9bb2ac092a 100644 --- a/src/angular/components/premium.component.ts +++ b/src/angular/components/premium.component.ts @@ -1,7 +1,5 @@ import { OnInit } from '@angular/core'; -import { Angulartics2 } from 'angulartics2'; - import { ApiService } from '../../abstractions/api.service'; import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; @@ -12,8 +10,7 @@ export class PremiumComponent implements OnInit { price: number = 10; refreshPromise: Promise; - constructor(protected analytics: Angulartics2, - protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, + constructor(protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected tokenService: TokenService, protected apiService: ApiService) { } async ngOnInit() { @@ -33,7 +30,7 @@ export class PremiumComponent implements OnInit { const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('premiumPurchaseAlert'), this.i18nService.t('premiumPurchase'), this.i18nService.t('yes'), this.i18nService.t('cancel')); if (confirmed) { - this.analytics.eventTrack.next({ action: 'Clicked Purchase Premium' }); + this.platformUtilsService.eventTrack('Clicked Purchase Premium'); this.platformUtilsService.launchUri('https://vault.bitwarden.com/#/?premium=purchase'); } } @@ -42,7 +39,7 @@ export class PremiumComponent implements OnInit { const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('premiumManageAlert'), this.i18nService.t('premiumManage'), this.i18nService.t('yes'), this.i18nService.t('cancel')); if (confirmed) { - this.analytics.eventTrack.next({ action: 'Clicked Manage Membership' }); + this.platformUtilsService.eventTrack('Clicked Manage Membership'); this.platformUtilsService.launchUri('https://vault.bitwarden.com/#/?premium=manage'); } } diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index fe9a747981..bad3b3ced9 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -1,7 +1,5 @@ import { Router } from '@angular/router'; -import { Angulartics2 } from 'angulartics2'; - import { KeysRequest } from '../../models/request/keysRequest'; import { RegisterRequest } from '../../models/request/registerRequest'; @@ -26,7 +24,6 @@ export class RegisterComponent { protected successRoute = 'login'; constructor(protected authService: AuthService, protected router: Router, - protected analytics: Angulartics2, protected i18nService: I18nService, protected cryptoService: CryptoService, protected apiService: ApiService, protected stateService: StateService, protected platformUtilsService: PlatformUtilsService) { } @@ -79,14 +76,14 @@ export class RegisterComponent { try { this.formPromise = this.apiService.postRegister(request); await this.formPromise; - this.analytics.eventTrack.next({ action: 'Registered' }); + this.platformUtilsService.eventTrack('Registered'); this.platformUtilsService.showToast('success', null, this.i18nService.t('newAccountCreated')); this.router.navigate([this.successRoute], { queryParams: { email: this.email } }); } catch { } } togglePassword(confirmField: boolean) { - this.analytics.eventTrack.next({ action: 'Toggled Master Password on Register' }); + this.platformUtilsService.eventTrack('Toggled Master Password on Register'); this.showPassword = !this.showPassword; document.getElementById(confirmField ? 'masterPasswordRetype' : 'masterPassword').focus(); } diff --git a/src/angular/components/two-factor-options.component.ts b/src/angular/components/two-factor-options.component.ts index d930b5e9fb..875dbb8510 100644 --- a/src/angular/components/two-factor-options.component.ts +++ b/src/angular/components/two-factor-options.component.ts @@ -6,8 +6,6 @@ import { } from '@angular/core'; import { Router } from '@angular/router'; -import { Angulartics2 } from 'angulartics2'; - import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; import { AuthService } from '../../abstractions/auth.service'; @@ -21,7 +19,6 @@ export class TwoFactorOptionsComponent implements OnInit { providers: any[] = []; constructor(protected authService: AuthService, protected router: Router, - protected analytics: Angulartics2, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected win: Window) { } @@ -34,7 +31,7 @@ export class TwoFactorOptionsComponent implements OnInit { } recover() { - this.analytics.eventTrack.next({ action: 'Selected Recover' }); + this.platformUtilsService.eventTrack('Selected Recover'); this.platformUtilsService.launchUri('https://help.bitwarden.com/article/lost-two-step-device/'); this.onRecoverSelected.emit(); } diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index dc08df0e9a..57daee1d64 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -4,8 +4,6 @@ import { } from '@angular/core'; import { Router } from '@angular/router'; -import { Angulartics2 } from 'angulartics2'; - import { DeviceType } from '../../enums/deviceType'; import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; @@ -42,7 +40,6 @@ export class TwoFactorComponent implements OnInit, OnDestroy { protected successRoute = 'vault'; constructor(protected authService: AuthService, protected router: Router, - protected analytics: Angulartics2, protected i18nService: I18nService, protected apiService: ApiService, protected platformUtilsService: PlatformUtilsService, protected win: Window, protected environmentService: EnvironmentService) { @@ -168,7 +165,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { if (this.onSuccessfulLogin != null) { this.onSuccessfulLogin(); } - this.analytics.eventTrack.next({ action: 'Logged In From Two-step' }); + this.platformUtilsService.eventTrack('Logged In From Two-step'); if (this.onSuccessfulLoginNavigate != null) { this.onSuccessfulLoginNavigate(); } else { diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index 11c5c16ca6..c257493e6a 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -8,8 +8,6 @@ import { Output, } from '@angular/core'; -import { Angulartics2 } from 'angulartics2'; - import { CipherType } from '../../enums/cipherType'; import { FieldType } from '../../enums/fieldType'; @@ -49,9 +47,8 @@ export class ViewComponent implements OnDestroy, OnInit { private totpInterval: any; constructor(protected cipherService: CipherService, protected totpService: TotpService, - protected tokenService: TokenService, + protected tokenService: TokenService, protected i18nService: I18nService, protected cryptoService: CryptoService, protected platformUtilsService: PlatformUtilsService, - protected i18nService: I18nService, protected analytics: Angulartics2, protected auditService: AuditService, protected win: Window, protected broadcasterService: BroadcasterService, protected ngZone: NgZone, protected changeDetectorRef: ChangeDetectorRef, protected userService: UserService) { } @@ -100,12 +97,12 @@ export class ViewComponent implements OnDestroy, OnInit { } togglePassword() { - this.analytics.eventTrack.next({ action: 'Toggled Password' }); + this.platformUtilsService.eventTrack('Toggled Password'); this.showPassword = !this.showPassword; } toggleCardCode() { - this.analytics.eventTrack.next({ action: 'Toggled Card Code' }); + this.platformUtilsService.eventTrack('Toggled Card Code'); this.showCardCode = !this.showCardCode; } @@ -114,7 +111,7 @@ export class ViewComponent implements OnDestroy, OnInit { return; } - this.analytics.eventTrack.next({ action: 'Check Password' }); + this.platformUtilsService.eventTrack('Check Password'); this.checkPasswordPromise = this.auditService.passwordLeaked(this.cipher.login.password); const matches = await this.checkPasswordPromise; @@ -136,7 +133,7 @@ export class ViewComponent implements OnDestroy, OnInit { return; } - this.analytics.eventTrack.next({ action: 'Launched Login URI' }); + this.platformUtilsService.eventTrack('Launched Login URI'); this.platformUtilsService.launchUri(uri.uri); } @@ -145,7 +142,7 @@ export class ViewComponent implements OnDestroy, OnInit { return; } - this.analytics.eventTrack.next({ action: 'Copied ' + aType }); + this.platformUtilsService.eventTrack('Copied ' + aType); const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(value, copyOptions); this.platformUtilsService.showToast('info', null, From 7ae640e5f8de3454fe8279aab2717abd6d12ca42 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 3 Oct 2018 00:09:07 -0400 Subject: [PATCH 0566/1626] implement eventTrack for electron --- src/electron/services/electronPlatformUtils.service.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index acf2845fcd..8dd08e73b3 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -172,6 +172,10 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { return Promise.resolve(result === 0); } + eventTrack(action: string, label?: string, options?: any) { + // TODO: track + } + isDev(): boolean { return isDev(); } From 6b3dc2344f2db167cbc404d83fa4602a1ca93ef8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 3 Oct 2018 09:41:41 -0400 Subject: [PATCH 0567/1626] move toast and event track to messaging for electron --- .../services/electronPlatformUtils.service.ts | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index 8dd08e73b3..c243558c92 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -13,6 +13,7 @@ import { import { DeviceType } from '../../enums/deviceType'; import { I18nService } from '../../abstractions/i18n.service'; +import { MessagingService } from '../../abstractions/messaging.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { AnalyticsIds } from '../../misc/analytics'; @@ -24,7 +25,8 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { private deviceCache: DeviceType = null; private analyticsIdCache: string = null; - constructor(private i18nService: I18nService, private isDesktopApp: boolean) { + constructor(private i18nService: I18nService, private messagingService: MessagingService, + private isDesktopApp: boolean) { this.identityClientId = isDesktopApp ? 'desktop' : 'connector'; } @@ -142,13 +144,12 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { showToast(type: 'error' | 'success' | 'warning' | 'info', title: string, text: string | string[], options?: any): void { - if ((options == null || options.global == null) && Utils.isBrowser) { - options.global = window; - } - if (options.global == null || options.global.BitwardenToasterService == null) { - throw new Error('BitwardenToasterService not available on global.'); - } - options.global.BitwardenToasterService.popAsync(type, title, text); + this.messagingService.send('showToast', { + text: text, + title: title, + type: type, + options: options, + }); } showDialog(text: string, title?: string, confirmText?: string, cancelText?: string, type?: string): @@ -173,7 +174,11 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { } eventTrack(action: string, label?: string, options?: any) { - // TODO: track + this.messagingService.send('analyticsEventTrack', { + action: action, + label: label, + options: options, + }); } isDev(): boolean { From 1f36c5fee6a294f455de8c10c3d7316f708ec214 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 4 Oct 2018 12:05:21 -0400 Subject: [PATCH 0568/1626] dont await void methods --- src/services/environment.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/environment.service.ts b/src/services/environment.service.ts index cfc6a5e6c0..28f0f286b6 100644 --- a/src/services/environment.service.ts +++ b/src/services/environment.service.ts @@ -42,7 +42,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { if (urls.base) { this.baseUrl = envUrls.base = urls.base; - await this.apiService.setUrls(envUrls); + this.apiService.setUrls(envUrls); return; } @@ -51,7 +51,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { this.identityUrl = envUrls.identity = urls.identity; this.iconsUrl = urls.icons; this.notificationsUrl = urls.notifications; - await this.apiService.setUrls(envUrls); + this.apiService.setUrls(envUrls); } async setUrls(urls: any): Promise { @@ -86,7 +86,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { envUrls.identity = this.identityUrl; } - await this.apiService.setUrls(envUrls); + this.apiService.setUrls(envUrls); if (this.notificationsService != null) { this.notificationsService.init(this); } From c3f67dbe26d7d5b30645a2857fd9f316fce7b6bc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 4 Oct 2018 14:38:15 -0400 Subject: [PATCH 0569/1626] rename domain to domainBase for weird nativescript issue --- src/models/domain/attachment.ts | 2 +- src/models/domain/card.ts | 2 +- src/models/domain/cipher.ts | 2 +- src/models/domain/collection.ts | 2 +- src/models/domain/{domain.ts => domainBase.ts} | 2 +- 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/domain/loginUri.ts | 2 +- src/models/domain/password.ts | 2 +- src/models/domain/secureNote.ts | 2 +- src/services/cipher.service.ts | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) rename src/models/domain/{domain.ts => domainBase.ts} (98%) diff --git a/src/models/domain/attachment.ts b/src/models/domain/attachment.ts index be345ce72f..cf50a18c03 100644 --- a/src/models/domain/attachment.ts +++ b/src/models/domain/attachment.ts @@ -1,7 +1,7 @@ import { AttachmentData } from '../data/attachmentData'; import { CipherString } from './cipherString'; -import Domain from './domain'; +import Domain from './domainBase'; import { AttachmentView } from '../view/attachmentView'; diff --git a/src/models/domain/card.ts b/src/models/domain/card.ts index a9a4791274..c1ef7a22d3 100644 --- a/src/models/domain/card.ts +++ b/src/models/domain/card.ts @@ -1,7 +1,7 @@ import { CardData } from '../data/cardData'; import { CipherString } from './cipherString'; -import Domain from './domain'; +import Domain from './domainBase'; import { CardView } from '../view/cardView'; diff --git a/src/models/domain/cipher.ts b/src/models/domain/cipher.ts index 1bbdd7675d..708774456c 100644 --- a/src/models/domain/cipher.ts +++ b/src/models/domain/cipher.ts @@ -7,7 +7,7 @@ import { CipherView } from '../view/cipherView'; import { Attachment } from './attachment'; import { Card } from './card'; import { CipherString } from './cipherString'; -import Domain from './domain'; +import Domain from './domainBase'; import { Field } from './field'; import { Identity } from './identity'; import { Login } from './login'; diff --git a/src/models/domain/collection.ts b/src/models/domain/collection.ts index 42886902d2..5d308f66b3 100644 --- a/src/models/domain/collection.ts +++ b/src/models/domain/collection.ts @@ -3,7 +3,7 @@ import { CollectionData } from '../data/collectionData'; import { CollectionView } from '../view/collectionView'; import { CipherString } from './cipherString'; -import Domain from './domain'; +import Domain from './domainBase'; export class Collection extends Domain { id: string; diff --git a/src/models/domain/domain.ts b/src/models/domain/domainBase.ts similarity index 98% rename from src/models/domain/domain.ts rename to src/models/domain/domainBase.ts index f5d27b0340..b73297bcd5 100644 --- a/src/models/domain/domain.ts +++ b/src/models/domain/domainBase.ts @@ -2,7 +2,7 @@ import { CipherString } from './cipherString'; import { View } from '../view/view'; -export default abstract class Domain { +export default class Domain { protected buildDomainModel(domain: D, dataObj: any, map: any, alreadyEncrypted: boolean, notEncList: any[] = []) { for (const prop in map) { diff --git a/src/models/domain/field.ts b/src/models/domain/field.ts index 82a884c849..2dec447f2b 100644 --- a/src/models/domain/field.ts +++ b/src/models/domain/field.ts @@ -3,7 +3,7 @@ import { FieldType } from '../../enums/fieldType'; import { FieldData } from '../data/fieldData'; import { CipherString } from './cipherString'; -import Domain from './domain'; +import Domain from './domainBase'; import { FieldView } from '../view/fieldView'; diff --git a/src/models/domain/folder.ts b/src/models/domain/folder.ts index c0a8d179a9..fc67788569 100644 --- a/src/models/domain/folder.ts +++ b/src/models/domain/folder.ts @@ -3,7 +3,7 @@ import { FolderData } from '../data/folderData'; import { FolderView } from '../view/folderView'; import { CipherString } from './cipherString'; -import Domain from './domain'; +import Domain from './domainBase'; export class Folder extends Domain { id: string; diff --git a/src/models/domain/identity.ts b/src/models/domain/identity.ts index 266d697579..87622e6c6a 100644 --- a/src/models/domain/identity.ts +++ b/src/models/domain/identity.ts @@ -1,7 +1,7 @@ import { IdentityData } from '../data/identityData'; import { CipherString } from './cipherString'; -import Domain from './domain'; +import Domain from './domainBase'; import { IdentityView } from '../view/identityView'; diff --git a/src/models/domain/login.ts b/src/models/domain/login.ts index dc721503f4..c40a830aba 100644 --- a/src/models/domain/login.ts +++ b/src/models/domain/login.ts @@ -5,7 +5,7 @@ import { LoginData } from '../data/loginData'; import { LoginView } from '../view/loginView'; import { CipherString } from './cipherString'; -import Domain from './domain'; +import Domain from './domainBase'; export class Login extends Domain { uris: LoginUri[]; diff --git a/src/models/domain/loginUri.ts b/src/models/domain/loginUri.ts index 1e1d9a93fc..631d5b0d02 100644 --- a/src/models/domain/loginUri.ts +++ b/src/models/domain/loginUri.ts @@ -5,7 +5,7 @@ import { LoginUriData } from '../data/loginUriData'; import { LoginUriView } from '../view/loginUriView'; import { CipherString } from './cipherString'; -import Domain from './domain'; +import Domain from './domainBase'; export class LoginUri extends Domain { uri: CipherString; diff --git a/src/models/domain/password.ts b/src/models/domain/password.ts index ef735ba0e1..5ef38a2010 100644 --- a/src/models/domain/password.ts +++ b/src/models/domain/password.ts @@ -1,7 +1,7 @@ import { PasswordHistoryData } from '../data/passwordHistoryData'; import { CipherString } from './cipherString'; -import Domain from './domain'; +import Domain from './domainBase'; import { PasswordHistoryView } from '../view/passwordHistoryView'; diff --git a/src/models/domain/secureNote.ts b/src/models/domain/secureNote.ts index 52988f1b25..620571fbb7 100644 --- a/src/models/domain/secureNote.ts +++ b/src/models/domain/secureNote.ts @@ -2,7 +2,7 @@ import { SecureNoteType } from '../../enums/secureNoteType'; import { SecureNoteData } from '../data/secureNoteData'; -import Domain from './domain'; +import Domain from './domainBase'; import { SecureNoteView } from '../view/secureNoteView'; diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index c6c6a7aa2b..b201c171bd 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -8,7 +8,7 @@ import { Attachment } from '../models/domain/attachment'; import { Card } from '../models/domain/card'; import { Cipher } from '../models/domain/cipher'; import { CipherString } from '../models/domain/cipherString'; -import Domain from '../models/domain/domain'; +import Domain from '../models/domain/domainBase'; import { Field } from '../models/domain/field'; import { Identity } from '../models/domain/identity'; import { Login } from '../models/domain/login'; From 4b7962dc8fba73003be1ae651013f5f817496551 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 8 Oct 2018 14:21:53 -0400 Subject: [PATCH 0570/1626] add support for u2f setup apis --- src/abstractions/api.service.ts | 8 +++++++- .../request/updateTwoFactorU2fDeleteRequest.ts | 5 +++++ src/models/request/updateTwoFactorU2fRequest.ts | 2 ++ src/models/response/twoFactorU2fResponse.ts | 16 ++++++++++++++-- src/services/api.service.ts | 16 +++++++++++++++- 5 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 src/models/request/updateTwoFactorU2fDeleteRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index d931034139..0410fab49b 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -42,6 +42,7 @@ import { UpdateProfileRequest } from '../models/request/updateProfileRequest'; import { UpdateTwoFactorAuthenticatorRequest } from '../models/request/updateTwoFactorAuthenticatorRequest'; import { UpdateTwoFactorDuoRequest } from '../models/request/updateTwoFactorDuoRequest'; import { UpdateTwoFactorEmailRequest } from '../models/request/updateTwoFactorEmailRequest'; +import { UpdateTwoFactorU2fDeleteRequest } from '../models/request/updateTwoFactorU2fDeleteRequest'; import { UpdateTwoFactorU2fRequest } from '../models/request/updateTwoFactorU2fRequest'; import { UpdateTwoFactorYubioOtpRequest } from '../models/request/updateTwoFactorYubioOtpRequest'; import { VerifyBankRequest } from '../models/request/verifyBankRequest'; @@ -80,7 +81,10 @@ import { TwoFactorDuoResponse } from '../models/response/twoFactorDuoResponse'; import { TwoFactorEmailResponse } from '../models/response/twoFactorEmailResponse'; import { TwoFactorProviderResponse } from '../models/response/twoFactorProviderResponse'; import { TwoFactorRecoverResponse } from '../models/response/twoFactorRescoverResponse'; -import { TwoFactorU2fResponse } from '../models/response/twoFactorU2fResponse'; +import { + ChallengeResponse, + TwoFactorU2fResponse, +} from '../models/response/twoFactorU2fResponse'; import { TwoFactorYubiKeyResponse } from '../models/response/twoFactorYubiKeyResponse'; import { UserKeyResponse } from '../models/response/userKeyResponse'; @@ -195,6 +199,7 @@ export abstract class ApiService { request: PasswordVerificationRequest) => Promise; getTwoFactorYubiKey: (request: PasswordVerificationRequest) => Promise; getTwoFactorU2f: (request: PasswordVerificationRequest) => Promise; + getTwoFactorU2fChallenge: (request: PasswordVerificationRequest) => Promise; getTwoFactorRecover: (request: PasswordVerificationRequest) => Promise; putTwoFactorAuthenticator: ( request: UpdateTwoFactorAuthenticatorRequest) => Promise; @@ -204,6 +209,7 @@ export abstract class ApiService { request: UpdateTwoFactorDuoRequest) => Promise; putTwoFactorYubiKey: (request: UpdateTwoFactorYubioOtpRequest) => Promise; putTwoFactorU2f: (request: UpdateTwoFactorU2fRequest) => Promise; + deleteTwoFactorU2f: (request: UpdateTwoFactorU2fDeleteRequest) => Promise; putTwoFactorDisable: (request: TwoFactorProviderRequest) => Promise; putTwoFactorOrganizationDisable: (organizationId: string, request: TwoFactorProviderRequest) => Promise; diff --git a/src/models/request/updateTwoFactorU2fDeleteRequest.ts b/src/models/request/updateTwoFactorU2fDeleteRequest.ts new file mode 100644 index 0000000000..01066a301e --- /dev/null +++ b/src/models/request/updateTwoFactorU2fDeleteRequest.ts @@ -0,0 +1,5 @@ +import { PasswordVerificationRequest } from './passwordVerificationRequest'; + +export class UpdateTwoFactorU2fDeleteRequest extends PasswordVerificationRequest { + id: number; +} diff --git a/src/models/request/updateTwoFactorU2fRequest.ts b/src/models/request/updateTwoFactorU2fRequest.ts index a0e959bd18..0b3fb7c2be 100644 --- a/src/models/request/updateTwoFactorU2fRequest.ts +++ b/src/models/request/updateTwoFactorU2fRequest.ts @@ -2,4 +2,6 @@ import { PasswordVerificationRequest } from './passwordVerificationRequest'; export class UpdateTwoFactorU2fRequest extends PasswordVerificationRequest { deviceResponse: string; + name: string; + id: number; } diff --git a/src/models/response/twoFactorU2fResponse.ts b/src/models/response/twoFactorU2fResponse.ts index 7ab62cc627..62ae93d68c 100644 --- a/src/models/response/twoFactorU2fResponse.ts +++ b/src/models/response/twoFactorU2fResponse.ts @@ -1,10 +1,22 @@ export class TwoFactorU2fResponse { enabled: boolean; - challenge: ChallengeResponse; + keys: KeyResponse[]; constructor(response: any) { this.enabled = response.Enabled; - this.challenge = response.Challenge == null ? null : new ChallengeResponse(response.Challenge); + this.keys = response.Keys == null ? null : response.Keys.map((k: any) => new KeyResponse(k)); + } +} + +export class KeyResponse { + name: string; + id: number; + compromised: boolean; + + constructor(response: any) { + this.name = response.Name; + this.id = response.Id; + this.compromised = response.Compromised; } } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 5acfe6f99a..18fdd9935c 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -48,6 +48,7 @@ import { UpdateProfileRequest } from '../models/request/updateProfileRequest'; import { UpdateTwoFactorAuthenticatorRequest } from '../models/request/updateTwoFactorAuthenticatorRequest'; import { UpdateTwoFactorDuoRequest } from '../models/request/updateTwoFactorDuoRequest'; import { UpdateTwoFactorEmailRequest } from '../models/request/updateTwoFactorEmailRequest'; +import { UpdateTwoFactorU2fDeleteRequest } from '../models/request/updateTwoFactorU2fDeleteRequest'; import { UpdateTwoFactorU2fRequest } from '../models/request/updateTwoFactorU2fRequest'; import { UpdateTwoFactorYubioOtpRequest } from '../models/request/updateTwoFactorYubioOtpRequest'; import { VerifyBankRequest } from '../models/request/verifyBankRequest'; @@ -87,7 +88,10 @@ import { TwoFactorDuoResponse } from '../models/response/twoFactorDuoResponse'; import { TwoFactorEmailResponse } from '../models/response/twoFactorEmailResponse'; import { TwoFactorProviderResponse } from '../models/response/twoFactorProviderResponse'; import { TwoFactorRecoverResponse } from '../models/response/twoFactorRescoverResponse'; -import { TwoFactorU2fResponse } from '../models/response/twoFactorU2fResponse'; +import { + ChallengeResponse, + TwoFactorU2fResponse, +} from '../models/response/twoFactorU2fResponse'; import { TwoFactorYubiKeyResponse } from '../models/response/twoFactorYubiKeyResponse'; import { UserKeyResponse } from '../models/response/userKeyResponse'; @@ -618,6 +622,11 @@ export class ApiService implements ApiServiceAbstraction { return new TwoFactorU2fResponse(r); } + async getTwoFactorU2fChallenge(request: PasswordVerificationRequest): Promise { + const r = await this.send('POST', '/two-factor/get-u2f-challenge', request, true, true); + return new ChallengeResponse(r); + } + async getTwoFactorRecover(request: PasswordVerificationRequest): Promise { const r = await this.send('POST', '/two-factor/get-recover', request, true, true); return new TwoFactorRecoverResponse(r); @@ -655,6 +664,11 @@ export class ApiService implements ApiServiceAbstraction { return new TwoFactorU2fResponse(r); } + async deleteTwoFactorU2f(request: UpdateTwoFactorU2fDeleteRequest): Promise { + const r = await this.send('DELETE', '/two-factor/u2f', request, true, true); + return new TwoFactorU2fResponse(r); + } + async putTwoFactorDisable(request: TwoFactorProviderRequest): Promise { const r = await this.send('PUT', '/two-factor/disable', request, true, true); return new TwoFactorProviderResponse(r); From 30a5257671b2a10af0d68db575782ac2cb30c729 Mon Sep 17 00:00:00 2001 From: ServiusHack Date: Mon, 8 Oct 2018 21:41:32 +0200 Subject: [PATCH 0571/1626] Add importer for Passman (#13) * Add importer for Passman The importer reads JSON files generated by the Nextcloud app Passman. The first tag is used as the folder name. This works well if passwords have no or only one tag. If no username is set then the email address is used as the username. Files are not being imported. * Fix indentation * Remove unsettable revision date * Fix tslint findings * Add email to notes when there is also a username If a username is set, that will become the username. Otherwise the email will become the username. If a username and an email is set the email will be added to the notes. --- src/importers/passmanJsonImporter.ts | 63 ++++++++++++++++++++++++++++ src/services/import.service.ts | 4 ++ 2 files changed, 67 insertions(+) create mode 100644 src/importers/passmanJsonImporter.ts diff --git a/src/importers/passmanJsonImporter.ts b/src/importers/passmanJsonImporter.ts new file mode 100644 index 0000000000..8abe053b58 --- /dev/null +++ b/src/importers/passmanJsonImporter.ts @@ -0,0 +1,63 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +export class PassmanJsonImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = JSON.parse(data); + if (results == null || results.length === 0) { + result.success = false; + return result; + } + + results.forEach((credential: any) => { + if (credential.tags.length > 0) { + const folderName = credential.tags[0].text; + this.processFolder(result, folderName); + } + + const cipher = this.initLoginCipher(); + cipher.name = credential.label; + if (cipher.name.length > 30) { + cipher.name = cipher.name.substring(0, 30); + } + + cipher.login.username = this.getValueOrDefault(credential.username); + if (cipher.login.username === null) { + cipher.login.username = this.getValueOrDefault(credential.email); + } else if (credential.email !== '') { + cipher.notes += 'E-Mail: ' + credential.email; + } + + cipher.login.password = this.getValueOrDefault(credential.password); + cipher.login.uris = this.makeUriArray(credential.url); + cipher.notes = this.getValueOrDefault(credential.description); + + if (credential.otp) { + cipher.login.totp = credential.otp.secret; + } + + credential.custom_fields.forEach((customField: any) => { + switch (customField.field_type) { + case 'text': + case 'password': + this.processKvp(cipher, customField.label, customField.value); + break; + } + }); + + this.convertToNoteIfNeeded(cipher); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + if (this.organization) { + this.moveFoldersToCollections(result); + } + + result.success = true; + return result; + } +} diff --git a/src/services/import.service.ts b/src/services/import.service.ts index f6548989e5..d5dbed16f9 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -41,6 +41,7 @@ import { OnePasswordWinCsvImporter } from '../importers/onepasswordWinCsvImporte import { PadlockCsvImporter } from '../importers/padlockCsvImporter'; import { PassKeepCsvImporter } from '../importers/passkeepCsvImporter'; import { PasspackCsvImporter } from '../importers/passpackCsvImporter'; +import { PassmanJsonImporter } from '../importers/passmanJsonImporter'; import { PasswordAgentCsvImporter } from '../importers/passwordAgentCsvImporter'; import { PasswordBossJsonImporter } from '../importers/passwordBossJsonImporter'; import { PasswordDragonXmlImporter } from '../importers/passwordDragonXmlImporter'; @@ -95,6 +96,7 @@ export class ImportService implements ImportServiceAbstraction { { id: 'blurcsv', name: 'Blur (csv)' }, { id: 'passwordagentcsv', name: 'Password Agent (csv)' }, { id: 'passpackcsv', name: 'Passpack (csv)' }, + { id: 'passmanjson', name: 'Passman (json)' }, ]; constructor(private cipherService: CipherService, private folderService: FolderService, @@ -211,6 +213,8 @@ export class ImportService implements ImportServiceAbstraction { return new PasswordAgentCsvImporter(); case 'passpackcsv': return new PasspackCsvImporter(); + case 'passmanjson': + return new PassmanJsonImporter(); default: return null; } From e0c32bebc74dcc801cea7a327aa540e91fc9bb17 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 8 Oct 2018 15:49:38 -0400 Subject: [PATCH 0572/1626] null checks and fix email in notes --- src/importers/passmanJsonImporter.ts | 35 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/importers/passmanJsonImporter.ts b/src/importers/passmanJsonImporter.ts index 8abe053b58..01110fac12 100644 --- a/src/importers/passmanJsonImporter.ts +++ b/src/importers/passmanJsonImporter.ts @@ -13,40 +13,41 @@ export class PassmanJsonImporter extends BaseImporter implements Importer { } results.forEach((credential: any) => { - if (credential.tags.length > 0) { + if (credential.tags != null && credential.tags.length > 0) { const folderName = credential.tags[0].text; this.processFolder(result, folderName); } const cipher = this.initLoginCipher(); cipher.name = credential.label; - if (cipher.name.length > 30) { + if (cipher.name != null && cipher.name.length > 30) { cipher.name = cipher.name.substring(0, 30); } cipher.login.username = this.getValueOrDefault(credential.username); - if (cipher.login.username === null) { + if (this.isNullOrWhitespace(cipher.login.username)) { cipher.login.username = this.getValueOrDefault(credential.email); - } else if (credential.email !== '') { - cipher.notes += 'E-Mail: ' + credential.email; + } else if (!this.isNullOrWhitespace(credential.email)) { + cipher.notes = ('Email: ' + credential.email + '\n'); } cipher.login.password = this.getValueOrDefault(credential.password); cipher.login.uris = this.makeUriArray(credential.url); - cipher.notes = this.getValueOrDefault(credential.description); - - if (credential.otp) { - cipher.login.totp = credential.otp.secret; + cipher.notes += this.getValueOrDefault(credential.description); + if (credential.otp != null) { + cipher.login.totp = this.getValueOrDefault(credential.otp.secret); } - credential.custom_fields.forEach((customField: any) => { - switch (customField.field_type) { - case 'text': - case 'password': - this.processKvp(cipher, customField.label, customField.value); - break; - } - }); + if (credential.custom_fields != null) { + credential.custom_fields.forEach((customField: any) => { + switch (customField.field_type) { + case 'text': + case 'password': + this.processKvp(cipher, customField.label, customField.value); + break; + } + }); + } this.convertToNoteIfNeeded(cipher); this.cleanupCipher(cipher); From 7b3fce17796bc7b415b65b7e869e8aa081eae022 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 8 Oct 2018 15:50:58 -0400 Subject: [PATCH 0573/1626] default empty string on description to notes --- src/importers/passmanJsonImporter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/importers/passmanJsonImporter.ts b/src/importers/passmanJsonImporter.ts index 01110fac12..08c39cef92 100644 --- a/src/importers/passmanJsonImporter.ts +++ b/src/importers/passmanJsonImporter.ts @@ -33,7 +33,7 @@ export class PassmanJsonImporter extends BaseImporter implements Importer { cipher.login.password = this.getValueOrDefault(credential.password); cipher.login.uris = this.makeUriArray(credential.url); - cipher.notes += this.getValueOrDefault(credential.description); + cipher.notes += this.getValueOrDefault(credential.description, ''); if (credential.otp != null) { cipher.login.totp = this.getValueOrDefault(credential.otp.secret); } From c4da05dbb03d7df71e2d503658ecdce1221aa3f7 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Mon, 8 Oct 2018 23:26:13 +0200 Subject: [PATCH 0574/1626] Add a passphrase generation mechanism (#12) Based on EFF's wordlist The wordlist was selected based on arguments mentionned in https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases over Arnold Reinhold's Diceware list from 1995 such as avoid short, deused or diffcult to pronounce words --- .../password-generator.component.ts | 10 + src/misc/wordlist.ts | 7777 +++++++++++++++++ src/services/passwordGeneration.service.ts | 18 + 3 files changed, 7805 insertions(+) create mode 100644 src/misc/wordlist.ts diff --git a/src/angular/components/password-generator.component.ts b/src/angular/components/password-generator.component.ts index f5fb6461b2..d95a76bcec 100644 --- a/src/angular/components/password-generator.component.ts +++ b/src/angular/components/password-generator.component.ts @@ -78,6 +78,12 @@ export class PasswordGeneratorComponent implements OnInit { this.options.minUppercase = 0; this.options.ambiguous = !this.avoidAmbiguous; + let typePassword = false; + if (!this.options.generateTypePassword || this.options.generateTypePassword === 'generate-password') { + typePassword = true; + } + this.options.generatePassphrase = !typePassword; + if (!this.options.uppercase && !this.options.lowercase && !this.options.number && !this.options.special) { this.options.lowercase = true; const lowercase = document.querySelector('#lowercase') as HTMLInputElement; @@ -111,5 +117,9 @@ export class PasswordGeneratorComponent implements OnInit { if (this.options.minSpecial + this.options.minNumber > this.options.length) { this.options.minSpecial = this.options.length - this.options.minNumber; } + + if (!this.options.numWords || this.options.length < 3) { + this.options.numWords = 3; + } } } diff --git a/src/misc/wordlist.ts b/src/misc/wordlist.ts new file mode 100644 index 0000000000..1850a07958 --- /dev/null +++ b/src/misc/wordlist.ts @@ -0,0 +1,7777 @@ +// EFF's Long Wordlist from https://www.eff.org/dice +export const WordList = ['abacus', +'abdomen', +'abdominal', +'abide', +'abiding', +'ability', +'ablaze', +'able', +'abnormal', +'abrasion', +'abrasive', +'abreast', +'abridge', +'abroad', +'abruptly', +'absence', +'absentee', +'absently', +'absinthe', +'absolute', +'absolve', +'abstain', +'abstract', +'absurd', +'accent', +'acclaim', +'acclimate', +'accompany', +'account', +'accuracy', +'accurate', +'accustom', +'acetone', +'achiness', +'aching', +'acid', +'acorn', +'acquaint', +'acquire', +'acre', +'acrobat', +'acronym', +'acting', +'action', +'activate', +'activator', +'active', +'activism', +'activist', +'activity', +'actress', +'acts', +'acutely', +'acuteness', +'aeration', +'aerobics', +'aerosol', +'aerospace', +'afar', +'affair', +'affected', +'affecting', +'affection', +'affidavit', +'affiliate', +'affirm', +'affix', +'afflicted', +'affluent', +'afford', +'affront', +'aflame', +'afloat', +'aflutter', +'afoot', +'afraid', +'afterglow', +'afterlife', +'aftermath', +'aftermost', +'afternoon', +'aged', +'ageless', +'agency', +'agenda', +'agent', +'aggregate', +'aghast', +'agile', +'agility', +'aging', +'agnostic', +'agonize', +'agonizing', +'agony', +'agreeable', +'agreeably', +'agreed', +'agreeing', +'agreement', +'aground', +'ahead', +'ahoy', +'aide', +'aids', +'aim', +'ajar', +'alabaster', +'alarm', +'albatross', +'album', +'alfalfa', +'algebra', +'algorithm', +'alias', +'alibi', +'alienable', +'alienate', +'aliens', +'alike', +'alive', +'alkaline', +'alkalize', +'almanac', +'almighty', +'almost', +'aloe', +'aloft', +'aloha', +'alone', +'alongside', +'aloof', +'alphabet', +'alright', +'although', +'altitude', +'alto', +'aluminum', +'alumni', +'always', +'amaretto', +'amaze', +'amazingly', +'amber', +'ambiance', +'ambiguity', +'ambiguous', +'ambition', +'ambitious', +'ambulance', +'ambush', +'amendable', +'amendment', +'amends', +'amenity', +'amiable', +'amicably', +'amid', +'amigo', +'amino', +'amiss', +'ammonia', +'ammonium', +'amnesty', +'amniotic', +'among', +'amount', +'amperage', +'ample', +'amplifier', +'amplify', +'amply', +'amuck', +'amulet', +'amusable', +'amused', +'amusement', +'amuser', +'amusing', +'anaconda', +'anaerobic', +'anagram', +'anatomist', +'anatomy', +'anchor', +'anchovy', +'ancient', +'android', +'anemia', +'anemic', +'aneurism', +'anew', +'angelfish', +'angelic', +'anger', +'angled', +'angler', +'angles', +'angling', +'angrily', +'angriness', +'anguished', +'angular', +'animal', +'animate', +'animating', +'animation', +'animator', +'anime', +'animosity', +'ankle', +'annex', +'annotate', +'announcer', +'annoying', +'annually', +'annuity', +'anointer', +'another', +'answering', +'antacid', +'antarctic', +'anteater', +'antelope', +'antennae', +'anthem', +'anthill', +'anthology', +'antibody', +'antics', +'antidote', +'antihero', +'antiquely', +'antiques', +'antiquity', +'antirust', +'antitoxic', +'antitrust', +'antiviral', +'antivirus', +'antler', +'antonym', +'antsy', +'anvil', +'anybody', +'anyhow', +'anymore', +'anyone', +'anyplace', +'anything', +'anytime', +'anyway', +'anywhere', +'aorta', +'apache', +'apostle', +'appealing', +'appear', +'appease', +'appeasing', +'appendage', +'appendix', +'appetite', +'appetizer', +'applaud', +'applause', +'apple', +'appliance', +'applicant', +'applied', +'apply', +'appointee', +'appraisal', +'appraiser', +'apprehend', +'approach', +'approval', +'approve', +'apricot', +'april', +'apron', +'aptitude', +'aptly', +'aqua', +'aqueduct', +'arbitrary', +'arbitrate', +'ardently', +'area', +'arena', +'arguable', +'arguably', +'argue', +'arise', +'armadillo', +'armband', +'armchair', +'armed', +'armful', +'armhole', +'arming', +'armless', +'armoire', +'armored', +'armory', +'armrest', +'army', +'aroma', +'arose', +'around', +'arousal', +'arrange', +'array', +'arrest', +'arrival', +'arrive', +'arrogance', +'arrogant', +'arson', +'art', +'ascend', +'ascension', +'ascent', +'ascertain', +'ashamed', +'ashen', +'ashes', +'ashy', +'aside', +'askew', +'asleep', +'asparagus', +'aspect', +'aspirate', +'aspire', +'aspirin', +'astonish', +'astound', +'astride', +'astrology', +'astronaut', +'astronomy', +'astute', +'atlantic', +'atlas', +'atom', +'atonable', +'atop', +'atrium', +'atrocious', +'atrophy', +'attach', +'attain', +'attempt', +'attendant', +'attendee', +'attention', +'attentive', +'attest', +'attic', +'attire', +'attitude', +'attractor', +'attribute', +'atypical', +'auction', +'audacious', +'audacity', +'audible', +'audibly', +'audience', +'audio', +'audition', +'augmented', +'august', +'authentic', +'author', +'autism', +'autistic', +'autograph', +'automaker', +'automated', +'automatic', +'autopilot', +'available', +'avalanche', +'avatar', +'avenge', +'avenging', +'avenue', +'average', +'aversion', +'avert', +'aviation', +'aviator', +'avid', +'avoid', +'await', +'awaken', +'award', +'aware', +'awhile', +'awkward', +'awning', +'awoke', +'awry', +'axis', +'babble', +'babbling', +'babied', +'baboon', +'backache', +'backboard', +'backboned', +'backdrop', +'backed', +'backer', +'backfield', +'backfire', +'backhand', +'backing', +'backlands', +'backlash', +'backless', +'backlight', +'backlit', +'backlog', +'backpack', +'backpedal', +'backrest', +'backroom', +'backshift', +'backside', +'backslid', +'backspace', +'backspin', +'backstab', +'backstage', +'backtalk', +'backtrack', +'backup', +'backward', +'backwash', +'backwater', +'backyard', +'bacon', +'bacteria', +'bacterium', +'badass', +'badge', +'badland', +'badly', +'badness', +'baffle', +'baffling', +'bagel', +'bagful', +'baggage', +'bagged', +'baggie', +'bagginess', +'bagging', +'baggy', +'bagpipe', +'baguette', +'baked', +'bakery', +'bakeshop', +'baking', +'balance', +'balancing', +'balcony', +'balmy', +'balsamic', +'bamboo', +'banana', +'banish', +'banister', +'banjo', +'bankable', +'bankbook', +'banked', +'banker', +'banking', +'banknote', +'bankroll', +'banner', +'bannister', +'banshee', +'banter', +'barbecue', +'barbed', +'barbell', +'barber', +'barcode', +'barge', +'bargraph', +'barista', +'baritone', +'barley', +'barmaid', +'barman', +'barn', +'barometer', +'barrack', +'barracuda', +'barrel', +'barrette', +'barricade', +'barrier', +'barstool', +'bartender', +'barterer', +'bash', +'basically', +'basics', +'basil', +'basin', +'basis', +'basket', +'batboy', +'batch', +'bath', +'baton', +'bats', +'battalion', +'battered', +'battering', +'battery', +'batting', +'battle', +'bauble', +'bazooka', +'blabber', +'bladder', +'blade', +'blah', +'blame', +'blaming', +'blanching', +'blandness', +'blank', +'blaspheme', +'blasphemy', +'blast', +'blatancy', +'blatantly', +'blazer', +'blazing', +'bleach', +'bleak', +'bleep', +'blemish', +'blend', +'bless', +'blighted', +'blimp', +'bling', +'blinked', +'blinker', +'blinking', +'blinks', +'blip', +'blissful', +'blitz', +'blizzard', +'bloated', +'bloating', +'blob', +'blog', +'bloomers', +'blooming', +'blooper', +'blot', +'blouse', +'blubber', +'bluff', +'bluish', +'blunderer', +'blunt', +'blurb', +'blurred', +'blurry', +'blurt', +'blush', +'blustery', +'boaster', +'boastful', +'boasting', +'boat', +'bobbed', +'bobbing', +'bobble', +'bobcat', +'bobsled', +'bobtail', +'bodacious', +'body', +'bogged', +'boggle', +'bogus', +'boil', +'bok', +'bolster', +'bolt', +'bonanza', +'bonded', +'bonding', +'bondless', +'boned', +'bonehead', +'boneless', +'bonelike', +'boney', +'bonfire', +'bonnet', +'bonsai', +'bonus', +'bony', +'boogeyman', +'boogieman', +'book', +'boondocks', +'booted', +'booth', +'bootie', +'booting', +'bootlace', +'bootleg', +'boots', +'boozy', +'borax', +'boring', +'borough', +'borrower', +'borrowing', +'boss', +'botanical', +'botanist', +'botany', +'botch', +'both', +'bottle', +'bottling', +'bottom', +'bounce', +'bouncing', +'bouncy', +'bounding', +'boundless', +'bountiful', +'bovine', +'boxcar', +'boxer', +'boxing', +'boxlike', +'boxy', +'breach', +'breath', +'breeches', +'breeching', +'breeder', +'breeding', +'breeze', +'breezy', +'brethren', +'brewery', +'brewing', +'briar', +'bribe', +'brick', +'bride', +'bridged', +'brigade', +'bright', +'brilliant', +'brim', +'bring', +'brink', +'brisket', +'briskly', +'briskness', +'bristle', +'brittle', +'broadband', +'broadcast', +'broaden', +'broadly', +'broadness', +'broadside', +'broadways', +'broiler', +'broiling', +'broken', +'broker', +'bronchial', +'bronco', +'bronze', +'bronzing', +'brook', +'broom', +'brought', +'browbeat', +'brownnose', +'browse', +'browsing', +'bruising', +'brunch', +'brunette', +'brunt', +'brush', +'brussels', +'brute', +'brutishly', +'bubble', +'bubbling', +'bubbly', +'buccaneer', +'bucked', +'bucket', +'buckle', +'buckshot', +'buckskin', +'bucktooth', +'buckwheat', +'buddhism', +'buddhist', +'budding', +'buddy', +'budget', +'buffalo', +'buffed', +'buffer', +'buffing', +'buffoon', +'buggy', +'bulb', +'bulge', +'bulginess', +'bulgur', +'bulk', +'bulldog', +'bulldozer', +'bullfight', +'bullfrog', +'bullhorn', +'bullion', +'bullish', +'bullpen', +'bullring', +'bullseye', +'bullwhip', +'bully', +'bunch', +'bundle', +'bungee', +'bunion', +'bunkbed', +'bunkhouse', +'bunkmate', +'bunny', +'bunt', +'busboy', +'bush', +'busily', +'busload', +'bust', +'busybody', +'buzz', +'cabana', +'cabbage', +'cabbie', +'cabdriver', +'cable', +'caboose', +'cache', +'cackle', +'cacti', +'cactus', +'caddie', +'caddy', +'cadet', +'cadillac', +'cadmium', +'cage', +'cahoots', +'cake', +'calamari', +'calamity', +'calcium', +'calculate', +'calculus', +'caliber', +'calibrate', +'calm', +'caloric', +'calorie', +'calzone', +'camcorder', +'cameo', +'camera', +'camisole', +'camper', +'campfire', +'camping', +'campsite', +'campus', +'canal', +'canary', +'cancel', +'candied', +'candle', +'candy', +'cane', +'canine', +'canister', +'cannabis', +'canned', +'canning', +'cannon', +'cannot', +'canola', +'canon', +'canopener', +'canopy', +'canteen', +'canyon', +'capable', +'capably', +'capacity', +'cape', +'capillary', +'capital', +'capitol', +'capped', +'capricorn', +'capsize', +'capsule', +'caption', +'captivate', +'captive', +'captivity', +'capture', +'caramel', +'carat', +'caravan', +'carbon', +'cardboard', +'carded', +'cardiac', +'cardigan', +'cardinal', +'cardstock', +'carefully', +'caregiver', +'careless', +'caress', +'caretaker', +'cargo', +'caring', +'carless', +'carload', +'carmaker', +'carnage', +'carnation', +'carnival', +'carnivore', +'carol', +'carpenter', +'carpentry', +'carpool', +'carport', +'carried', +'carrot', +'carrousel', +'carry', +'cartel', +'cartload', +'carton', +'cartoon', +'cartridge', +'cartwheel', +'carve', +'carving', +'carwash', +'cascade', +'case', +'cash', +'casing', +'casino', +'casket', +'cassette', +'casually', +'casualty', +'catacomb', +'catalog', +'catalyst', +'catalyze', +'catapult', +'cataract', +'catatonic', +'catcall', +'catchable', +'catcher', +'catching', +'catchy', +'caterer', +'catering', +'catfight', +'catfish', +'cathedral', +'cathouse', +'catlike', +'catnap', +'catnip', +'catsup', +'cattail', +'cattishly', +'cattle', +'catty', +'catwalk', +'caucasian', +'caucus', +'causal', +'causation', +'cause', +'causing', +'cauterize', +'caution', +'cautious', +'cavalier', +'cavalry', +'caviar', +'cavity', +'cedar', +'celery', +'celestial', +'celibacy', +'celibate', +'celtic', +'cement', +'census', +'ceramics', +'ceremony', +'certainly', +'certainty', +'certified', +'certify', +'cesarean', +'cesspool', +'chafe', +'chaffing', +'chain', +'chair', +'chalice', +'challenge', +'chamber', +'chamomile', +'champion', +'chance', +'change', +'channel', +'chant', +'chaos', +'chaperone', +'chaplain', +'chapped', +'chaps', +'chapter', +'character', +'charbroil', +'charcoal', +'charger', +'charging', +'chariot', +'charity', +'charm', +'charred', +'charter', +'charting', +'chase', +'chasing', +'chaste', +'chastise', +'chastity', +'chatroom', +'chatter', +'chatting', +'chatty', +'cheating', +'cheddar', +'cheek', +'cheer', +'cheese', +'cheesy', +'chef', +'chemicals', +'chemist', +'chemo', +'cherisher', +'cherub', +'chess', +'chest', +'chevron', +'chevy', +'chewable', +'chewer', +'chewing', +'chewy', +'chief', +'chihuahua', +'childcare', +'childhood', +'childish', +'childless', +'childlike', +'chili', +'chill', +'chimp', +'chip', +'chirping', +'chirpy', +'chitchat', +'chivalry', +'chive', +'chloride', +'chlorine', +'choice', +'chokehold', +'choking', +'chomp', +'chooser', +'choosing', +'choosy', +'chop', +'chosen', +'chowder', +'chowtime', +'chrome', +'chubby', +'chuck', +'chug', +'chummy', +'chump', +'chunk', +'churn', +'chute', +'cider', +'cilantro', +'cinch', +'cinema', +'cinnamon', +'circle', +'circling', +'circular', +'circulate', +'circus', +'citable', +'citadel', +'citation', +'citizen', +'citric', +'citrus', +'city', +'civic', +'civil', +'clad', +'claim', +'clambake', +'clammy', +'clamor', +'clamp', +'clamshell', +'clang', +'clanking', +'clapped', +'clapper', +'clapping', +'clarify', +'clarinet', +'clarity', +'clash', +'clasp', +'class', +'clatter', +'clause', +'clavicle', +'claw', +'clay', +'clean', +'clear', +'cleat', +'cleaver', +'cleft', +'clench', +'clergyman', +'clerical', +'clerk', +'clever', +'clicker', +'client', +'climate', +'climatic', +'cling', +'clinic', +'clinking', +'clip', +'clique', +'cloak', +'clobber', +'clock', +'clone', +'cloning', +'closable', +'closure', +'clothes', +'clothing', +'cloud', +'clover', +'clubbed', +'clubbing', +'clubhouse', +'clump', +'clumsily', +'clumsy', +'clunky', +'clustered', +'clutch', +'clutter', +'coach', +'coagulant', +'coastal', +'coaster', +'coasting', +'coastland', +'coastline', +'coat', +'coauthor', +'cobalt', +'cobbler', +'cobweb', +'cocoa', +'coconut', +'cod', +'coeditor', +'coerce', +'coexist', +'coffee', +'cofounder', +'cognition', +'cognitive', +'cogwheel', +'coherence', +'coherent', +'cohesive', +'coil', +'coke', +'cola', +'cold', +'coleslaw', +'coliseum', +'collage', +'collapse', +'collar', +'collected', +'collector', +'collide', +'collie', +'collision', +'colonial', +'colonist', +'colonize', +'colony', +'colossal', +'colt', +'coma', +'come', +'comfort', +'comfy', +'comic', +'coming', +'comma', +'commence', +'commend', +'comment', +'commerce', +'commode', +'commodity', +'commodore', +'common', +'commotion', +'commute', +'commuting', +'compacted', +'compacter', +'compactly', +'compactor', +'companion', +'company', +'compare', +'compel', +'compile', +'comply', +'component', +'composed', +'composer', +'composite', +'compost', +'composure', +'compound', +'compress', +'comprised', +'computer', +'computing', +'comrade', +'concave', +'conceal', +'conceded', +'concept', +'concerned', +'concert', +'conch', +'concierge', +'concise', +'conclude', +'concrete', +'concur', +'condense', +'condiment', +'condition', +'condone', +'conducive', +'conductor', +'conduit', +'cone', +'confess', +'confetti', +'confidant', +'confident', +'confider', +'confiding', +'configure', +'confined', +'confining', +'confirm', +'conflict', +'conform', +'confound', +'confront', +'confused', +'confusing', +'confusion', +'congenial', +'congested', +'congrats', +'congress', +'conical', +'conjoined', +'conjure', +'conjuror', +'connected', +'connector', +'consensus', +'consent', +'console', +'consoling', +'consonant', +'constable', +'constant', +'constrain', +'constrict', +'construct', +'consult', +'consumer', +'consuming', +'contact', +'container', +'contempt', +'contend', +'contented', +'contently', +'contents', +'contest', +'context', +'contort', +'contour', +'contrite', +'control', +'contusion', +'convene', +'convent', +'copartner', +'cope', +'copied', +'copier', +'copilot', +'coping', +'copious', +'copper', +'copy', +'coral', +'cork', +'cornball', +'cornbread', +'corncob', +'cornea', +'corned', +'corner', +'cornfield', +'cornflake', +'cornhusk', +'cornmeal', +'cornstalk', +'corny', +'coronary', +'coroner', +'corporal', +'corporate', +'corral', +'correct', +'corridor', +'corrode', +'corroding', +'corrosive', +'corsage', +'corset', +'cortex', +'cosigner', +'cosmetics', +'cosmic', +'cosmos', +'cosponsor', +'cost', +'cottage', +'cotton', +'couch', +'cough', +'could', +'countable', +'countdown', +'counting', +'countless', +'country', +'county', +'courier', +'covenant', +'cover', +'coveted', +'coveting', +'coyness', +'cozily', +'coziness', +'cozy', +'crabbing', +'crabgrass', +'crablike', +'crabmeat', +'cradle', +'cradling', +'crafter', +'craftily', +'craftsman', +'craftwork', +'crafty', +'cramp', +'cranberry', +'crane', +'cranial', +'cranium', +'crank', +'crate', +'crave', +'craving', +'crawfish', +'crawlers', +'crawling', +'crayfish', +'crayon', +'crazed', +'crazily', +'craziness', +'crazy', +'creamed', +'creamer', +'creamlike', +'crease', +'creasing', +'creatable', +'create', +'creation', +'creative', +'creature', +'credible', +'credibly', +'credit', +'creed', +'creme', +'creole', +'crepe', +'crept', +'crescent', +'crested', +'cresting', +'crestless', +'crevice', +'crewless', +'crewman', +'crewmate', +'crib', +'cricket', +'cried', +'crier', +'crimp', +'crimson', +'cringe', +'cringing', +'crinkle', +'crinkly', +'crisped', +'crisping', +'crisply', +'crispness', +'crispy', +'criteria', +'critter', +'croak', +'crock', +'crook', +'croon', +'crop', +'cross', +'crouch', +'crouton', +'crowbar', +'crowd', +'crown', +'crucial', +'crudely', +'crudeness', +'cruelly', +'cruelness', +'cruelty', +'crumb', +'crummiest', +'crummy', +'crumpet', +'crumpled', +'cruncher', +'crunching', +'crunchy', +'crusader', +'crushable', +'crushed', +'crusher', +'crushing', +'crust', +'crux', +'crying', +'cryptic', +'crystal', +'cubbyhole', +'cube', +'cubical', +'cubicle', +'cucumber', +'cuddle', +'cuddly', +'cufflink', +'culinary', +'culminate', +'culpable', +'culprit', +'cultivate', +'cultural', +'culture', +'cupbearer', +'cupcake', +'cupid', +'cupped', +'cupping', +'curable', +'curator', +'curdle', +'cure', +'curfew', +'curing', +'curled', +'curler', +'curliness', +'curling', +'curly', +'curry', +'curse', +'cursive', +'cursor', +'curtain', +'curtly', +'curtsy', +'curvature', +'curve', +'curvy', +'cushy', +'cusp', +'cussed', +'custard', +'custodian', +'custody', +'customary', +'customer', +'customize', +'customs', +'cut', +'cycle', +'cyclic', +'cycling', +'cyclist', +'cylinder', +'cymbal', +'cytoplasm', +'cytoplast', +'dab', +'dad', +'daffodil', +'dagger', +'daily', +'daintily', +'dainty', +'dairy', +'daisy', +'dallying', +'dance', +'dancing', +'dandelion', +'dander', +'dandruff', +'dandy', +'danger', +'dangle', +'dangling', +'daredevil', +'dares', +'daringly', +'darkened', +'darkening', +'darkish', +'darkness', +'darkroom', +'darling', +'darn', +'dart', +'darwinism', +'dash', +'dastardly', +'data', +'datebook', +'dating', +'daughter', +'daunting', +'dawdler', +'dawn', +'daybed', +'daybreak', +'daycare', +'daydream', +'daylight', +'daylong', +'dayroom', +'daytime', +'dazzler', +'dazzling', +'deacon', +'deafening', +'deafness', +'dealer', +'dealing', +'dealmaker', +'dealt', +'dean', +'debatable', +'debate', +'debating', +'debit', +'debrief', +'debtless', +'debtor', +'debug', +'debunk', +'decade', +'decaf', +'decal', +'decathlon', +'decay', +'deceased', +'deceit', +'deceiver', +'deceiving', +'december', +'decency', +'decent', +'deception', +'deceptive', +'decibel', +'decidable', +'decimal', +'decimeter', +'decipher', +'deck', +'declared', +'decline', +'decode', +'decompose', +'decorated', +'decorator', +'decoy', +'decrease', +'decree', +'dedicate', +'dedicator', +'deduce', +'deduct', +'deed', +'deem', +'deepen', +'deeply', +'deepness', +'deface', +'defacing', +'defame', +'default', +'defeat', +'defection', +'defective', +'defendant', +'defender', +'defense', +'defensive', +'deferral', +'deferred', +'defiance', +'defiant', +'defile', +'defiling', +'define', +'definite', +'deflate', +'deflation', +'deflator', +'deflected', +'deflector', +'defog', +'deforest', +'defraud', +'defrost', +'deftly', +'defuse', +'defy', +'degraded', +'degrading', +'degrease', +'degree', +'dehydrate', +'deity', +'dejected', +'delay', +'delegate', +'delegator', +'delete', +'deletion', +'delicacy', +'delicate', +'delicious', +'delighted', +'delirious', +'delirium', +'deliverer', +'delivery', +'delouse', +'delta', +'deluge', +'delusion', +'deluxe', +'demanding', +'demeaning', +'demeanor', +'demise', +'democracy', +'democrat', +'demote', +'demotion', +'demystify', +'denatured', +'deniable', +'denial', +'denim', +'denote', +'dense', +'density', +'dental', +'dentist', +'denture', +'deny', +'deodorant', +'deodorize', +'departed', +'departure', +'depict', +'deplete', +'depletion', +'deplored', +'deploy', +'deport', +'depose', +'depraved', +'depravity', +'deprecate', +'depress', +'deprive', +'depth', +'deputize', +'deputy', +'derail', +'deranged', +'derby', +'derived', +'desecrate', +'deserve', +'deserving', +'designate', +'designed', +'designer', +'designing', +'deskbound', +'desktop', +'deskwork', +'desolate', +'despair', +'despise', +'despite', +'destiny', +'destitute', +'destruct', +'detached', +'detail', +'detection', +'detective', +'detector', +'detention', +'detergent', +'detest', +'detonate', +'detonator', +'detoxify', +'detract', +'deuce', +'devalue', +'deviancy', +'deviant', +'deviate', +'deviation', +'deviator', +'device', +'devious', +'devotedly', +'devotee', +'devotion', +'devourer', +'devouring', +'devoutly', +'dexterity', +'dexterous', +'diabetes', +'diabetic', +'diabolic', +'diagnoses', +'diagnosis', +'diagram', +'dial', +'diameter', +'diaper', +'diaphragm', +'diary', +'dice', +'dicing', +'dictate', +'dictation', +'dictator', +'difficult', +'diffused', +'diffuser', +'diffusion', +'diffusive', +'dig', +'dilation', +'diligence', +'diligent', +'dill', +'dilute', +'dime', +'diminish', +'dimly', +'dimmed', +'dimmer', +'dimness', +'dimple', +'diner', +'dingbat', +'dinghy', +'dinginess', +'dingo', +'dingy', +'dining', +'dinner', +'diocese', +'dioxide', +'diploma', +'dipped', +'dipper', +'dipping', +'directed', +'direction', +'directive', +'directly', +'directory', +'direness', +'dirtiness', +'disabled', +'disagree', +'disallow', +'disarm', +'disarray', +'disaster', +'disband', +'disbelief', +'disburse', +'discard', +'discern', +'discharge', +'disclose', +'discolor', +'discount', +'discourse', +'discover', +'discuss', +'disdain', +'disengage', +'disfigure', +'disgrace', +'dish', +'disinfect', +'disjoin', +'disk', +'dislike', +'disliking', +'dislocate', +'dislodge', +'disloyal', +'dismantle', +'dismay', +'dismiss', +'dismount', +'disobey', +'disorder', +'disown', +'disparate', +'disparity', +'dispatch', +'dispense', +'dispersal', +'dispersed', +'disperser', +'displace', +'display', +'displease', +'disposal', +'dispose', +'disprove', +'dispute', +'disregard', +'disrupt', +'dissuade', +'distance', +'distant', +'distaste', +'distill', +'distinct', +'distort', +'distract', +'distress', +'district', +'distrust', +'ditch', +'ditto', +'ditzy', +'dividable', +'divided', +'dividend', +'dividers', +'dividing', +'divinely', +'diving', +'divinity', +'divisible', +'divisibly', +'division', +'divisive', +'divorcee', +'dizziness', +'dizzy', +'doable', +'docile', +'dock', +'doctrine', +'document', +'dodge', +'dodgy', +'doily', +'doing', +'dole', +'dollar', +'dollhouse', +'dollop', +'dolly', +'dolphin', +'domain', +'domelike', +'domestic', +'dominion', +'dominoes', +'donated', +'donation', +'donator', +'donor', +'donut', +'doodle', +'doorbell', +'doorframe', +'doorknob', +'doorman', +'doormat', +'doornail', +'doorpost', +'doorstep', +'doorstop', +'doorway', +'doozy', +'dork', +'dormitory', +'dorsal', +'dosage', +'dose', +'dotted', +'doubling', +'douche', +'dove', +'down', +'dowry', +'doze', +'drab', +'dragging', +'dragonfly', +'dragonish', +'dragster', +'drainable', +'drainage', +'drained', +'drainer', +'drainpipe', +'dramatic', +'dramatize', +'drank', +'drapery', +'drastic', +'draw', +'dreaded', +'dreadful', +'dreadlock', +'dreamboat', +'dreamily', +'dreamland', +'dreamless', +'dreamlike', +'dreamt', +'dreamy', +'drearily', +'dreary', +'drench', +'dress', +'drew', +'dribble', +'dried', +'drier', +'drift', +'driller', +'drilling', +'drinkable', +'drinking', +'dripping', +'drippy', +'drivable', +'driven', +'driver', +'driveway', +'driving', +'drizzle', +'drizzly', +'drone', +'drool', +'droop', +'drop-down', +'dropbox', +'dropkick', +'droplet', +'dropout', +'dropper', +'drove', +'drown', +'drowsily', +'drudge', +'drum', +'dry', +'dubbed', +'dubiously', +'duchess', +'duckbill', +'ducking', +'duckling', +'ducktail', +'ducky', +'duct', +'dude', +'duffel', +'dugout', +'duh', +'duke', +'duller', +'dullness', +'duly', +'dumping', +'dumpling', +'dumpster', +'duo', +'dupe', +'duplex', +'duplicate', +'duplicity', +'durable', +'durably', +'duration', +'duress', +'during', +'dusk', +'dust', +'dutiful', +'duty', +'duvet', +'dwarf', +'dweeb', +'dwelled', +'dweller', +'dwelling', +'dwindle', +'dwindling', +'dynamic', +'dynamite', +'dynasty', +'dyslexia', +'dyslexic', +'each', +'eagle', +'earache', +'eardrum', +'earflap', +'earful', +'earlobe', +'early', +'earmark', +'earmuff', +'earphone', +'earpiece', +'earplugs', +'earring', +'earshot', +'earthen', +'earthlike', +'earthling', +'earthly', +'earthworm', +'earthy', +'earwig', +'easeful', +'easel', +'easiest', +'easily', +'easiness', +'easing', +'eastbound', +'eastcoast', +'easter', +'eastward', +'eatable', +'eaten', +'eatery', +'eating', +'eats', +'ebay', +'ebony', +'ebook', +'ecard', +'eccentric', +'echo', +'eclair', +'eclipse', +'ecologist', +'ecology', +'economic', +'economist', +'economy', +'ecosphere', +'ecosystem', +'edge', +'edginess', +'edging', +'edgy', +'edition', +'editor', +'educated', +'education', +'educator', +'eel', +'effective', +'effects', +'efficient', +'effort', +'eggbeater', +'egging', +'eggnog', +'eggplant', +'eggshell', +'egomaniac', +'egotism', +'egotistic', +'either', +'eject', +'elaborate', +'elastic', +'elated', +'elbow', +'eldercare', +'elderly', +'eldest', +'electable', +'election', +'elective', +'elephant', +'elevate', +'elevating', +'elevation', +'elevator', +'eleven', +'elf', +'eligible', +'eligibly', +'eliminate', +'elite', +'elitism', +'elixir', +'elk', +'ellipse', +'elliptic', +'elm', +'elongated', +'elope', +'eloquence', +'eloquent', +'elsewhere', +'elude', +'elusive', +'elves', +'email', +'embargo', +'embark', +'embassy', +'embattled', +'embellish', +'ember', +'embezzle', +'emblaze', +'emblem', +'embody', +'embolism', +'emboss', +'embroider', +'emcee', +'emerald', +'emergency', +'emission', +'emit', +'emote', +'emoticon', +'emotion', +'empathic', +'empathy', +'emperor', +'emphases', +'emphasis', +'emphasize', +'emphatic', +'empirical', +'employed', +'employee', +'employer', +'emporium', +'empower', +'emptier', +'emptiness', +'empty', +'emu', +'enable', +'enactment', +'enamel', +'enchanted', +'enchilada', +'encircle', +'enclose', +'enclosure', +'encode', +'encore', +'encounter', +'encourage', +'encroach', +'encrust', +'encrypt', +'endanger', +'endeared', +'endearing', +'ended', +'ending', +'endless', +'endnote', +'endocrine', +'endorphin', +'endorse', +'endowment', +'endpoint', +'endurable', +'endurance', +'enduring', +'energetic', +'energize', +'energy', +'enforced', +'enforcer', +'engaged', +'engaging', +'engine', +'engorge', +'engraved', +'engraver', +'engraving', +'engross', +'engulf', +'enhance', +'enigmatic', +'enjoyable', +'enjoyably', +'enjoyer', +'enjoying', +'enjoyment', +'enlarged', +'enlarging', +'enlighten', +'enlisted', +'enquirer', +'enrage', +'enrich', +'enroll', +'enslave', +'ensnare', +'ensure', +'entail', +'entangled', +'entering', +'entertain', +'enticing', +'entire', +'entitle', +'entity', +'entomb', +'entourage', +'entrap', +'entree', +'entrench', +'entrust', +'entryway', +'entwine', +'enunciate', +'envelope', +'enviable', +'enviably', +'envious', +'envision', +'envoy', +'envy', +'enzyme', +'epic', +'epidemic', +'epidermal', +'epidermis', +'epidural', +'epilepsy', +'epileptic', +'epilogue', +'epiphany', +'episode', +'equal', +'equate', +'equation', +'equator', +'equinox', +'equipment', +'equity', +'equivocal', +'eradicate', +'erasable', +'erased', +'eraser', +'erasure', +'ergonomic', +'errand', +'errant', +'erratic', +'error', +'erupt', +'escalate', +'escalator', +'escapable', +'escapade', +'escapist', +'escargot', +'eskimo', +'esophagus', +'espionage', +'espresso', +'esquire', +'essay', +'essence', +'essential', +'establish', +'estate', +'esteemed', +'estimate', +'estimator', +'estranged', +'estrogen', +'etching', +'eternal', +'eternity', +'ethanol', +'ether', +'ethically', +'ethics', +'euphemism', +'evacuate', +'evacuee', +'evade', +'evaluate', +'evaluator', +'evaporate', +'evasion', +'evasive', +'even', +'everglade', +'evergreen', +'everybody', +'everyday', +'everyone', +'evict', +'evidence', +'evident', +'evil', +'evoke', +'evolution', +'evolve', +'exact', +'exalted', +'example', +'excavate', +'excavator', +'exceeding', +'exception', +'excess', +'exchange', +'excitable', +'exciting', +'exclaim', +'exclude', +'excluding', +'exclusion', +'exclusive', +'excretion', +'excretory', +'excursion', +'excusable', +'excusably', +'excuse', +'exemplary', +'exemplify', +'exemption', +'exerciser', +'exert', +'exes', +'exfoliate', +'exhale', +'exhaust', +'exhume', +'exile', +'existing', +'exit', +'exodus', +'exonerate', +'exorcism', +'exorcist', +'expand', +'expanse', +'expansion', +'expansive', +'expectant', +'expedited', +'expediter', +'expel', +'expend', +'expenses', +'expensive', +'expert', +'expire', +'expiring', +'explain', +'expletive', +'explicit', +'explode', +'exploit', +'explore', +'exploring', +'exponent', +'exporter', +'exposable', +'expose', +'exposure', +'express', +'expulsion', +'exquisite', +'extended', +'extending', +'extent', +'extenuate', +'exterior', +'external', +'extinct', +'extortion', +'extradite', +'extras', +'extrovert', +'extrude', +'extruding', +'exuberant', +'fable', +'fabric', +'fabulous', +'facebook', +'facecloth', +'facedown', +'faceless', +'facelift', +'faceplate', +'faceted', +'facial', +'facility', +'facing', +'facsimile', +'faction', +'factoid', +'factor', +'factsheet', +'factual', +'faculty', +'fade', +'fading', +'failing', +'falcon', +'fall', +'false', +'falsify', +'fame', +'familiar', +'family', +'famine', +'famished', +'fanatic', +'fancied', +'fanciness', +'fancy', +'fanfare', +'fang', +'fanning', +'fantasize', +'fantastic', +'fantasy', +'fascism', +'fastball', +'faster', +'fasting', +'fastness', +'faucet', +'favorable', +'favorably', +'favored', +'favoring', +'favorite', +'fax', +'feast', +'federal', +'fedora', +'feeble', +'feed', +'feel', +'feisty', +'feline', +'felt-tip', +'feminine', +'feminism', +'feminist', +'feminize', +'femur', +'fence', +'fencing', +'fender', +'ferment', +'fernlike', +'ferocious', +'ferocity', +'ferret', +'ferris', +'ferry', +'fervor', +'fester', +'festival', +'festive', +'festivity', +'fetal', +'fetch', +'fever', +'fiber', +'fiction', +'fiddle', +'fiddling', +'fidelity', +'fidgeting', +'fidgety', +'fifteen', +'fifth', +'fiftieth', +'fifty', +'figment', +'figure', +'figurine', +'filing', +'filled', +'filler', +'filling', +'film', +'filter', +'filth', +'filtrate', +'finale', +'finalist', +'finalize', +'finally', +'finance', +'financial', +'finch', +'fineness', +'finer', +'finicky', +'finished', +'finisher', +'finishing', +'finite', +'finless', +'finlike', +'fiscally', +'fit', +'five', +'flaccid', +'flagman', +'flagpole', +'flagship', +'flagstick', +'flagstone', +'flail', +'flakily', +'flaky', +'flame', +'flammable', +'flanked', +'flanking', +'flannels', +'flap', +'flaring', +'flashback', +'flashbulb', +'flashcard', +'flashily', +'flashing', +'flashy', +'flask', +'flatbed', +'flatfoot', +'flatly', +'flatness', +'flatten', +'flattered', +'flatterer', +'flattery', +'flattop', +'flatware', +'flatworm', +'flavored', +'flavorful', +'flavoring', +'flaxseed', +'fled', +'fleshed', +'fleshy', +'flick', +'flier', +'flight', +'flinch', +'fling', +'flint', +'flip', +'flirt', +'float', +'flock', +'flogging', +'flop', +'floral', +'florist', +'floss', +'flounder', +'flyable', +'flyaway', +'flyer', +'flying', +'flyover', +'flypaper', +'foam', +'foe', +'fog', +'foil', +'folic', +'folk', +'follicle', +'follow', +'fondling', +'fondly', +'fondness', +'fondue', +'font', +'food', +'fool', +'footage', +'football', +'footbath', +'footboard', +'footer', +'footgear', +'foothill', +'foothold', +'footing', +'footless', +'footman', +'footnote', +'footpad', +'footpath', +'footprint', +'footrest', +'footsie', +'footsore', +'footwear', +'footwork', +'fossil', +'foster', +'founder', +'founding', +'fountain', +'fox', +'foyer', +'fraction', +'fracture', +'fragile', +'fragility', +'fragment', +'fragrance', +'fragrant', +'frail', +'frame', +'framing', +'frantic', +'fraternal', +'frayed', +'fraying', +'frays', +'freckled', +'freckles', +'freebase', +'freebee', +'freebie', +'freedom', +'freefall', +'freehand', +'freeing', +'freeload', +'freely', +'freemason', +'freeness', +'freestyle', +'freeware', +'freeway', +'freewill', +'freezable', +'freezing', +'freight', +'french', +'frenzied', +'frenzy', +'frequency', +'frequent', +'fresh', +'fretful', +'fretted', +'friction', +'friday', +'fridge', +'fried', +'friend', +'frighten', +'frightful', +'frigidity', +'frigidly', +'frill', +'fringe', +'frisbee', +'frisk', +'fritter', +'frivolous', +'frolic', +'from', +'front', +'frostbite', +'frosted', +'frostily', +'frosting', +'frostlike', +'frosty', +'froth', +'frown', +'frozen', +'fructose', +'frugality', +'frugally', +'fruit', +'frustrate', +'frying', +'gab', +'gaffe', +'gag', +'gainfully', +'gaining', +'gains', +'gala', +'gallantly', +'galleria', +'gallery', +'galley', +'gallon', +'gallows', +'gallstone', +'galore', +'galvanize', +'gambling', +'game', +'gaming', +'gamma', +'gander', +'gangly', +'gangrene', +'gangway', +'gap', +'garage', +'garbage', +'garden', +'gargle', +'garland', +'garlic', +'garment', +'garnet', +'garnish', +'garter', +'gas', +'gatherer', +'gathering', +'gating', +'gauging', +'gauntlet', +'gauze', +'gave', +'gawk', +'gazing', +'gear', +'gecko', +'geek', +'geiger', +'gem', +'gender', +'generic', +'generous', +'genetics', +'genre', +'gentile', +'gentleman', +'gently', +'gents', +'geography', +'geologic', +'geologist', +'geology', +'geometric', +'geometry', +'geranium', +'gerbil', +'geriatric', +'germicide', +'germinate', +'germless', +'germproof', +'gestate', +'gestation', +'gesture', +'getaway', +'getting', +'getup', +'giant', +'gibberish', +'giblet', +'giddily', +'giddiness', +'giddy', +'gift', +'gigabyte', +'gigahertz', +'gigantic', +'giggle', +'giggling', +'giggly', +'gigolo', +'gilled', +'gills', +'gimmick', +'girdle', +'giveaway', +'given', +'giver', +'giving', +'gizmo', +'gizzard', +'glacial', +'glacier', +'glade', +'gladiator', +'gladly', +'glamorous', +'glamour', +'glance', +'glancing', +'glandular', +'glare', +'glaring', +'glass', +'glaucoma', +'glazing', +'gleaming', +'gleeful', +'glider', +'gliding', +'glimmer', +'glimpse', +'glisten', +'glitch', +'glitter', +'glitzy', +'gloater', +'gloating', +'gloomily', +'gloomy', +'glorified', +'glorifier', +'glorify', +'glorious', +'glory', +'gloss', +'glove', +'glowing', +'glowworm', +'glucose', +'glue', +'gluten', +'glutinous', +'glutton', +'gnarly', +'gnat', +'goal', +'goatskin', +'goes', +'goggles', +'going', +'goldfish', +'goldmine', +'goldsmith', +'golf', +'goliath', +'gonad', +'gondola', +'gone', +'gong', +'good', +'gooey', +'goofball', +'goofiness', +'goofy', +'google', +'goon', +'gopher', +'gore', +'gorged', +'gorgeous', +'gory', +'gosling', +'gossip', +'gothic', +'gotten', +'gout', +'gown', +'grab', +'graceful', +'graceless', +'gracious', +'gradation', +'graded', +'grader', +'gradient', +'grading', +'gradually', +'graduate', +'graffiti', +'grafted', +'grafting', +'grain', +'granddad', +'grandkid', +'grandly', +'grandma', +'grandpa', +'grandson', +'granite', +'granny', +'granola', +'grant', +'granular', +'grape', +'graph', +'grapple', +'grappling', +'grasp', +'grass', +'gratified', +'gratify', +'grating', +'gratitude', +'gratuity', +'gravel', +'graveness', +'graves', +'graveyard', +'gravitate', +'gravity', +'gravy', +'gray', +'grazing', +'greasily', +'greedily', +'greedless', +'greedy', +'green', +'greeter', +'greeting', +'grew', +'greyhound', +'grid', +'grief', +'grievance', +'grieving', +'grievous', +'grill', +'grimace', +'grimacing', +'grime', +'griminess', +'grimy', +'grinch', +'grinning', +'grip', +'gristle', +'grit', +'groggily', +'groggy', +'groin', +'groom', +'groove', +'grooving', +'groovy', +'grope', +'ground', +'grouped', +'grout', +'grove', +'grower', +'growing', +'growl', +'grub', +'grudge', +'grudging', +'grueling', +'gruffly', +'grumble', +'grumbling', +'grumbly', +'grumpily', +'grunge', +'grunt', +'guacamole', +'guidable', +'guidance', +'guide', +'guiding', +'guileless', +'guise', +'gulf', +'gullible', +'gully', +'gulp', +'gumball', +'gumdrop', +'gumminess', +'gumming', +'gummy', +'gurgle', +'gurgling', +'guru', +'gush', +'gusto', +'gusty', +'gutless', +'guts', +'gutter', +'guy', +'guzzler', +'gyration', +'habitable', +'habitant', +'habitat', +'habitual', +'hacked', +'hacker', +'hacking', +'hacksaw', +'had', +'haggler', +'haiku', +'half', +'halogen', +'halt', +'halved', +'halves', +'hamburger', +'hamlet', +'hammock', +'hamper', +'hamster', +'hamstring', +'handbag', +'handball', +'handbook', +'handbrake', +'handcart', +'handclap', +'handclasp', +'handcraft', +'handcuff', +'handed', +'handful', +'handgrip', +'handgun', +'handheld', +'handiness', +'handiwork', +'handlebar', +'handled', +'handler', +'handling', +'handmade', +'handoff', +'handpick', +'handprint', +'handrail', +'handsaw', +'handset', +'handsfree', +'handshake', +'handstand', +'handwash', +'handwork', +'handwoven', +'handwrite', +'handyman', +'hangnail', +'hangout', +'hangover', +'hangup', +'hankering', +'hankie', +'hanky', +'haphazard', +'happening', +'happier', +'happiest', +'happily', +'happiness', +'happy', +'harbor', +'hardcopy', +'hardcore', +'hardcover', +'harddisk', +'hardened', +'hardener', +'hardening', +'hardhat', +'hardhead', +'hardiness', +'hardly', +'hardness', +'hardship', +'hardware', +'hardwired', +'hardwood', +'hardy', +'harmful', +'harmless', +'harmonica', +'harmonics', +'harmonize', +'harmony', +'harness', +'harpist', +'harsh', +'harvest', +'hash', +'hassle', +'haste', +'hastily', +'hastiness', +'hasty', +'hatbox', +'hatchback', +'hatchery', +'hatchet', +'hatching', +'hatchling', +'hate', +'hatless', +'hatred', +'haunt', +'haven', +'hazard', +'hazelnut', +'hazily', +'haziness', +'hazing', +'hazy', +'headache', +'headband', +'headboard', +'headcount', +'headdress', +'headed', +'header', +'headfirst', +'headgear', +'heading', +'headlamp', +'headless', +'headlock', +'headphone', +'headpiece', +'headrest', +'headroom', +'headscarf', +'headset', +'headsman', +'headstand', +'headstone', +'headway', +'headwear', +'heap', +'heat', +'heave', +'heavily', +'heaviness', +'heaving', +'hedge', +'hedging', +'heftiness', +'hefty', +'helium', +'helmet', +'helper', +'helpful', +'helping', +'helpless', +'helpline', +'hemlock', +'hemstitch', +'hence', +'henchman', +'henna', +'herald', +'herbal', +'herbicide', +'herbs', +'heritage', +'hermit', +'heroics', +'heroism', +'herring', +'herself', +'hertz', +'hesitancy', +'hesitant', +'hesitate', +'hexagon', +'hexagram', +'hubcap', +'huddle', +'huddling', +'huff', +'hug', +'hula', +'hulk', +'hull', +'human', +'humble', +'humbling', +'humbly', +'humid', +'humiliate', +'humility', +'humming', +'hummus', +'humongous', +'humorist', +'humorless', +'humorous', +'humpback', +'humped', +'humvee', +'hunchback', +'hundredth', +'hunger', +'hungrily', +'hungry', +'hunk', +'hunter', +'hunting', +'huntress', +'huntsman', +'hurdle', +'hurled', +'hurler', +'hurling', +'hurray', +'hurricane', +'hurried', +'hurry', +'hurt', +'husband', +'hush', +'husked', +'huskiness', +'hut', +'hybrid', +'hydrant', +'hydrated', +'hydration', +'hydrogen', +'hydroxide', +'hyperlink', +'hypertext', +'hyphen', +'hypnoses', +'hypnosis', +'hypnotic', +'hypnotism', +'hypnotist', +'hypnotize', +'hypocrisy', +'hypocrite', +'ibuprofen', +'ice', +'iciness', +'icing', +'icky', +'icon', +'icy', +'idealism', +'idealist', +'idealize', +'ideally', +'idealness', +'identical', +'identify', +'identity', +'ideology', +'idiocy', +'idiom', +'idly', +'igloo', +'ignition', +'ignore', +'iguana', +'illicitly', +'illusion', +'illusive', +'image', +'imaginary', +'imagines', +'imaging', +'imbecile', +'imitate', +'imitation', +'immature', +'immerse', +'immersion', +'imminent', +'immobile', +'immodest', +'immorally', +'immortal', +'immovable', +'immovably', +'immunity', +'immunize', +'impaired', +'impale', +'impart', +'impatient', +'impeach', +'impeding', +'impending', +'imperfect', +'imperial', +'impish', +'implant', +'implement', +'implicate', +'implicit', +'implode', +'implosion', +'implosive', +'imply', +'impolite', +'important', +'importer', +'impose', +'imposing', +'impotence', +'impotency', +'impotent', +'impound', +'imprecise', +'imprint', +'imprison', +'impromptu', +'improper', +'improve', +'improving', +'improvise', +'imprudent', +'impulse', +'impulsive', +'impure', +'impurity', +'iodine', +'iodize', +'ion', +'ipad', +'iphone', +'ipod', +'irate', +'irk', +'iron', +'irregular', +'irrigate', +'irritable', +'irritably', +'irritant', +'irritate', +'islamic', +'islamist', +'isolated', +'isolating', +'isolation', +'isotope', +'issue', +'issuing', +'italicize', +'italics', +'item', +'itinerary', +'itunes', +'ivory', +'ivy', +'jab', +'jackal', +'jacket', +'jackknife', +'jackpot', +'jailbird', +'jailbreak', +'jailer', +'jailhouse', +'jalapeno', +'jam', +'janitor', +'january', +'jargon', +'jarring', +'jasmine', +'jaundice', +'jaunt', +'java', +'jawed', +'jawless', +'jawline', +'jaws', +'jaybird', +'jaywalker', +'jazz', +'jeep', +'jeeringly', +'jellied', +'jelly', +'jersey', +'jester', +'jet', +'jiffy', +'jigsaw', +'jimmy', +'jingle', +'jingling', +'jinx', +'jitters', +'jittery', +'job', +'jockey', +'jockstrap', +'jogger', +'jogging', +'john', +'joining', +'jokester', +'jokingly', +'jolliness', +'jolly', +'jolt', +'jot', +'jovial', +'joyfully', +'joylessly', +'joyous', +'joyride', +'joystick', +'jubilance', +'jubilant', +'judge', +'judgingly', +'judicial', +'judiciary', +'judo', +'juggle', +'juggling', +'jugular', +'juice', +'juiciness', +'juicy', +'jujitsu', +'jukebox', +'july', +'jumble', +'jumbo', +'jump', +'junction', +'juncture', +'june', +'junior', +'juniper', +'junkie', +'junkman', +'junkyard', +'jurist', +'juror', +'jury', +'justice', +'justifier', +'justify', +'justly', +'justness', +'juvenile', +'kabob', +'kangaroo', +'karaoke', +'karate', +'karma', +'kebab', +'keenly', +'keenness', +'keep', +'keg', +'kelp', +'kennel', +'kept', +'kerchief', +'kerosene', +'kettle', +'kick', +'kiln', +'kilobyte', +'kilogram', +'kilometer', +'kilowatt', +'kilt', +'kimono', +'kindle', +'kindling', +'kindly', +'kindness', +'kindred', +'kinetic', +'kinfolk', +'king', +'kinship', +'kinsman', +'kinswoman', +'kissable', +'kisser', +'kissing', +'kitchen', +'kite', +'kitten', +'kitty', +'kiwi', +'kleenex', +'knapsack', +'knee', +'knelt', +'knickers', +'knoll', +'koala', +'kooky', +'kosher', +'krypton', +'kudos', +'kung', +'labored', +'laborer', +'laboring', +'laborious', +'labrador', +'ladder', +'ladies', +'ladle', +'ladybug', +'ladylike', +'lagged', +'lagging', +'lagoon', +'lair', +'lake', +'lance', +'landed', +'landfall', +'landfill', +'landing', +'landlady', +'landless', +'landline', +'landlord', +'landmark', +'landmass', +'landmine', +'landowner', +'landscape', +'landside', +'landslide', +'language', +'lankiness', +'lanky', +'lantern', +'lapdog', +'lapel', +'lapped', +'lapping', +'laptop', +'lard', +'large', +'lark', +'lash', +'lasso', +'last', +'latch', +'late', +'lather', +'latitude', +'latrine', +'latter', +'latticed', +'launch', +'launder', +'laundry', +'laurel', +'lavender', +'lavish', +'laxative', +'lazily', +'laziness', +'lazy', +'lecturer', +'left', +'legacy', +'legal', +'legend', +'legged', +'leggings', +'legible', +'legibly', +'legislate', +'lego', +'legroom', +'legume', +'legwarmer', +'legwork', +'lemon', +'lend', +'length', +'lens', +'lent', +'leotard', +'lesser', +'letdown', +'lethargic', +'lethargy', +'letter', +'lettuce', +'level', +'leverage', +'levers', +'levitate', +'levitator', +'liability', +'liable', +'liberty', +'librarian', +'library', +'licking', +'licorice', +'lid', +'life', +'lifter', +'lifting', +'liftoff', +'ligament', +'likely', +'likeness', +'likewise', +'liking', +'lilac', +'lilly', +'lily', +'limb', +'limeade', +'limelight', +'limes', +'limit', +'limping', +'limpness', +'line', +'lingo', +'linguini', +'linguist', +'lining', +'linked', +'linoleum', +'linseed', +'lint', +'lion', +'lip', +'liquefy', +'liqueur', +'liquid', +'lisp', +'list', +'litigate', +'litigator', +'litmus', +'litter', +'little', +'livable', +'lived', +'lively', +'liver', +'livestock', +'lividly', +'living', +'lizard', +'lubricant', +'lubricate', +'lucid', +'luckily', +'luckiness', +'luckless', +'lucrative', +'ludicrous', +'lugged', +'lukewarm', +'lullaby', +'lumber', +'luminance', +'luminous', +'lumpiness', +'lumping', +'lumpish', +'lunacy', +'lunar', +'lunchbox', +'luncheon', +'lunchroom', +'lunchtime', +'lung', +'lurch', +'lure', +'luridness', +'lurk', +'lushly', +'lushness', +'luster', +'lustfully', +'lustily', +'lustiness', +'lustrous', +'lusty', +'luxurious', +'luxury', +'lying', +'lyrically', +'lyricism', +'lyricist', +'lyrics', +'macarena', +'macaroni', +'macaw', +'mace', +'machine', +'machinist', +'magazine', +'magenta', +'maggot', +'magical', +'magician', +'magma', +'magnesium', +'magnetic', +'magnetism', +'magnetize', +'magnifier', +'magnify', +'magnitude', +'magnolia', +'mahogany', +'maimed', +'majestic', +'majesty', +'majorette', +'majority', +'makeover', +'maker', +'makeshift', +'making', +'malformed', +'malt', +'mama', +'mammal', +'mammary', +'mammogram', +'manager', +'managing', +'manatee', +'mandarin', +'mandate', +'mandatory', +'mandolin', +'manger', +'mangle', +'mango', +'mangy', +'manhandle', +'manhole', +'manhood', +'manhunt', +'manicotti', +'manicure', +'manifesto', +'manila', +'mankind', +'manlike', +'manliness', +'manly', +'manmade', +'manned', +'mannish', +'manor', +'manpower', +'mantis', +'mantra', +'manual', +'many', +'map', +'marathon', +'marauding', +'marbled', +'marbles', +'marbling', +'march', +'mardi', +'margarine', +'margarita', +'margin', +'marigold', +'marina', +'marine', +'marital', +'maritime', +'marlin', +'marmalade', +'maroon', +'married', +'marrow', +'marry', +'marshland', +'marshy', +'marsupial', +'marvelous', +'marxism', +'mascot', +'masculine', +'mashed', +'mashing', +'massager', +'masses', +'massive', +'mastiff', +'matador', +'matchbook', +'matchbox', +'matcher', +'matching', +'matchless', +'material', +'maternal', +'maternity', +'math', +'mating', +'matriarch', +'matrimony', +'matrix', +'matron', +'matted', +'matter', +'maturely', +'maturing', +'maturity', +'mauve', +'maverick', +'maximize', +'maximum', +'maybe', +'mayday', +'mayflower', +'moaner', +'moaning', +'mobile', +'mobility', +'mobilize', +'mobster', +'mocha', +'mocker', +'mockup', +'modified', +'modify', +'modular', +'modulator', +'module', +'moisten', +'moistness', +'moisture', +'molar', +'molasses', +'mold', +'molecular', +'molecule', +'molehill', +'mollusk', +'mom', +'monastery', +'monday', +'monetary', +'monetize', +'moneybags', +'moneyless', +'moneywise', +'mongoose', +'mongrel', +'monitor', +'monkhood', +'monogamy', +'monogram', +'monologue', +'monopoly', +'monorail', +'monotone', +'monotype', +'monoxide', +'monsieur', +'monsoon', +'monstrous', +'monthly', +'monument', +'moocher', +'moodiness', +'moody', +'mooing', +'moonbeam', +'mooned', +'moonlight', +'moonlike', +'moonlit', +'moonrise', +'moonscape', +'moonshine', +'moonstone', +'moonwalk', +'mop', +'morale', +'morality', +'morally', +'morbidity', +'morbidly', +'morphine', +'morphing', +'morse', +'mortality', +'mortally', +'mortician', +'mortified', +'mortify', +'mortuary', +'mosaic', +'mossy', +'most', +'mothball', +'mothproof', +'motion', +'motivate', +'motivator', +'motive', +'motocross', +'motor', +'motto', +'mountable', +'mountain', +'mounted', +'mounting', +'mourner', +'mournful', +'mouse', +'mousiness', +'moustache', +'mousy', +'mouth', +'movable', +'move', +'movie', +'moving', +'mower', +'mowing', +'much', +'muck', +'mud', +'mug', +'mulberry', +'mulch', +'mule', +'mulled', +'mullets', +'multiple', +'multiply', +'multitask', +'multitude', +'mumble', +'mumbling', +'mumbo', +'mummified', +'mummify', +'mummy', +'mumps', +'munchkin', +'mundane', +'municipal', +'muppet', +'mural', +'murkiness', +'murky', +'murmuring', +'muscular', +'museum', +'mushily', +'mushiness', +'mushroom', +'mushy', +'music', +'musket', +'muskiness', +'musky', +'mustang', +'mustard', +'muster', +'mustiness', +'musty', +'mutable', +'mutate', +'mutation', +'mute', +'mutilated', +'mutilator', +'mutiny', +'mutt', +'mutual', +'muzzle', +'myself', +'myspace', +'mystified', +'mystify', +'myth', +'nacho', +'nag', +'nail', +'name', +'naming', +'nanny', +'nanometer', +'nape', +'napkin', +'napped', +'napping', +'nappy', +'narrow', +'nastily', +'nastiness', +'national', +'native', +'nativity', +'natural', +'nature', +'naturist', +'nautical', +'navigate', +'navigator', +'navy', +'nearby', +'nearest', +'nearly', +'nearness', +'neatly', +'neatness', +'nebula', +'nebulizer', +'nectar', +'negate', +'negation', +'negative', +'neglector', +'negligee', +'negligent', +'negotiate', +'nemeses', +'nemesis', +'neon', +'nephew', +'nerd', +'nervous', +'nervy', +'nest', +'net', +'neurology', +'neuron', +'neurosis', +'neurotic', +'neuter', +'neutron', +'never', +'next', +'nibble', +'nickname', +'nicotine', +'niece', +'nifty', +'nimble', +'nimbly', +'nineteen', +'ninetieth', +'ninja', +'nintendo', +'ninth', +'nuclear', +'nuclei', +'nucleus', +'nugget', +'nullify', +'number', +'numbing', +'numbly', +'numbness', +'numeral', +'numerate', +'numerator', +'numeric', +'numerous', +'nuptials', +'nursery', +'nursing', +'nurture', +'nutcase', +'nutlike', +'nutmeg', +'nutrient', +'nutshell', +'nuttiness', +'nutty', +'nuzzle', +'nylon', +'oaf', +'oak', +'oasis', +'oat', +'obedience', +'obedient', +'obituary', +'object', +'obligate', +'obliged', +'oblivion', +'oblivious', +'oblong', +'obnoxious', +'oboe', +'obscure', +'obscurity', +'observant', +'observer', +'observing', +'obsessed', +'obsession', +'obsessive', +'obsolete', +'obstacle', +'obstinate', +'obstruct', +'obtain', +'obtrusive', +'obtuse', +'obvious', +'occultist', +'occupancy', +'occupant', +'occupier', +'occupy', +'ocean', +'ocelot', +'octagon', +'octane', +'october', +'octopus', +'ogle', +'oil', +'oink', +'ointment', +'okay', +'old', +'olive', +'olympics', +'omega', +'omen', +'ominous', +'omission', +'omit', +'omnivore', +'onboard', +'oncoming', +'ongoing', +'onion', +'online', +'onlooker', +'only', +'onscreen', +'onset', +'onshore', +'onslaught', +'onstage', +'onto', +'onward', +'onyx', +'oops', +'ooze', +'oozy', +'opacity', +'opal', +'open', +'operable', +'operate', +'operating', +'operation', +'operative', +'operator', +'opium', +'opossum', +'opponent', +'oppose', +'opposing', +'opposite', +'oppressed', +'oppressor', +'opt', +'opulently', +'osmosis', +'other', +'otter', +'ouch', +'ought', +'ounce', +'outage', +'outback', +'outbid', +'outboard', +'outbound', +'outbreak', +'outburst', +'outcast', +'outclass', +'outcome', +'outdated', +'outdoors', +'outer', +'outfield', +'outfit', +'outflank', +'outgoing', +'outgrow', +'outhouse', +'outing', +'outlast', +'outlet', +'outline', +'outlook', +'outlying', +'outmatch', +'outmost', +'outnumber', +'outplayed', +'outpost', +'outpour', +'output', +'outrage', +'outrank', +'outreach', +'outright', +'outscore', +'outsell', +'outshine', +'outshoot', +'outsider', +'outskirts', +'outsmart', +'outsource', +'outspoken', +'outtakes', +'outthink', +'outward', +'outweigh', +'outwit', +'oval', +'ovary', +'oven', +'overact', +'overall', +'overarch', +'overbid', +'overbill', +'overbite', +'overblown', +'overboard', +'overbook', +'overbuilt', +'overcast', +'overcoat', +'overcome', +'overcook', +'overcrowd', +'overdraft', +'overdrawn', +'overdress', +'overdrive', +'overdue', +'overeager', +'overeater', +'overexert', +'overfed', +'overfeed', +'overfill', +'overflow', +'overfull', +'overgrown', +'overhand', +'overhang', +'overhaul', +'overhead', +'overhear', +'overheat', +'overhung', +'overjoyed', +'overkill', +'overlabor', +'overlaid', +'overlap', +'overlay', +'overload', +'overlook', +'overlord', +'overlying', +'overnight', +'overpass', +'overpay', +'overplant', +'overplay', +'overpower', +'overprice', +'overrate', +'overreach', +'overreact', +'override', +'overripe', +'overrule', +'overrun', +'overshoot', +'overshot', +'oversight', +'oversized', +'oversleep', +'oversold', +'overspend', +'overstate', +'overstay', +'overstep', +'overstock', +'overstuff', +'oversweet', +'overtake', +'overthrow', +'overtime', +'overtly', +'overtone', +'overture', +'overturn', +'overuse', +'overvalue', +'overview', +'overwrite', +'owl', +'oxford', +'oxidant', +'oxidation', +'oxidize', +'oxidizing', +'oxygen', +'oxymoron', +'oyster', +'ozone', +'paced', +'pacemaker', +'pacific', +'pacifier', +'pacifism', +'pacifist', +'pacify', +'padded', +'padding', +'paddle', +'paddling', +'padlock', +'pagan', +'pager', +'paging', +'pajamas', +'palace', +'palatable', +'palm', +'palpable', +'palpitate', +'paltry', +'pampered', +'pamperer', +'pampers', +'pamphlet', +'panama', +'pancake', +'pancreas', +'panda', +'pandemic', +'pang', +'panhandle', +'panic', +'panning', +'panorama', +'panoramic', +'panther', +'pantomime', +'pantry', +'pants', +'pantyhose', +'paparazzi', +'papaya', +'paper', +'paprika', +'papyrus', +'parabola', +'parachute', +'parade', +'paradox', +'paragraph', +'parakeet', +'paralegal', +'paralyses', +'paralysis', +'paralyze', +'paramedic', +'parameter', +'paramount', +'parasail', +'parasite', +'parasitic', +'parcel', +'parched', +'parchment', +'pardon', +'parish', +'parka', +'parking', +'parkway', +'parlor', +'parmesan', +'parole', +'parrot', +'parsley', +'parsnip', +'partake', +'parted', +'parting', +'partition', +'partly', +'partner', +'partridge', +'party', +'passable', +'passably', +'passage', +'passcode', +'passenger', +'passerby', +'passing', +'passion', +'passive', +'passivism', +'passover', +'passport', +'password', +'pasta', +'pasted', +'pastel', +'pastime', +'pastor', +'pastrami', +'pasture', +'pasty', +'patchwork', +'patchy', +'paternal', +'paternity', +'path', +'patience', +'patient', +'patio', +'patriarch', +'patriot', +'patrol', +'patronage', +'patronize', +'pauper', +'pavement', +'paver', +'pavestone', +'pavilion', +'paving', +'pawing', +'payable', +'payback', +'paycheck', +'payday', +'payee', +'payer', +'paying', +'payment', +'payphone', +'payroll', +'pebble', +'pebbly', +'pecan', +'pectin', +'peculiar', +'peddling', +'pediatric', +'pedicure', +'pedigree', +'pedometer', +'pegboard', +'pelican', +'pellet', +'pelt', +'pelvis', +'penalize', +'penalty', +'pencil', +'pendant', +'pending', +'penholder', +'penknife', +'pennant', +'penniless', +'penny', +'penpal', +'pension', +'pentagon', +'pentagram', +'pep', +'perceive', +'percent', +'perch', +'percolate', +'perennial', +'perfected', +'perfectly', +'perfume', +'periscope', +'perish', +'perjurer', +'perjury', +'perkiness', +'perky', +'perm', +'peroxide', +'perpetual', +'perplexed', +'persecute', +'persevere', +'persuaded', +'persuader', +'pesky', +'peso', +'pessimism', +'pessimist', +'pester', +'pesticide', +'petal', +'petite', +'petition', +'petri', +'petroleum', +'petted', +'petticoat', +'pettiness', +'petty', +'petunia', +'phantom', +'phobia', +'phoenix', +'phonebook', +'phoney', +'phonics', +'phoniness', +'phony', +'phosphate', +'photo', +'phrase', +'phrasing', +'placard', +'placate', +'placidly', +'plank', +'planner', +'plant', +'plasma', +'plaster', +'plastic', +'plated', +'platform', +'plating', +'platinum', +'platonic', +'platter', +'platypus', +'plausible', +'plausibly', +'playable', +'playback', +'player', +'playful', +'playgroup', +'playhouse', +'playing', +'playlist', +'playmaker', +'playmate', +'playoff', +'playpen', +'playroom', +'playset', +'plaything', +'playtime', +'plaza', +'pleading', +'pleat', +'pledge', +'plentiful', +'plenty', +'plethora', +'plexiglas', +'pliable', +'plod', +'plop', +'plot', +'plow', +'ploy', +'pluck', +'plug', +'plunder', +'plunging', +'plural', +'plus', +'plutonium', +'plywood', +'poach', +'pod', +'poem', +'poet', +'pogo', +'pointed', +'pointer', +'pointing', +'pointless', +'pointy', +'poise', +'poison', +'poker', +'poking', +'polar', +'police', +'policy', +'polio', +'polish', +'politely', +'polka', +'polo', +'polyester', +'polygon', +'polygraph', +'polymer', +'poncho', +'pond', +'pony', +'popcorn', +'pope', +'poplar', +'popper', +'poppy', +'popsicle', +'populace', +'popular', +'populate', +'porcupine', +'pork', +'porous', +'porridge', +'portable', +'portal', +'portfolio', +'porthole', +'portion', +'portly', +'portside', +'poser', +'posh', +'posing', +'possible', +'possibly', +'possum', +'postage', +'postal', +'postbox', +'postcard', +'posted', +'poster', +'posting', +'postnasal', +'posture', +'postwar', +'pouch', +'pounce', +'pouncing', +'pound', +'pouring', +'pout', +'powdered', +'powdering', +'powdery', +'power', +'powwow', +'pox', +'praising', +'prance', +'prancing', +'pranker', +'prankish', +'prankster', +'prayer', +'praying', +'preacher', +'preaching', +'preachy', +'preamble', +'precinct', +'precise', +'precision', +'precook', +'precut', +'predator', +'predefine', +'predict', +'preface', +'prefix', +'preflight', +'preformed', +'pregame', +'pregnancy', +'pregnant', +'preheated', +'prelaunch', +'prelaw', +'prelude', +'premiere', +'premises', +'premium', +'prenatal', +'preoccupy', +'preorder', +'prepaid', +'prepay', +'preplan', +'preppy', +'preschool', +'prescribe', +'preseason', +'preset', +'preshow', +'president', +'presoak', +'press', +'presume', +'presuming', +'preteen', +'pretended', +'pretender', +'pretense', +'pretext', +'pretty', +'pretzel', +'prevail', +'prevalent', +'prevent', +'preview', +'previous', +'prewar', +'prewashed', +'prideful', +'pried', +'primal', +'primarily', +'primary', +'primate', +'primer', +'primp', +'princess', +'print', +'prior', +'prism', +'prison', +'prissy', +'pristine', +'privacy', +'private', +'privatize', +'prize', +'proactive', +'probable', +'probably', +'probation', +'probe', +'probing', +'probiotic', +'problem', +'procedure', +'process', +'proclaim', +'procreate', +'procurer', +'prodigal', +'prodigy', +'produce', +'product', +'profane', +'profanity', +'professed', +'professor', +'profile', +'profound', +'profusely', +'progeny', +'prognosis', +'program', +'progress', +'projector', +'prologue', +'prolonged', +'promenade', +'prominent', +'promoter', +'promotion', +'prompter', +'promptly', +'prone', +'prong', +'pronounce', +'pronto', +'proofing', +'proofread', +'proofs', +'propeller', +'properly', +'property', +'proponent', +'proposal', +'propose', +'props', +'prorate', +'protector', +'protegee', +'proton', +'prototype', +'protozoan', +'protract', +'protrude', +'proud', +'provable', +'proved', +'proven', +'provided', +'provider', +'providing', +'province', +'proving', +'provoke', +'provoking', +'provolone', +'prowess', +'prowler', +'prowling', +'proximity', +'proxy', +'prozac', +'prude', +'prudishly', +'prune', +'pruning', +'pry', +'psychic', +'public', +'publisher', +'pucker', +'pueblo', +'pug', +'pull', +'pulmonary', +'pulp', +'pulsate', +'pulse', +'pulverize', +'puma', +'pumice', +'pummel', +'punch', +'punctual', +'punctuate', +'punctured', +'pungent', +'punisher', +'punk', +'pupil', +'puppet', +'puppy', +'purchase', +'pureblood', +'purebred', +'purely', +'pureness', +'purgatory', +'purge', +'purging', +'purifier', +'purify', +'purist', +'puritan', +'purity', +'purple', +'purplish', +'purposely', +'purr', +'purse', +'pursuable', +'pursuant', +'pursuit', +'purveyor', +'pushcart', +'pushchair', +'pusher', +'pushiness', +'pushing', +'pushover', +'pushpin', +'pushup', +'pushy', +'putdown', +'putt', +'puzzle', +'puzzling', +'pyramid', +'pyromania', +'python', +'quack', +'quadrant', +'quail', +'quaintly', +'quake', +'quaking', +'qualified', +'qualifier', +'qualify', +'quality', +'qualm', +'quantum', +'quarrel', +'quarry', +'quartered', +'quarterly', +'quarters', +'quartet', +'quench', +'query', +'quicken', +'quickly', +'quickness', +'quicksand', +'quickstep', +'quiet', +'quill', +'quilt', +'quintet', +'quintuple', +'quirk', +'quit', +'quiver', +'quizzical', +'quotable', +'quotation', +'quote', +'rabid', +'race', +'racing', +'racism', +'rack', +'racoon', +'radar', +'radial', +'radiance', +'radiantly', +'radiated', +'radiation', +'radiator', +'radio', +'radish', +'raffle', +'raft', +'rage', +'ragged', +'raging', +'ragweed', +'raider', +'railcar', +'railing', +'railroad', +'railway', +'raisin', +'rake', +'raking', +'rally', +'ramble', +'rambling', +'ramp', +'ramrod', +'ranch', +'rancidity', +'random', +'ranged', +'ranger', +'ranging', +'ranked', +'ranking', +'ransack', +'ranting', +'rants', +'rare', +'rarity', +'rascal', +'rash', +'rasping', +'ravage', +'raven', +'ravine', +'raving', +'ravioli', +'ravishing', +'reabsorb', +'reach', +'reacquire', +'reaction', +'reactive', +'reactor', +'reaffirm', +'ream', +'reanalyze', +'reappear', +'reapply', +'reappoint', +'reapprove', +'rearrange', +'rearview', +'reason', +'reassign', +'reassure', +'reattach', +'reawake', +'rebalance', +'rebate', +'rebel', +'rebirth', +'reboot', +'reborn', +'rebound', +'rebuff', +'rebuild', +'rebuilt', +'reburial', +'rebuttal', +'recall', +'recant', +'recapture', +'recast', +'recede', +'recent', +'recess', +'recharger', +'recipient', +'recital', +'recite', +'reckless', +'reclaim', +'recliner', +'reclining', +'recluse', +'reclusive', +'recognize', +'recoil', +'recollect', +'recolor', +'reconcile', +'reconfirm', +'reconvene', +'recopy', +'record', +'recount', +'recoup', +'recovery', +'recreate', +'rectal', +'rectangle', +'rectified', +'rectify', +'recycled', +'recycler', +'recycling', +'reemerge', +'reenact', +'reenter', +'reentry', +'reexamine', +'referable', +'referee', +'reference', +'refill', +'refinance', +'refined', +'refinery', +'refining', +'refinish', +'reflected', +'reflector', +'reflex', +'reflux', +'refocus', +'refold', +'reforest', +'reformat', +'reformed', +'reformer', +'reformist', +'refract', +'refrain', +'refreeze', +'refresh', +'refried', +'refueling', +'refund', +'refurbish', +'refurnish', +'refusal', +'refuse', +'refusing', +'refutable', +'refute', +'regain', +'regalia', +'regally', +'reggae', +'regime', +'region', +'register', +'registrar', +'registry', +'regress', +'regretful', +'regroup', +'regular', +'regulate', +'regulator', +'rehab', +'reheat', +'rehire', +'rehydrate', +'reimburse', +'reissue', +'reiterate', +'rejoice', +'rejoicing', +'rejoin', +'rekindle', +'relapse', +'relapsing', +'relatable', +'related', +'relation', +'relative', +'relax', +'relay', +'relearn', +'release', +'relenting', +'reliable', +'reliably', +'reliance', +'reliant', +'relic', +'relieve', +'relieving', +'relight', +'relish', +'relive', +'reload', +'relocate', +'relock', +'reluctant', +'rely', +'remake', +'remark', +'remarry', +'rematch', +'remedial', +'remedy', +'remember', +'reminder', +'remindful', +'remission', +'remix', +'remnant', +'remodeler', +'remold', +'remorse', +'remote', +'removable', +'removal', +'removed', +'remover', +'removing', +'rename', +'renderer', +'rendering', +'rendition', +'renegade', +'renewable', +'renewably', +'renewal', +'renewed', +'renounce', +'renovate', +'renovator', +'rentable', +'rental', +'rented', +'renter', +'reoccupy', +'reoccur', +'reopen', +'reorder', +'repackage', +'repacking', +'repaint', +'repair', +'repave', +'repaying', +'repayment', +'repeal', +'repeated', +'repeater', +'repent', +'rephrase', +'replace', +'replay', +'replica', +'reply', +'reporter', +'repose', +'repossess', +'repost', +'repressed', +'reprimand', +'reprint', +'reprise', +'reproach', +'reprocess', +'reproduce', +'reprogram', +'reps', +'reptile', +'reptilian', +'repugnant', +'repulsion', +'repulsive', +'repurpose', +'reputable', +'reputably', +'request', +'require', +'requisite', +'reroute', +'rerun', +'resale', +'resample', +'rescuer', +'reseal', +'research', +'reselect', +'reseller', +'resemble', +'resend', +'resent', +'reset', +'reshape', +'reshoot', +'reshuffle', +'residence', +'residency', +'resident', +'residual', +'residue', +'resigned', +'resilient', +'resistant', +'resisting', +'resize', +'resolute', +'resolved', +'resonant', +'resonate', +'resort', +'resource', +'respect', +'resubmit', +'result', +'resume', +'resupply', +'resurface', +'resurrect', +'retail', +'retainer', +'retaining', +'retake', +'retaliate', +'retention', +'rethink', +'retinal', +'retired', +'retiree', +'retiring', +'retold', +'retool', +'retorted', +'retouch', +'retrace', +'retract', +'retrain', +'retread', +'retreat', +'retrial', +'retrieval', +'retriever', +'retry', +'return', +'retying', +'retype', +'reunion', +'reunite', +'reusable', +'reuse', +'reveal', +'reveler', +'revenge', +'revenue', +'reverb', +'revered', +'reverence', +'reverend', +'reversal', +'reverse', +'reversing', +'reversion', +'revert', +'revisable', +'revise', +'revision', +'revisit', +'revivable', +'revival', +'reviver', +'reviving', +'revocable', +'revoke', +'revolt', +'revolver', +'revolving', +'reward', +'rewash', +'rewind', +'rewire', +'reword', +'rework', +'rewrap', +'rewrite', +'rhyme', +'ribbon', +'ribcage', +'rice', +'riches', +'richly', +'richness', +'rickety', +'ricotta', +'riddance', +'ridden', +'ride', +'riding', +'rifling', +'rift', +'rigging', +'rigid', +'rigor', +'rimless', +'rimmed', +'rind', +'rink', +'rinse', +'rinsing', +'riot', +'ripcord', +'ripeness', +'ripening', +'ripping', +'ripple', +'rippling', +'riptide', +'rise', +'rising', +'risk', +'risotto', +'ritalin', +'ritzy', +'rival', +'riverbank', +'riverbed', +'riverboat', +'riverside', +'riveter', +'riveting', +'roamer', +'roaming', +'roast', +'robbing', +'robe', +'robin', +'robotics', +'robust', +'rockband', +'rocker', +'rocket', +'rockfish', +'rockiness', +'rocking', +'rocklike', +'rockslide', +'rockstar', +'rocky', +'rogue', +'roman', +'romp', +'rope', +'roping', +'roster', +'rosy', +'rotten', +'rotting', +'rotunda', +'roulette', +'rounding', +'roundish', +'roundness', +'roundup', +'roundworm', +'routine', +'routing', +'rover', +'roving', +'royal', +'rubbed', +'rubber', +'rubbing', +'rubble', +'rubdown', +'ruby', +'ruckus', +'rudder', +'rug', +'ruined', +'rule', +'rumble', +'rumbling', +'rummage', +'rumor', +'runaround', +'rundown', +'runner', +'running', +'runny', +'runt', +'runway', +'rupture', +'rural', +'ruse', +'rush', +'rust', +'rut', +'sabbath', +'sabotage', +'sacrament', +'sacred', +'sacrifice', +'sadden', +'saddlebag', +'saddled', +'saddling', +'sadly', +'sadness', +'safari', +'safeguard', +'safehouse', +'safely', +'safeness', +'saffron', +'saga', +'sage', +'sagging', +'saggy', +'said', +'saint', +'sake', +'salad', +'salami', +'salaried', +'salary', +'saline', +'salon', +'saloon', +'salsa', +'salt', +'salutary', +'salute', +'salvage', +'salvaging', +'salvation', +'same', +'sample', +'sampling', +'sanction', +'sanctity', +'sanctuary', +'sandal', +'sandbag', +'sandbank', +'sandbar', +'sandblast', +'sandbox', +'sanded', +'sandfish', +'sanding', +'sandlot', +'sandpaper', +'sandpit', +'sandstone', +'sandstorm', +'sandworm', +'sandy', +'sanitary', +'sanitizer', +'sank', +'santa', +'sapling', +'sappiness', +'sappy', +'sarcasm', +'sarcastic', +'sardine', +'sash', +'sasquatch', +'sassy', +'satchel', +'satiable', +'satin', +'satirical', +'satisfied', +'satisfy', +'saturate', +'saturday', +'sauciness', +'saucy', +'sauna', +'savage', +'savanna', +'saved', +'savings', +'savior', +'savor', +'saxophone', +'say', +'scabbed', +'scabby', +'scalded', +'scalding', +'scale', +'scaling', +'scallion', +'scallop', +'scalping', +'scam', +'scandal', +'scanner', +'scanning', +'scant', +'scapegoat', +'scarce', +'scarcity', +'scarecrow', +'scared', +'scarf', +'scarily', +'scariness', +'scarring', +'scary', +'scavenger', +'scenic', +'schedule', +'schematic', +'scheme', +'scheming', +'schilling', +'schnapps', +'scholar', +'science', +'scientist', +'scion', +'scoff', +'scolding', +'scone', +'scoop', +'scooter', +'scope', +'scorch', +'scorebook', +'scorecard', +'scored', +'scoreless', +'scorer', +'scoring', +'scorn', +'scorpion', +'scotch', +'scoundrel', +'scoured', +'scouring', +'scouting', +'scouts', +'scowling', +'scrabble', +'scraggly', +'scrambled', +'scrambler', +'scrap', +'scratch', +'scrawny', +'screen', +'scribble', +'scribe', +'scribing', +'scrimmage', +'script', +'scroll', +'scrooge', +'scrounger', +'scrubbed', +'scrubber', +'scruffy', +'scrunch', +'scrutiny', +'scuba', +'scuff', +'sculptor', +'sculpture', +'scurvy', +'scuttle', +'secluded', +'secluding', +'seclusion', +'second', +'secrecy', +'secret', +'sectional', +'sector', +'secular', +'securely', +'security', +'sedan', +'sedate', +'sedation', +'sedative', +'sediment', +'seduce', +'seducing', +'segment', +'seismic', +'seizing', +'seldom', +'selected', +'selection', +'selective', +'selector', +'self', +'seltzer', +'semantic', +'semester', +'semicolon', +'semifinal', +'seminar', +'semisoft', +'semisweet', +'senate', +'senator', +'send', +'senior', +'senorita', +'sensation', +'sensitive', +'sensitize', +'sensually', +'sensuous', +'sepia', +'september', +'septic', +'septum', +'sequel', +'sequence', +'sequester', +'series', +'sermon', +'serotonin', +'serpent', +'serrated', +'serve', +'service', +'serving', +'sesame', +'sessions', +'setback', +'setting', +'settle', +'settling', +'setup', +'sevenfold', +'seventeen', +'seventh', +'seventy', +'severity', +'shabby', +'shack', +'shaded', +'shadily', +'shadiness', +'shading', +'shadow', +'shady', +'shaft', +'shakable', +'shakily', +'shakiness', +'shaking', +'shaky', +'shale', +'shallot', +'shallow', +'shame', +'shampoo', +'shamrock', +'shank', +'shanty', +'shape', +'shaping', +'share', +'sharpener', +'sharper', +'sharpie', +'sharply', +'sharpness', +'shawl', +'sheath', +'shed', +'sheep', +'sheet', +'shelf', +'shell', +'shelter', +'shelve', +'shelving', +'sherry', +'shield', +'shifter', +'shifting', +'shiftless', +'shifty', +'shimmer', +'shimmy', +'shindig', +'shine', +'shingle', +'shininess', +'shining', +'shiny', +'ship', +'shirt', +'shivering', +'shock', +'shone', +'shoplift', +'shopper', +'shopping', +'shoptalk', +'shore', +'shortage', +'shortcake', +'shortcut', +'shorten', +'shorter', +'shorthand', +'shortlist', +'shortly', +'shortness', +'shorts', +'shortwave', +'shorty', +'shout', +'shove', +'showbiz', +'showcase', +'showdown', +'shower', +'showgirl', +'showing', +'showman', +'shown', +'showoff', +'showpiece', +'showplace', +'showroom', +'showy', +'shrank', +'shrapnel', +'shredder', +'shredding', +'shrewdly', +'shriek', +'shrill', +'shrimp', +'shrine', +'shrink', +'shrivel', +'shrouded', +'shrubbery', +'shrubs', +'shrug', +'shrunk', +'shucking', +'shudder', +'shuffle', +'shuffling', +'shun', +'shush', +'shut', +'shy', +'siamese', +'siberian', +'sibling', +'siding', +'sierra', +'siesta', +'sift', +'sighing', +'silenced', +'silencer', +'silent', +'silica', +'silicon', +'silk', +'silliness', +'silly', +'silo', +'silt', +'silver', +'similarly', +'simile', +'simmering', +'simple', +'simplify', +'simply', +'sincere', +'sincerity', +'singer', +'singing', +'single', +'singular', +'sinister', +'sinless', +'sinner', +'sinuous', +'sip', +'siren', +'sister', +'sitcom', +'sitter', +'sitting', +'situated', +'situation', +'sixfold', +'sixteen', +'sixth', +'sixties', +'sixtieth', +'sixtyfold', +'sizable', +'sizably', +'size', +'sizing', +'sizzle', +'sizzling', +'skater', +'skating', +'skedaddle', +'skeletal', +'skeleton', +'skeptic', +'sketch', +'skewed', +'skewer', +'skid', +'skied', +'skier', +'skies', +'skiing', +'skilled', +'skillet', +'skillful', +'skimmed', +'skimmer', +'skimming', +'skimpily', +'skincare', +'skinhead', +'skinless', +'skinning', +'skinny', +'skintight', +'skipper', +'skipping', +'skirmish', +'skirt', +'skittle', +'skydiver', +'skylight', +'skyline', +'skype', +'skyrocket', +'skyward', +'slab', +'slacked', +'slacker', +'slacking', +'slackness', +'slacks', +'slain', +'slam', +'slander', +'slang', +'slapping', +'slapstick', +'slashed', +'slashing', +'slate', +'slather', +'slaw', +'sled', +'sleek', +'sleep', +'sleet', +'sleeve', +'slept', +'sliceable', +'sliced', +'slicer', +'slicing', +'slick', +'slider', +'slideshow', +'sliding', +'slighted', +'slighting', +'slightly', +'slimness', +'slimy', +'slinging', +'slingshot', +'slinky', +'slip', +'slit', +'sliver', +'slobbery', +'slogan', +'sloped', +'sloping', +'sloppily', +'sloppy', +'slot', +'slouching', +'slouchy', +'sludge', +'slug', +'slum', +'slurp', +'slush', +'sly', +'small', +'smartly', +'smartness', +'smasher', +'smashing', +'smashup', +'smell', +'smelting', +'smile', +'smilingly', +'smirk', +'smite', +'smith', +'smitten', +'smock', +'smog', +'smoked', +'smokeless', +'smokiness', +'smoking', +'smoky', +'smolder', +'smooth', +'smother', +'smudge', +'smudgy', +'smuggler', +'smuggling', +'smugly', +'smugness', +'snack', +'snagged', +'snaking', +'snap', +'snare', +'snarl', +'snazzy', +'sneak', +'sneer', +'sneeze', +'sneezing', +'snide', +'sniff', +'snippet', +'snipping', +'snitch', +'snooper', +'snooze', +'snore', +'snoring', +'snorkel', +'snort', +'snout', +'snowbird', +'snowboard', +'snowbound', +'snowcap', +'snowdrift', +'snowdrop', +'snowfall', +'snowfield', +'snowflake', +'snowiness', +'snowless', +'snowman', +'snowplow', +'snowshoe', +'snowstorm', +'snowsuit', +'snowy', +'snub', +'snuff', +'snuggle', +'snugly', +'snugness', +'speak', +'spearfish', +'spearhead', +'spearman', +'spearmint', +'species', +'specimen', +'specked', +'speckled', +'specks', +'spectacle', +'spectator', +'spectrum', +'speculate', +'speech', +'speed', +'spellbind', +'speller', +'spelling', +'spendable', +'spender', +'spending', +'spent', +'spew', +'sphere', +'spherical', +'sphinx', +'spider', +'spied', +'spiffy', +'spill', +'spilt', +'spinach', +'spinal', +'spindle', +'spinner', +'spinning', +'spinout', +'spinster', +'spiny', +'spiral', +'spirited', +'spiritism', +'spirits', +'spiritual', +'splashed', +'splashing', +'splashy', +'splatter', +'spleen', +'splendid', +'splendor', +'splice', +'splicing', +'splinter', +'splotchy', +'splurge', +'spoilage', +'spoiled', +'spoiler', +'spoiling', +'spoils', +'spoken', +'spokesman', +'sponge', +'spongy', +'sponsor', +'spoof', +'spookily', +'spooky', +'spool', +'spoon', +'spore', +'sporting', +'sports', +'sporty', +'spotless', +'spotlight', +'spotted', +'spotter', +'spotting', +'spotty', +'spousal', +'spouse', +'spout', +'sprain', +'sprang', +'sprawl', +'spray', +'spree', +'sprig', +'spring', +'sprinkled', +'sprinkler', +'sprint', +'sprite', +'sprout', +'spruce', +'sprung', +'spry', +'spud', +'spur', +'sputter', +'spyglass', +'squabble', +'squad', +'squall', +'squander', +'squash', +'squatted', +'squatter', +'squatting', +'squeak', +'squealer', +'squealing', +'squeamish', +'squeegee', +'squeeze', +'squeezing', +'squid', +'squiggle', +'squiggly', +'squint', +'squire', +'squirt', +'squishier', +'squishy', +'stability', +'stabilize', +'stable', +'stack', +'stadium', +'staff', +'stage', +'staging', +'stagnant', +'stagnate', +'stainable', +'stained', +'staining', +'stainless', +'stalemate', +'staleness', +'stalling', +'stallion', +'stamina', +'stammer', +'stamp', +'stand', +'stank', +'staple', +'stapling', +'starboard', +'starch', +'stardom', +'stardust', +'starfish', +'stargazer', +'staring', +'stark', +'starless', +'starlet', +'starlight', +'starlit', +'starring', +'starry', +'starship', +'starter', +'starting', +'startle', +'startling', +'startup', +'starved', +'starving', +'stash', +'state', +'static', +'statistic', +'statue', +'stature', +'status', +'statute', +'statutory', +'staunch', +'stays', +'steadfast', +'steadier', +'steadily', +'steadying', +'steam', +'steed', +'steep', +'steerable', +'steering', +'steersman', +'stegosaur', +'stellar', +'stem', +'stench', +'stencil', +'step', +'stereo', +'sterile', +'sterility', +'sterilize', +'sterling', +'sternness', +'sternum', +'stew', +'stick', +'stiffen', +'stiffly', +'stiffness', +'stifle', +'stifling', +'stillness', +'stilt', +'stimulant', +'stimulate', +'stimuli', +'stimulus', +'stinger', +'stingily', +'stinging', +'stingray', +'stingy', +'stinking', +'stinky', +'stipend', +'stipulate', +'stir', +'stitch', +'stock', +'stoic', +'stoke', +'stole', +'stomp', +'stonewall', +'stoneware', +'stonework', +'stoning', +'stony', +'stood', +'stooge', +'stool', +'stoop', +'stoplight', +'stoppable', +'stoppage', +'stopped', +'stopper', +'stopping', +'stopwatch', +'storable', +'storage', +'storeroom', +'storewide', +'storm', +'stout', +'stove', +'stowaway', +'stowing', +'straddle', +'straggler', +'strained', +'strainer', +'straining', +'strangely', +'stranger', +'strangle', +'strategic', +'strategy', +'stratus', +'straw', +'stray', +'streak', +'stream', +'street', +'strength', +'strenuous', +'strep', +'stress', +'stretch', +'strewn', +'stricken', +'strict', +'stride', +'strife', +'strike', +'striking', +'strive', +'striving', +'strobe', +'strode', +'stroller', +'strongbox', +'strongly', +'strongman', +'struck', +'structure', +'strudel', +'struggle', +'strum', +'strung', +'strut', +'stubbed', +'stubble', +'stubbly', +'stubborn', +'stucco', +'stuck', +'student', +'studied', +'studio', +'study', +'stuffed', +'stuffing', +'stuffy', +'stumble', +'stumbling', +'stump', +'stung', +'stunned', +'stunner', +'stunning', +'stunt', +'stupor', +'sturdily', +'sturdy', +'styling', +'stylishly', +'stylist', +'stylized', +'stylus', +'suave', +'subarctic', +'subatomic', +'subdivide', +'subdued', +'subduing', +'subfloor', +'subgroup', +'subheader', +'subject', +'sublease', +'sublet', +'sublevel', +'sublime', +'submarine', +'submerge', +'submersed', +'submitter', +'subpanel', +'subpar', +'subplot', +'subprime', +'subscribe', +'subscript', +'subsector', +'subside', +'subsiding', +'subsidize', +'subsidy', +'subsoil', +'subsonic', +'substance', +'subsystem', +'subtext', +'subtitle', +'subtly', +'subtotal', +'subtract', +'subtype', +'suburb', +'subway', +'subwoofer', +'subzero', +'succulent', +'such', +'suction', +'sudden', +'sudoku', +'suds', +'sufferer', +'suffering', +'suffice', +'suffix', +'suffocate', +'suffrage', +'sugar', +'suggest', +'suing', +'suitable', +'suitably', +'suitcase', +'suitor', +'sulfate', +'sulfide', +'sulfite', +'sulfur', +'sulk', +'sullen', +'sulphate', +'sulphuric', +'sultry', +'superbowl', +'superglue', +'superhero', +'superior', +'superjet', +'superman', +'supermom', +'supernova', +'supervise', +'supper', +'supplier', +'supply', +'support', +'supremacy', +'supreme', +'surcharge', +'surely', +'sureness', +'surface', +'surfacing', +'surfboard', +'surfer', +'surgery', +'surgical', +'surging', +'surname', +'surpass', +'surplus', +'surprise', +'surreal', +'surrender', +'surrogate', +'surround', +'survey', +'survival', +'survive', +'surviving', +'survivor', +'sushi', +'suspect', +'suspend', +'suspense', +'sustained', +'sustainer', +'swab', +'swaddling', +'swagger', +'swampland', +'swan', +'swapping', +'swarm', +'sway', +'swear', +'sweat', +'sweep', +'swell', +'swept', +'swerve', +'swifter', +'swiftly', +'swiftness', +'swimmable', +'swimmer', +'swimming', +'swimsuit', +'swimwear', +'swinger', +'swinging', +'swipe', +'swirl', +'switch', +'swivel', +'swizzle', +'swooned', +'swoop', +'swoosh', +'swore', +'sworn', +'swung', +'sycamore', +'sympathy', +'symphonic', +'symphony', +'symptom', +'synapse', +'syndrome', +'synergy', +'synopses', +'synopsis', +'synthesis', +'synthetic', +'syrup', +'system', +'t-shirt', +'tabasco', +'tabby', +'tableful', +'tables', +'tablet', +'tableware', +'tabloid', +'tackiness', +'tacking', +'tackle', +'tackling', +'tacky', +'taco', +'tactful', +'tactical', +'tactics', +'tactile', +'tactless', +'tadpole', +'taekwondo', +'tag', +'tainted', +'take', +'taking', +'talcum', +'talisman', +'tall', +'talon', +'tamale', +'tameness', +'tamer', +'tamper', +'tank', +'tanned', +'tannery', +'tanning', +'tantrum', +'tapeless', +'tapered', +'tapering', +'tapestry', +'tapioca', +'tapping', +'taps', +'tarantula', +'target', +'tarmac', +'tarnish', +'tarot', +'tartar', +'tartly', +'tartness', +'task', +'tassel', +'taste', +'tastiness', +'tasting', +'tasty', +'tattered', +'tattle', +'tattling', +'tattoo', +'taunt', +'tavern', +'thank', +'that', +'thaw', +'theater', +'theatrics', +'thee', +'theft', +'theme', +'theology', +'theorize', +'thermal', +'thermos', +'thesaurus', +'these', +'thesis', +'thespian', +'thicken', +'thicket', +'thickness', +'thieving', +'thievish', +'thigh', +'thimble', +'thing', +'think', +'thinly', +'thinner', +'thinness', +'thinning', +'thirstily', +'thirsting', +'thirsty', +'thirteen', +'thirty', +'thong', +'thorn', +'those', +'thousand', +'thrash', +'thread', +'threaten', +'threefold', +'thrift', +'thrill', +'thrive', +'thriving', +'throat', +'throbbing', +'throng', +'throttle', +'throwaway', +'throwback', +'thrower', +'throwing', +'thud', +'thumb', +'thumping', +'thursday', +'thus', +'thwarting', +'thyself', +'tiara', +'tibia', +'tidal', +'tidbit', +'tidiness', +'tidings', +'tidy', +'tiger', +'tighten', +'tightly', +'tightness', +'tightrope', +'tightwad', +'tigress', +'tile', +'tiling', +'till', +'tilt', +'timid', +'timing', +'timothy', +'tinderbox', +'tinfoil', +'tingle', +'tingling', +'tingly', +'tinker', +'tinkling', +'tinsel', +'tinsmith', +'tint', +'tinwork', +'tiny', +'tipoff', +'tipped', +'tipper', +'tipping', +'tiptoeing', +'tiptop', +'tiring', +'tissue', +'trace', +'tracing', +'track', +'traction', +'tractor', +'trade', +'trading', +'tradition', +'traffic', +'tragedy', +'trailing', +'trailside', +'train', +'traitor', +'trance', +'tranquil', +'transfer', +'transform', +'translate', +'transpire', +'transport', +'transpose', +'trapdoor', +'trapeze', +'trapezoid', +'trapped', +'trapper', +'trapping', +'traps', +'trash', +'travel', +'traverse', +'travesty', +'tray', +'treachery', +'treading', +'treadmill', +'treason', +'treat', +'treble', +'tree', +'trekker', +'tremble', +'trembling', +'tremor', +'trench', +'trend', +'trespass', +'triage', +'trial', +'triangle', +'tribesman', +'tribunal', +'tribune', +'tributary', +'tribute', +'triceps', +'trickery', +'trickily', +'tricking', +'trickle', +'trickster', +'tricky', +'tricolor', +'tricycle', +'trident', +'tried', +'trifle', +'trifocals', +'trillion', +'trilogy', +'trimester', +'trimmer', +'trimming', +'trimness', +'trinity', +'trio', +'tripod', +'tripping', +'triumph', +'trivial', +'trodden', +'trolling', +'trombone', +'trophy', +'tropical', +'tropics', +'trouble', +'troubling', +'trough', +'trousers', +'trout', +'trowel', +'truce', +'truck', +'truffle', +'trump', +'trunks', +'trustable', +'trustee', +'trustful', +'trusting', +'trustless', +'truth', +'try', +'tubby', +'tubeless', +'tubular', +'tucking', +'tuesday', +'tug', +'tuition', +'tulip', +'tumble', +'tumbling', +'tummy', +'turban', +'turbine', +'turbofan', +'turbojet', +'turbulent', +'turf', +'turkey', +'turmoil', +'turret', +'turtle', +'tusk', +'tutor', +'tutu', +'tux', +'tweak', +'tweed', +'tweet', +'tweezers', +'twelve', +'twentieth', +'twenty', +'twerp', +'twice', +'twiddle', +'twiddling', +'twig', +'twilight', +'twine', +'twins', +'twirl', +'twistable', +'twisted', +'twister', +'twisting', +'twisty', +'twitch', +'twitter', +'tycoon', +'tying', +'tyke', +'udder', +'ultimate', +'ultimatum', +'ultra', +'umbilical', +'umbrella', +'umpire', +'unabashed', +'unable', +'unadorned', +'unadvised', +'unafraid', +'unaired', +'unaligned', +'unaltered', +'unarmored', +'unashamed', +'unaudited', +'unawake', +'unaware', +'unbaked', +'unbalance', +'unbeaten', +'unbend', +'unbent', +'unbiased', +'unbitten', +'unblended', +'unblessed', +'unblock', +'unbolted', +'unbounded', +'unboxed', +'unbraided', +'unbridle', +'unbroken', +'unbuckled', +'unbundle', +'unburned', +'unbutton', +'uncanny', +'uncapped', +'uncaring', +'uncertain', +'unchain', +'unchanged', +'uncharted', +'uncheck', +'uncivil', +'unclad', +'unclaimed', +'unclamped', +'unclasp', +'uncle', +'unclip', +'uncloak', +'unclog', +'unclothed', +'uncoated', +'uncoiled', +'uncolored', +'uncombed', +'uncommon', +'uncooked', +'uncork', +'uncorrupt', +'uncounted', +'uncouple', +'uncouth', +'uncover', +'uncross', +'uncrown', +'uncrushed', +'uncured', +'uncurious', +'uncurled', +'uncut', +'undamaged', +'undated', +'undaunted', +'undead', +'undecided', +'undefined', +'underage', +'underarm', +'undercoat', +'undercook', +'undercut', +'underdog', +'underdone', +'underfed', +'underfeed', +'underfoot', +'undergo', +'undergrad', +'underhand', +'underline', +'underling', +'undermine', +'undermost', +'underpaid', +'underpass', +'underpay', +'underrate', +'undertake', +'undertone', +'undertook', +'undertow', +'underuse', +'underwear', +'underwent', +'underwire', +'undesired', +'undiluted', +'undivided', +'undocked', +'undoing', +'undone', +'undrafted', +'undress', +'undrilled', +'undusted', +'undying', +'unearned', +'unearth', +'unease', +'uneasily', +'uneasy', +'uneatable', +'uneaten', +'unedited', +'unelected', +'unending', +'unengaged', +'unenvied', +'unequal', +'unethical', +'uneven', +'unexpired', +'unexposed', +'unfailing', +'unfair', +'unfasten', +'unfazed', +'unfeeling', +'unfiled', +'unfilled', +'unfitted', +'unfitting', +'unfixable', +'unfixed', +'unflawed', +'unfocused', +'unfold', +'unfounded', +'unframed', +'unfreeze', +'unfrosted', +'unfrozen', +'unfunded', +'unglazed', +'ungloved', +'unglue', +'ungodly', +'ungraded', +'ungreased', +'unguarded', +'unguided', +'unhappily', +'unhappy', +'unharmed', +'unhealthy', +'unheard', +'unhearing', +'unheated', +'unhelpful', +'unhidden', +'unhinge', +'unhitched', +'unholy', +'unhook', +'unicorn', +'unicycle', +'unified', +'unifier', +'uniformed', +'uniformly', +'unify', +'unimpeded', +'uninjured', +'uninstall', +'uninsured', +'uninvited', +'union', +'uniquely', +'unisexual', +'unison', +'unissued', +'unit', +'universal', +'universe', +'unjustly', +'unkempt', +'unkind', +'unknotted', +'unknowing', +'unknown', +'unlaced', +'unlatch', +'unlawful', +'unleaded', +'unlearned', +'unleash', +'unless', +'unleveled', +'unlighted', +'unlikable', +'unlimited', +'unlined', +'unlinked', +'unlisted', +'unlit', +'unlivable', +'unloaded', +'unloader', +'unlocked', +'unlocking', +'unlovable', +'unloved', +'unlovely', +'unloving', +'unluckily', +'unlucky', +'unmade', +'unmanaged', +'unmanned', +'unmapped', +'unmarked', +'unmasked', +'unmasking', +'unmatched', +'unmindful', +'unmixable', +'unmixed', +'unmolded', +'unmoral', +'unmovable', +'unmoved', +'unmoving', +'unnamable', +'unnamed', +'unnatural', +'unneeded', +'unnerve', +'unnerving', +'unnoticed', +'unopened', +'unopposed', +'unpack', +'unpadded', +'unpaid', +'unpainted', +'unpaired', +'unpaved', +'unpeeled', +'unpicked', +'unpiloted', +'unpinned', +'unplanned', +'unplanted', +'unpleased', +'unpledged', +'unplowed', +'unplug', +'unpopular', +'unproven', +'unquote', +'unranked', +'unrated', +'unraveled', +'unreached', +'unread', +'unreal', +'unreeling', +'unrefined', +'unrelated', +'unrented', +'unrest', +'unretired', +'unrevised', +'unrigged', +'unripe', +'unrivaled', +'unroasted', +'unrobed', +'unroll', +'unruffled', +'unruly', +'unrushed', +'unsaddle', +'unsafe', +'unsaid', +'unsalted', +'unsaved', +'unsavory', +'unscathed', +'unscented', +'unscrew', +'unsealed', +'unseated', +'unsecured', +'unseeing', +'unseemly', +'unseen', +'unselect', +'unselfish', +'unsent', +'unsettled', +'unshackle', +'unshaken', +'unshaved', +'unshaven', +'unsheathe', +'unshipped', +'unsightly', +'unsigned', +'unskilled', +'unsliced', +'unsmooth', +'unsnap', +'unsocial', +'unsoiled', +'unsold', +'unsolved', +'unsorted', +'unspoiled', +'unspoken', +'unstable', +'unstaffed', +'unstamped', +'unsteady', +'unsterile', +'unstirred', +'unstitch', +'unstopped', +'unstuck', +'unstuffed', +'unstylish', +'unsubtle', +'unsubtly', +'unsuited', +'unsure', +'unsworn', +'untagged', +'untainted', +'untaken', +'untamed', +'untangled', +'untapped', +'untaxed', +'unthawed', +'unthread', +'untidy', +'untie', +'until', +'untimed', +'untimely', +'untitled', +'untoasted', +'untold', +'untouched', +'untracked', +'untrained', +'untreated', +'untried', +'untrimmed', +'untrue', +'untruth', +'unturned', +'untwist', +'untying', +'unusable', +'unused', +'unusual', +'unvalued', +'unvaried', +'unvarying', +'unveiled', +'unveiling', +'unvented', +'unviable', +'unvisited', +'unvocal', +'unwanted', +'unwarlike', +'unwary', +'unwashed', +'unwatched', +'unweave', +'unwed', +'unwelcome', +'unwell', +'unwieldy', +'unwilling', +'unwind', +'unwired', +'unwitting', +'unwomanly', +'unworldly', +'unworn', +'unworried', +'unworthy', +'unwound', +'unwoven', +'unwrapped', +'unwritten', +'unzip', +'upbeat', +'upchuck', +'upcoming', +'upcountry', +'update', +'upfront', +'upgrade', +'upheaval', +'upheld', +'uphill', +'uphold', +'uplifted', +'uplifting', +'upload', +'upon', +'upper', +'upright', +'uprising', +'upriver', +'uproar', +'uproot', +'upscale', +'upside', +'upstage', +'upstairs', +'upstart', +'upstate', +'upstream', +'upstroke', +'upswing', +'uptake', +'uptight', +'uptown', +'upturned', +'upward', +'upwind', +'uranium', +'urban', +'urchin', +'urethane', +'urgency', +'urgent', +'urging', +'urologist', +'urology', +'usable', +'usage', +'useable', +'used', +'uselessly', +'user', +'usher', +'usual', +'utensil', +'utility', +'utilize', +'utmost', +'utopia', +'utter', +'vacancy', +'vacant', +'vacate', +'vacation', +'vagabond', +'vagrancy', +'vagrantly', +'vaguely', +'vagueness', +'valiant', +'valid', +'valium', +'valley', +'valuables', +'value', +'vanilla', +'vanish', +'vanity', +'vanquish', +'vantage', +'vaporizer', +'variable', +'variably', +'varied', +'variety', +'various', +'varmint', +'varnish', +'varsity', +'varying', +'vascular', +'vaseline', +'vastly', +'vastness', +'veal', +'vegan', +'veggie', +'vehicular', +'velcro', +'velocity', +'velvet', +'vendetta', +'vending', +'vendor', +'veneering', +'vengeful', +'venomous', +'ventricle', +'venture', +'venue', +'venus', +'verbalize', +'verbally', +'verbose', +'verdict', +'verify', +'verse', +'version', +'versus', +'vertebrae', +'vertical', +'vertigo', +'very', +'vessel', +'vest', +'veteran', +'veto', +'vexingly', +'viability', +'viable', +'vibes', +'vice', +'vicinity', +'victory', +'video', +'viewable', +'viewer', +'viewing', +'viewless', +'viewpoint', +'vigorous', +'village', +'villain', +'vindicate', +'vineyard', +'vintage', +'violate', +'violation', +'violator', +'violet', +'violin', +'viper', +'viral', +'virtual', +'virtuous', +'virus', +'visa', +'viscosity', +'viscous', +'viselike', +'visible', +'visibly', +'vision', +'visiting', +'visitor', +'visor', +'vista', +'vitality', +'vitalize', +'vitally', +'vitamins', +'vivacious', +'vividly', +'vividness', +'vixen', +'vocalist', +'vocalize', +'vocally', +'vocation', +'voice', +'voicing', +'void', +'volatile', +'volley', +'voltage', +'volumes', +'voter', +'voting', +'voucher', +'vowed', +'vowel', +'voyage', +'wackiness', +'wad', +'wafer', +'waffle', +'waged', +'wager', +'wages', +'waggle', +'wagon', +'wake', +'waking', +'walk', +'walmart', +'walnut', +'walrus', +'waltz', +'wand', +'wannabe', +'wanted', +'wanting', +'wasabi', +'washable', +'washbasin', +'washboard', +'washbowl', +'washcloth', +'washday', +'washed', +'washer', +'washhouse', +'washing', +'washout', +'washroom', +'washstand', +'washtub', +'wasp', +'wasting', +'watch', +'water', +'waviness', +'waving', +'wavy', +'whacking', +'whacky', +'wham', +'wharf', +'wheat', +'whenever', +'whiff', +'whimsical', +'whinny', +'whiny', +'whisking', +'whoever', +'whole', +'whomever', +'whoopee', +'whooping', +'whoops', +'why', +'wick', +'widely', +'widen', +'widget', +'widow', +'width', +'wieldable', +'wielder', +'wife', +'wifi', +'wikipedia', +'wildcard', +'wildcat', +'wilder', +'wildfire', +'wildfowl', +'wildland', +'wildlife', +'wildly', +'wildness', +'willed', +'willfully', +'willing', +'willow', +'willpower', +'wilt', +'wimp', +'wince', +'wincing', +'wind', +'wing', +'winking', +'winner', +'winnings', +'winter', +'wipe', +'wired', +'wireless', +'wiring', +'wiry', +'wisdom', +'wise', +'wish', +'wisplike', +'wispy', +'wistful', +'wizard', +'wobble', +'wobbling', +'wobbly', +'wok', +'wolf', +'wolverine', +'womanhood', +'womankind', +'womanless', +'womanlike', +'womanly', +'womb', +'woof', +'wooing', +'wool', +'woozy', +'word', +'work', +'worried', +'worrier', +'worrisome', +'worry', +'worsening', +'worshiper', +'worst', +'wound', +'woven', +'wow', +'wrangle', +'wrath', +'wreath', +'wreckage', +'wrecker', +'wrecking', +'wrench', +'wriggle', +'wriggly', +'wrinkle', +'wrinkly', +'wrist', +'writing', +'written', +'wrongdoer', +'wronged', +'wrongful', +'wrongly', +'wrongness', +'wrought', +'xbox', +'xerox', +'yahoo', +'yam', +'yanking', +'yapping', +'yard', +'yarn', +'yeah', +'yearbook', +'yearling', +'yearly', +'yearning', +'yeast', +'yelling', +'yelp', +'yen', +'yesterday', +'yiddish', +'yield', +'yin', +'yippee', +'yo-yo', +'yodel', +'yoga', +'yogurt', +'yonder', +'yoyo', +'yummy', +'zap', +'zealous', +'zebra', +'zen', +'zeppelin', +'zero', +'zestfully', +'zesty', +'zigzagged', +'zipfile', +'zipping', +'zippy', +'zips', +'zit', +'zodiac', +'zombie', +'zone', +'zoning', +'zookeeper', +'zoologist', +'zoology', +'zoom']; diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index cc5ebe4f34..9227f40446 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -7,6 +7,8 @@ import { } from '../abstractions/passwordGeneration.service'; import { StorageService } from '../abstractions/storage.service'; +import { WordList } from '../misc/wordlist'; + const DefaultOptions = { length: 14, ambiguous: false, @@ -37,6 +39,10 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr // overload defaults with given options const o = Object.assign({}, DefaultOptions, options); + if (o.generatePassphrase) { + return this.generatePassphrase(options); + } + // sanitize if (o.uppercase && o.minUppercase <= 0) { o.minUppercase = 1; @@ -150,6 +156,18 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return password; } + async generatePassphrase(options: any): Promise { + const o = Object.assign({}, DefaultOptions, options); + + const listLength = WordList.length - 1; + const wordList = new Array(o.numWords); + for (let i = 0; i < o.numWords; i++) { + const wordindex = await this.cryptoService.randomNumber(0, listLength); + wordList[i] = WordList[wordindex]; + } + return wordList.join(' '); + } + async getOptions() { if (this.optionsCache == null) { const options = await this.storageService.get(Keys.options); From d5f86747bf5533308b271b5fade75a6761f5d4e5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 8 Oct 2018 17:54:54 -0400 Subject: [PATCH 0575/1626] passphrase cleanup --- .../passwordGeneration.service.ts | 1 + .../password-generator.component.ts | 13 +- src/misc/wordlist.ts | 15554 ++++++++-------- src/services/import.service.ts | 2 +- src/services/passwordGeneration.service.ts | 22 +- 5 files changed, 7802 insertions(+), 7790 deletions(-) diff --git a/src/abstractions/passwordGeneration.service.ts b/src/abstractions/passwordGeneration.service.ts index 414e29e710..c2d904cc96 100644 --- a/src/abstractions/passwordGeneration.service.ts +++ b/src/abstractions/passwordGeneration.service.ts @@ -2,6 +2,7 @@ import { GeneratedPasswordHistory } from '../models/domain/generatedPasswordHist export abstract class PasswordGenerationService { generatePassword: (options: any) => Promise; + generatePassphrase: (options: any) => Promise; getOptions: () => any; saveOptions: (options: any) => Promise; getHistory: () => Promise; diff --git a/src/angular/components/password-generator.component.ts b/src/angular/components/password-generator.component.ts index d95a76bcec..424640a434 100644 --- a/src/angular/components/password-generator.component.ts +++ b/src/angular/components/password-generator.component.ts @@ -17,6 +17,7 @@ export class PasswordGeneratorComponent implements OnInit { password: string = '-'; showOptions = false; avoidAmbiguous = false; + type = '0'; constructor(protected passwordGenerationService: PasswordGenerationService, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, @@ -25,6 +26,7 @@ export class PasswordGeneratorComponent implements OnInit { async ngOnInit() { this.options = await this.passwordGenerationService.getOptions(); this.avoidAmbiguous = !this.options.ambiguous; + this.type = this.options.type === 1 ? '1' : '0'; this.password = await this.passwordGenerationService.generatePassword(this.options); this.platformUtilsService.eventTrack('Generated Password'); await this.passwordGenerationService.addHistory(this.password); @@ -77,12 +79,7 @@ export class PasswordGeneratorComponent implements OnInit { this.options.minLowercase = 0; this.options.minUppercase = 0; this.options.ambiguous = !this.avoidAmbiguous; - - let typePassword = false; - if (!this.options.generateTypePassword || this.options.generateTypePassword === 'generate-password') { - typePassword = true; - } - this.options.generatePassphrase = !typePassword; + this.options.type = this.type == null || this.type === '0' ? 0 : 1; if (!this.options.uppercase && !this.options.lowercase && !this.options.number && !this.options.special) { this.options.lowercase = true; @@ -118,8 +115,10 @@ export class PasswordGeneratorComponent implements OnInit { this.options.minSpecial = this.options.length - this.options.minNumber; } - if (!this.options.numWords || this.options.length < 3) { + if (this.options.numWords == null || this.options.length < 3) { this.options.numWords = 3; + } else if (this.options.numWords > 20) { + this.options.numWords = 20; } } } diff --git a/src/misc/wordlist.ts b/src/misc/wordlist.ts index 1850a07958..fdbfa5ad3e 100644 --- a/src/misc/wordlist.ts +++ b/src/misc/wordlist.ts @@ -1,7777 +1,7779 @@ // EFF's Long Wordlist from https://www.eff.org/dice -export const WordList = ['abacus', -'abdomen', -'abdominal', -'abide', -'abiding', -'ability', -'ablaze', -'able', -'abnormal', -'abrasion', -'abrasive', -'abreast', -'abridge', -'abroad', -'abruptly', -'absence', -'absentee', -'absently', -'absinthe', -'absolute', -'absolve', -'abstain', -'abstract', -'absurd', -'accent', -'acclaim', -'acclimate', -'accompany', -'account', -'accuracy', -'accurate', -'accustom', -'acetone', -'achiness', -'aching', -'acid', -'acorn', -'acquaint', -'acquire', -'acre', -'acrobat', -'acronym', -'acting', -'action', -'activate', -'activator', -'active', -'activism', -'activist', -'activity', -'actress', -'acts', -'acutely', -'acuteness', -'aeration', -'aerobics', -'aerosol', -'aerospace', -'afar', -'affair', -'affected', -'affecting', -'affection', -'affidavit', -'affiliate', -'affirm', -'affix', -'afflicted', -'affluent', -'afford', -'affront', -'aflame', -'afloat', -'aflutter', -'afoot', -'afraid', -'afterglow', -'afterlife', -'aftermath', -'aftermost', -'afternoon', -'aged', -'ageless', -'agency', -'agenda', -'agent', -'aggregate', -'aghast', -'agile', -'agility', -'aging', -'agnostic', -'agonize', -'agonizing', -'agony', -'agreeable', -'agreeably', -'agreed', -'agreeing', -'agreement', -'aground', -'ahead', -'ahoy', -'aide', -'aids', -'aim', -'ajar', -'alabaster', -'alarm', -'albatross', -'album', -'alfalfa', -'algebra', -'algorithm', -'alias', -'alibi', -'alienable', -'alienate', -'aliens', -'alike', -'alive', -'alkaline', -'alkalize', -'almanac', -'almighty', -'almost', -'aloe', -'aloft', -'aloha', -'alone', -'alongside', -'aloof', -'alphabet', -'alright', -'although', -'altitude', -'alto', -'aluminum', -'alumni', -'always', -'amaretto', -'amaze', -'amazingly', -'amber', -'ambiance', -'ambiguity', -'ambiguous', -'ambition', -'ambitious', -'ambulance', -'ambush', -'amendable', -'amendment', -'amends', -'amenity', -'amiable', -'amicably', -'amid', -'amigo', -'amino', -'amiss', -'ammonia', -'ammonium', -'amnesty', -'amniotic', -'among', -'amount', -'amperage', -'ample', -'amplifier', -'amplify', -'amply', -'amuck', -'amulet', -'amusable', -'amused', -'amusement', -'amuser', -'amusing', -'anaconda', -'anaerobic', -'anagram', -'anatomist', -'anatomy', -'anchor', -'anchovy', -'ancient', -'android', -'anemia', -'anemic', -'aneurism', -'anew', -'angelfish', -'angelic', -'anger', -'angled', -'angler', -'angles', -'angling', -'angrily', -'angriness', -'anguished', -'angular', -'animal', -'animate', -'animating', -'animation', -'animator', -'anime', -'animosity', -'ankle', -'annex', -'annotate', -'announcer', -'annoying', -'annually', -'annuity', -'anointer', -'another', -'answering', -'antacid', -'antarctic', -'anteater', -'antelope', -'antennae', -'anthem', -'anthill', -'anthology', -'antibody', -'antics', -'antidote', -'antihero', -'antiquely', -'antiques', -'antiquity', -'antirust', -'antitoxic', -'antitrust', -'antiviral', -'antivirus', -'antler', -'antonym', -'antsy', -'anvil', -'anybody', -'anyhow', -'anymore', -'anyone', -'anyplace', -'anything', -'anytime', -'anyway', -'anywhere', -'aorta', -'apache', -'apostle', -'appealing', -'appear', -'appease', -'appeasing', -'appendage', -'appendix', -'appetite', -'appetizer', -'applaud', -'applause', -'apple', -'appliance', -'applicant', -'applied', -'apply', -'appointee', -'appraisal', -'appraiser', -'apprehend', -'approach', -'approval', -'approve', -'apricot', -'april', -'apron', -'aptitude', -'aptly', -'aqua', -'aqueduct', -'arbitrary', -'arbitrate', -'ardently', -'area', -'arena', -'arguable', -'arguably', -'argue', -'arise', -'armadillo', -'armband', -'armchair', -'armed', -'armful', -'armhole', -'arming', -'armless', -'armoire', -'armored', -'armory', -'armrest', -'army', -'aroma', -'arose', -'around', -'arousal', -'arrange', -'array', -'arrest', -'arrival', -'arrive', -'arrogance', -'arrogant', -'arson', -'art', -'ascend', -'ascension', -'ascent', -'ascertain', -'ashamed', -'ashen', -'ashes', -'ashy', -'aside', -'askew', -'asleep', -'asparagus', -'aspect', -'aspirate', -'aspire', -'aspirin', -'astonish', -'astound', -'astride', -'astrology', -'astronaut', -'astronomy', -'astute', -'atlantic', -'atlas', -'atom', -'atonable', -'atop', -'atrium', -'atrocious', -'atrophy', -'attach', -'attain', -'attempt', -'attendant', -'attendee', -'attention', -'attentive', -'attest', -'attic', -'attire', -'attitude', -'attractor', -'attribute', -'atypical', -'auction', -'audacious', -'audacity', -'audible', -'audibly', -'audience', -'audio', -'audition', -'augmented', -'august', -'authentic', -'author', -'autism', -'autistic', -'autograph', -'automaker', -'automated', -'automatic', -'autopilot', -'available', -'avalanche', -'avatar', -'avenge', -'avenging', -'avenue', -'average', -'aversion', -'avert', -'aviation', -'aviator', -'avid', -'avoid', -'await', -'awaken', -'award', -'aware', -'awhile', -'awkward', -'awning', -'awoke', -'awry', -'axis', -'babble', -'babbling', -'babied', -'baboon', -'backache', -'backboard', -'backboned', -'backdrop', -'backed', -'backer', -'backfield', -'backfire', -'backhand', -'backing', -'backlands', -'backlash', -'backless', -'backlight', -'backlit', -'backlog', -'backpack', -'backpedal', -'backrest', -'backroom', -'backshift', -'backside', -'backslid', -'backspace', -'backspin', -'backstab', -'backstage', -'backtalk', -'backtrack', -'backup', -'backward', -'backwash', -'backwater', -'backyard', -'bacon', -'bacteria', -'bacterium', -'badass', -'badge', -'badland', -'badly', -'badness', -'baffle', -'baffling', -'bagel', -'bagful', -'baggage', -'bagged', -'baggie', -'bagginess', -'bagging', -'baggy', -'bagpipe', -'baguette', -'baked', -'bakery', -'bakeshop', -'baking', -'balance', -'balancing', -'balcony', -'balmy', -'balsamic', -'bamboo', -'banana', -'banish', -'banister', -'banjo', -'bankable', -'bankbook', -'banked', -'banker', -'banking', -'banknote', -'bankroll', -'banner', -'bannister', -'banshee', -'banter', -'barbecue', -'barbed', -'barbell', -'barber', -'barcode', -'barge', -'bargraph', -'barista', -'baritone', -'barley', -'barmaid', -'barman', -'barn', -'barometer', -'barrack', -'barracuda', -'barrel', -'barrette', -'barricade', -'barrier', -'barstool', -'bartender', -'barterer', -'bash', -'basically', -'basics', -'basil', -'basin', -'basis', -'basket', -'batboy', -'batch', -'bath', -'baton', -'bats', -'battalion', -'battered', -'battering', -'battery', -'batting', -'battle', -'bauble', -'bazooka', -'blabber', -'bladder', -'blade', -'blah', -'blame', -'blaming', -'blanching', -'blandness', -'blank', -'blaspheme', -'blasphemy', -'blast', -'blatancy', -'blatantly', -'blazer', -'blazing', -'bleach', -'bleak', -'bleep', -'blemish', -'blend', -'bless', -'blighted', -'blimp', -'bling', -'blinked', -'blinker', -'blinking', -'blinks', -'blip', -'blissful', -'blitz', -'blizzard', -'bloated', -'bloating', -'blob', -'blog', -'bloomers', -'blooming', -'blooper', -'blot', -'blouse', -'blubber', -'bluff', -'bluish', -'blunderer', -'blunt', -'blurb', -'blurred', -'blurry', -'blurt', -'blush', -'blustery', -'boaster', -'boastful', -'boasting', -'boat', -'bobbed', -'bobbing', -'bobble', -'bobcat', -'bobsled', -'bobtail', -'bodacious', -'body', -'bogged', -'boggle', -'bogus', -'boil', -'bok', -'bolster', -'bolt', -'bonanza', -'bonded', -'bonding', -'bondless', -'boned', -'bonehead', -'boneless', -'bonelike', -'boney', -'bonfire', -'bonnet', -'bonsai', -'bonus', -'bony', -'boogeyman', -'boogieman', -'book', -'boondocks', -'booted', -'booth', -'bootie', -'booting', -'bootlace', -'bootleg', -'boots', -'boozy', -'borax', -'boring', -'borough', -'borrower', -'borrowing', -'boss', -'botanical', -'botanist', -'botany', -'botch', -'both', -'bottle', -'bottling', -'bottom', -'bounce', -'bouncing', -'bouncy', -'bounding', -'boundless', -'bountiful', -'bovine', -'boxcar', -'boxer', -'boxing', -'boxlike', -'boxy', -'breach', -'breath', -'breeches', -'breeching', -'breeder', -'breeding', -'breeze', -'breezy', -'brethren', -'brewery', -'brewing', -'briar', -'bribe', -'brick', -'bride', -'bridged', -'brigade', -'bright', -'brilliant', -'brim', -'bring', -'brink', -'brisket', -'briskly', -'briskness', -'bristle', -'brittle', -'broadband', -'broadcast', -'broaden', -'broadly', -'broadness', -'broadside', -'broadways', -'broiler', -'broiling', -'broken', -'broker', -'bronchial', -'bronco', -'bronze', -'bronzing', -'brook', -'broom', -'brought', -'browbeat', -'brownnose', -'browse', -'browsing', -'bruising', -'brunch', -'brunette', -'brunt', -'brush', -'brussels', -'brute', -'brutishly', -'bubble', -'bubbling', -'bubbly', -'buccaneer', -'bucked', -'bucket', -'buckle', -'buckshot', -'buckskin', -'bucktooth', -'buckwheat', -'buddhism', -'buddhist', -'budding', -'buddy', -'budget', -'buffalo', -'buffed', -'buffer', -'buffing', -'buffoon', -'buggy', -'bulb', -'bulge', -'bulginess', -'bulgur', -'bulk', -'bulldog', -'bulldozer', -'bullfight', -'bullfrog', -'bullhorn', -'bullion', -'bullish', -'bullpen', -'bullring', -'bullseye', -'bullwhip', -'bully', -'bunch', -'bundle', -'bungee', -'bunion', -'bunkbed', -'bunkhouse', -'bunkmate', -'bunny', -'bunt', -'busboy', -'bush', -'busily', -'busload', -'bust', -'busybody', -'buzz', -'cabana', -'cabbage', -'cabbie', -'cabdriver', -'cable', -'caboose', -'cache', -'cackle', -'cacti', -'cactus', -'caddie', -'caddy', -'cadet', -'cadillac', -'cadmium', -'cage', -'cahoots', -'cake', -'calamari', -'calamity', -'calcium', -'calculate', -'calculus', -'caliber', -'calibrate', -'calm', -'caloric', -'calorie', -'calzone', -'camcorder', -'cameo', -'camera', -'camisole', -'camper', -'campfire', -'camping', -'campsite', -'campus', -'canal', -'canary', -'cancel', -'candied', -'candle', -'candy', -'cane', -'canine', -'canister', -'cannabis', -'canned', -'canning', -'cannon', -'cannot', -'canola', -'canon', -'canopener', -'canopy', -'canteen', -'canyon', -'capable', -'capably', -'capacity', -'cape', -'capillary', -'capital', -'capitol', -'capped', -'capricorn', -'capsize', -'capsule', -'caption', -'captivate', -'captive', -'captivity', -'capture', -'caramel', -'carat', -'caravan', -'carbon', -'cardboard', -'carded', -'cardiac', -'cardigan', -'cardinal', -'cardstock', -'carefully', -'caregiver', -'careless', -'caress', -'caretaker', -'cargo', -'caring', -'carless', -'carload', -'carmaker', -'carnage', -'carnation', -'carnival', -'carnivore', -'carol', -'carpenter', -'carpentry', -'carpool', -'carport', -'carried', -'carrot', -'carrousel', -'carry', -'cartel', -'cartload', -'carton', -'cartoon', -'cartridge', -'cartwheel', -'carve', -'carving', -'carwash', -'cascade', -'case', -'cash', -'casing', -'casino', -'casket', -'cassette', -'casually', -'casualty', -'catacomb', -'catalog', -'catalyst', -'catalyze', -'catapult', -'cataract', -'catatonic', -'catcall', -'catchable', -'catcher', -'catching', -'catchy', -'caterer', -'catering', -'catfight', -'catfish', -'cathedral', -'cathouse', -'catlike', -'catnap', -'catnip', -'catsup', -'cattail', -'cattishly', -'cattle', -'catty', -'catwalk', -'caucasian', -'caucus', -'causal', -'causation', -'cause', -'causing', -'cauterize', -'caution', -'cautious', -'cavalier', -'cavalry', -'caviar', -'cavity', -'cedar', -'celery', -'celestial', -'celibacy', -'celibate', -'celtic', -'cement', -'census', -'ceramics', -'ceremony', -'certainly', -'certainty', -'certified', -'certify', -'cesarean', -'cesspool', -'chafe', -'chaffing', -'chain', -'chair', -'chalice', -'challenge', -'chamber', -'chamomile', -'champion', -'chance', -'change', -'channel', -'chant', -'chaos', -'chaperone', -'chaplain', -'chapped', -'chaps', -'chapter', -'character', -'charbroil', -'charcoal', -'charger', -'charging', -'chariot', -'charity', -'charm', -'charred', -'charter', -'charting', -'chase', -'chasing', -'chaste', -'chastise', -'chastity', -'chatroom', -'chatter', -'chatting', -'chatty', -'cheating', -'cheddar', -'cheek', -'cheer', -'cheese', -'cheesy', -'chef', -'chemicals', -'chemist', -'chemo', -'cherisher', -'cherub', -'chess', -'chest', -'chevron', -'chevy', -'chewable', -'chewer', -'chewing', -'chewy', -'chief', -'chihuahua', -'childcare', -'childhood', -'childish', -'childless', -'childlike', -'chili', -'chill', -'chimp', -'chip', -'chirping', -'chirpy', -'chitchat', -'chivalry', -'chive', -'chloride', -'chlorine', -'choice', -'chokehold', -'choking', -'chomp', -'chooser', -'choosing', -'choosy', -'chop', -'chosen', -'chowder', -'chowtime', -'chrome', -'chubby', -'chuck', -'chug', -'chummy', -'chump', -'chunk', -'churn', -'chute', -'cider', -'cilantro', -'cinch', -'cinema', -'cinnamon', -'circle', -'circling', -'circular', -'circulate', -'circus', -'citable', -'citadel', -'citation', -'citizen', -'citric', -'citrus', -'city', -'civic', -'civil', -'clad', -'claim', -'clambake', -'clammy', -'clamor', -'clamp', -'clamshell', -'clang', -'clanking', -'clapped', -'clapper', -'clapping', -'clarify', -'clarinet', -'clarity', -'clash', -'clasp', -'class', -'clatter', -'clause', -'clavicle', -'claw', -'clay', -'clean', -'clear', -'cleat', -'cleaver', -'cleft', -'clench', -'clergyman', -'clerical', -'clerk', -'clever', -'clicker', -'client', -'climate', -'climatic', -'cling', -'clinic', -'clinking', -'clip', -'clique', -'cloak', -'clobber', -'clock', -'clone', -'cloning', -'closable', -'closure', -'clothes', -'clothing', -'cloud', -'clover', -'clubbed', -'clubbing', -'clubhouse', -'clump', -'clumsily', -'clumsy', -'clunky', -'clustered', -'clutch', -'clutter', -'coach', -'coagulant', -'coastal', -'coaster', -'coasting', -'coastland', -'coastline', -'coat', -'coauthor', -'cobalt', -'cobbler', -'cobweb', -'cocoa', -'coconut', -'cod', -'coeditor', -'coerce', -'coexist', -'coffee', -'cofounder', -'cognition', -'cognitive', -'cogwheel', -'coherence', -'coherent', -'cohesive', -'coil', -'coke', -'cola', -'cold', -'coleslaw', -'coliseum', -'collage', -'collapse', -'collar', -'collected', -'collector', -'collide', -'collie', -'collision', -'colonial', -'colonist', -'colonize', -'colony', -'colossal', -'colt', -'coma', -'come', -'comfort', -'comfy', -'comic', -'coming', -'comma', -'commence', -'commend', -'comment', -'commerce', -'commode', -'commodity', -'commodore', -'common', -'commotion', -'commute', -'commuting', -'compacted', -'compacter', -'compactly', -'compactor', -'companion', -'company', -'compare', -'compel', -'compile', -'comply', -'component', -'composed', -'composer', -'composite', -'compost', -'composure', -'compound', -'compress', -'comprised', -'computer', -'computing', -'comrade', -'concave', -'conceal', -'conceded', -'concept', -'concerned', -'concert', -'conch', -'concierge', -'concise', -'conclude', -'concrete', -'concur', -'condense', -'condiment', -'condition', -'condone', -'conducive', -'conductor', -'conduit', -'cone', -'confess', -'confetti', -'confidant', -'confident', -'confider', -'confiding', -'configure', -'confined', -'confining', -'confirm', -'conflict', -'conform', -'confound', -'confront', -'confused', -'confusing', -'confusion', -'congenial', -'congested', -'congrats', -'congress', -'conical', -'conjoined', -'conjure', -'conjuror', -'connected', -'connector', -'consensus', -'consent', -'console', -'consoling', -'consonant', -'constable', -'constant', -'constrain', -'constrict', -'construct', -'consult', -'consumer', -'consuming', -'contact', -'container', -'contempt', -'contend', -'contented', -'contently', -'contents', -'contest', -'context', -'contort', -'contour', -'contrite', -'control', -'contusion', -'convene', -'convent', -'copartner', -'cope', -'copied', -'copier', -'copilot', -'coping', -'copious', -'copper', -'copy', -'coral', -'cork', -'cornball', -'cornbread', -'corncob', -'cornea', -'corned', -'corner', -'cornfield', -'cornflake', -'cornhusk', -'cornmeal', -'cornstalk', -'corny', -'coronary', -'coroner', -'corporal', -'corporate', -'corral', -'correct', -'corridor', -'corrode', -'corroding', -'corrosive', -'corsage', -'corset', -'cortex', -'cosigner', -'cosmetics', -'cosmic', -'cosmos', -'cosponsor', -'cost', -'cottage', -'cotton', -'couch', -'cough', -'could', -'countable', -'countdown', -'counting', -'countless', -'country', -'county', -'courier', -'covenant', -'cover', -'coveted', -'coveting', -'coyness', -'cozily', -'coziness', -'cozy', -'crabbing', -'crabgrass', -'crablike', -'crabmeat', -'cradle', -'cradling', -'crafter', -'craftily', -'craftsman', -'craftwork', -'crafty', -'cramp', -'cranberry', -'crane', -'cranial', -'cranium', -'crank', -'crate', -'crave', -'craving', -'crawfish', -'crawlers', -'crawling', -'crayfish', -'crayon', -'crazed', -'crazily', -'craziness', -'crazy', -'creamed', -'creamer', -'creamlike', -'crease', -'creasing', -'creatable', -'create', -'creation', -'creative', -'creature', -'credible', -'credibly', -'credit', -'creed', -'creme', -'creole', -'crepe', -'crept', -'crescent', -'crested', -'cresting', -'crestless', -'crevice', -'crewless', -'crewman', -'crewmate', -'crib', -'cricket', -'cried', -'crier', -'crimp', -'crimson', -'cringe', -'cringing', -'crinkle', -'crinkly', -'crisped', -'crisping', -'crisply', -'crispness', -'crispy', -'criteria', -'critter', -'croak', -'crock', -'crook', -'croon', -'crop', -'cross', -'crouch', -'crouton', -'crowbar', -'crowd', -'crown', -'crucial', -'crudely', -'crudeness', -'cruelly', -'cruelness', -'cruelty', -'crumb', -'crummiest', -'crummy', -'crumpet', -'crumpled', -'cruncher', -'crunching', -'crunchy', -'crusader', -'crushable', -'crushed', -'crusher', -'crushing', -'crust', -'crux', -'crying', -'cryptic', -'crystal', -'cubbyhole', -'cube', -'cubical', -'cubicle', -'cucumber', -'cuddle', -'cuddly', -'cufflink', -'culinary', -'culminate', -'culpable', -'culprit', -'cultivate', -'cultural', -'culture', -'cupbearer', -'cupcake', -'cupid', -'cupped', -'cupping', -'curable', -'curator', -'curdle', -'cure', -'curfew', -'curing', -'curled', -'curler', -'curliness', -'curling', -'curly', -'curry', -'curse', -'cursive', -'cursor', -'curtain', -'curtly', -'curtsy', -'curvature', -'curve', -'curvy', -'cushy', -'cusp', -'cussed', -'custard', -'custodian', -'custody', -'customary', -'customer', -'customize', -'customs', -'cut', -'cycle', -'cyclic', -'cycling', -'cyclist', -'cylinder', -'cymbal', -'cytoplasm', -'cytoplast', -'dab', -'dad', -'daffodil', -'dagger', -'daily', -'daintily', -'dainty', -'dairy', -'daisy', -'dallying', -'dance', -'dancing', -'dandelion', -'dander', -'dandruff', -'dandy', -'danger', -'dangle', -'dangling', -'daredevil', -'dares', -'daringly', -'darkened', -'darkening', -'darkish', -'darkness', -'darkroom', -'darling', -'darn', -'dart', -'darwinism', -'dash', -'dastardly', -'data', -'datebook', -'dating', -'daughter', -'daunting', -'dawdler', -'dawn', -'daybed', -'daybreak', -'daycare', -'daydream', -'daylight', -'daylong', -'dayroom', -'daytime', -'dazzler', -'dazzling', -'deacon', -'deafening', -'deafness', -'dealer', -'dealing', -'dealmaker', -'dealt', -'dean', -'debatable', -'debate', -'debating', -'debit', -'debrief', -'debtless', -'debtor', -'debug', -'debunk', -'decade', -'decaf', -'decal', -'decathlon', -'decay', -'deceased', -'deceit', -'deceiver', -'deceiving', -'december', -'decency', -'decent', -'deception', -'deceptive', -'decibel', -'decidable', -'decimal', -'decimeter', -'decipher', -'deck', -'declared', -'decline', -'decode', -'decompose', -'decorated', -'decorator', -'decoy', -'decrease', -'decree', -'dedicate', -'dedicator', -'deduce', -'deduct', -'deed', -'deem', -'deepen', -'deeply', -'deepness', -'deface', -'defacing', -'defame', -'default', -'defeat', -'defection', -'defective', -'defendant', -'defender', -'defense', -'defensive', -'deferral', -'deferred', -'defiance', -'defiant', -'defile', -'defiling', -'define', -'definite', -'deflate', -'deflation', -'deflator', -'deflected', -'deflector', -'defog', -'deforest', -'defraud', -'defrost', -'deftly', -'defuse', -'defy', -'degraded', -'degrading', -'degrease', -'degree', -'dehydrate', -'deity', -'dejected', -'delay', -'delegate', -'delegator', -'delete', -'deletion', -'delicacy', -'delicate', -'delicious', -'delighted', -'delirious', -'delirium', -'deliverer', -'delivery', -'delouse', -'delta', -'deluge', -'delusion', -'deluxe', -'demanding', -'demeaning', -'demeanor', -'demise', -'democracy', -'democrat', -'demote', -'demotion', -'demystify', -'denatured', -'deniable', -'denial', -'denim', -'denote', -'dense', -'density', -'dental', -'dentist', -'denture', -'deny', -'deodorant', -'deodorize', -'departed', -'departure', -'depict', -'deplete', -'depletion', -'deplored', -'deploy', -'deport', -'depose', -'depraved', -'depravity', -'deprecate', -'depress', -'deprive', -'depth', -'deputize', -'deputy', -'derail', -'deranged', -'derby', -'derived', -'desecrate', -'deserve', -'deserving', -'designate', -'designed', -'designer', -'designing', -'deskbound', -'desktop', -'deskwork', -'desolate', -'despair', -'despise', -'despite', -'destiny', -'destitute', -'destruct', -'detached', -'detail', -'detection', -'detective', -'detector', -'detention', -'detergent', -'detest', -'detonate', -'detonator', -'detoxify', -'detract', -'deuce', -'devalue', -'deviancy', -'deviant', -'deviate', -'deviation', -'deviator', -'device', -'devious', -'devotedly', -'devotee', -'devotion', -'devourer', -'devouring', -'devoutly', -'dexterity', -'dexterous', -'diabetes', -'diabetic', -'diabolic', -'diagnoses', -'diagnosis', -'diagram', -'dial', -'diameter', -'diaper', -'diaphragm', -'diary', -'dice', -'dicing', -'dictate', -'dictation', -'dictator', -'difficult', -'diffused', -'diffuser', -'diffusion', -'diffusive', -'dig', -'dilation', -'diligence', -'diligent', -'dill', -'dilute', -'dime', -'diminish', -'dimly', -'dimmed', -'dimmer', -'dimness', -'dimple', -'diner', -'dingbat', -'dinghy', -'dinginess', -'dingo', -'dingy', -'dining', -'dinner', -'diocese', -'dioxide', -'diploma', -'dipped', -'dipper', -'dipping', -'directed', -'direction', -'directive', -'directly', -'directory', -'direness', -'dirtiness', -'disabled', -'disagree', -'disallow', -'disarm', -'disarray', -'disaster', -'disband', -'disbelief', -'disburse', -'discard', -'discern', -'discharge', -'disclose', -'discolor', -'discount', -'discourse', -'discover', -'discuss', -'disdain', -'disengage', -'disfigure', -'disgrace', -'dish', -'disinfect', -'disjoin', -'disk', -'dislike', -'disliking', -'dislocate', -'dislodge', -'disloyal', -'dismantle', -'dismay', -'dismiss', -'dismount', -'disobey', -'disorder', -'disown', -'disparate', -'disparity', -'dispatch', -'dispense', -'dispersal', -'dispersed', -'disperser', -'displace', -'display', -'displease', -'disposal', -'dispose', -'disprove', -'dispute', -'disregard', -'disrupt', -'dissuade', -'distance', -'distant', -'distaste', -'distill', -'distinct', -'distort', -'distract', -'distress', -'district', -'distrust', -'ditch', -'ditto', -'ditzy', -'dividable', -'divided', -'dividend', -'dividers', -'dividing', -'divinely', -'diving', -'divinity', -'divisible', -'divisibly', -'division', -'divisive', -'divorcee', -'dizziness', -'dizzy', -'doable', -'docile', -'dock', -'doctrine', -'document', -'dodge', -'dodgy', -'doily', -'doing', -'dole', -'dollar', -'dollhouse', -'dollop', -'dolly', -'dolphin', -'domain', -'domelike', -'domestic', -'dominion', -'dominoes', -'donated', -'donation', -'donator', -'donor', -'donut', -'doodle', -'doorbell', -'doorframe', -'doorknob', -'doorman', -'doormat', -'doornail', -'doorpost', -'doorstep', -'doorstop', -'doorway', -'doozy', -'dork', -'dormitory', -'dorsal', -'dosage', -'dose', -'dotted', -'doubling', -'douche', -'dove', -'down', -'dowry', -'doze', -'drab', -'dragging', -'dragonfly', -'dragonish', -'dragster', -'drainable', -'drainage', -'drained', -'drainer', -'drainpipe', -'dramatic', -'dramatize', -'drank', -'drapery', -'drastic', -'draw', -'dreaded', -'dreadful', -'dreadlock', -'dreamboat', -'dreamily', -'dreamland', -'dreamless', -'dreamlike', -'dreamt', -'dreamy', -'drearily', -'dreary', -'drench', -'dress', -'drew', -'dribble', -'dried', -'drier', -'drift', -'driller', -'drilling', -'drinkable', -'drinking', -'dripping', -'drippy', -'drivable', -'driven', -'driver', -'driveway', -'driving', -'drizzle', -'drizzly', -'drone', -'drool', -'droop', -'drop-down', -'dropbox', -'dropkick', -'droplet', -'dropout', -'dropper', -'drove', -'drown', -'drowsily', -'drudge', -'drum', -'dry', -'dubbed', -'dubiously', -'duchess', -'duckbill', -'ducking', -'duckling', -'ducktail', -'ducky', -'duct', -'dude', -'duffel', -'dugout', -'duh', -'duke', -'duller', -'dullness', -'duly', -'dumping', -'dumpling', -'dumpster', -'duo', -'dupe', -'duplex', -'duplicate', -'duplicity', -'durable', -'durably', -'duration', -'duress', -'during', -'dusk', -'dust', -'dutiful', -'duty', -'duvet', -'dwarf', -'dweeb', -'dwelled', -'dweller', -'dwelling', -'dwindle', -'dwindling', -'dynamic', -'dynamite', -'dynasty', -'dyslexia', -'dyslexic', -'each', -'eagle', -'earache', -'eardrum', -'earflap', -'earful', -'earlobe', -'early', -'earmark', -'earmuff', -'earphone', -'earpiece', -'earplugs', -'earring', -'earshot', -'earthen', -'earthlike', -'earthling', -'earthly', -'earthworm', -'earthy', -'earwig', -'easeful', -'easel', -'easiest', -'easily', -'easiness', -'easing', -'eastbound', -'eastcoast', -'easter', -'eastward', -'eatable', -'eaten', -'eatery', -'eating', -'eats', -'ebay', -'ebony', -'ebook', -'ecard', -'eccentric', -'echo', -'eclair', -'eclipse', -'ecologist', -'ecology', -'economic', -'economist', -'economy', -'ecosphere', -'ecosystem', -'edge', -'edginess', -'edging', -'edgy', -'edition', -'editor', -'educated', -'education', -'educator', -'eel', -'effective', -'effects', -'efficient', -'effort', -'eggbeater', -'egging', -'eggnog', -'eggplant', -'eggshell', -'egomaniac', -'egotism', -'egotistic', -'either', -'eject', -'elaborate', -'elastic', -'elated', -'elbow', -'eldercare', -'elderly', -'eldest', -'electable', -'election', -'elective', -'elephant', -'elevate', -'elevating', -'elevation', -'elevator', -'eleven', -'elf', -'eligible', -'eligibly', -'eliminate', -'elite', -'elitism', -'elixir', -'elk', -'ellipse', -'elliptic', -'elm', -'elongated', -'elope', -'eloquence', -'eloquent', -'elsewhere', -'elude', -'elusive', -'elves', -'email', -'embargo', -'embark', -'embassy', -'embattled', -'embellish', -'ember', -'embezzle', -'emblaze', -'emblem', -'embody', -'embolism', -'emboss', -'embroider', -'emcee', -'emerald', -'emergency', -'emission', -'emit', -'emote', -'emoticon', -'emotion', -'empathic', -'empathy', -'emperor', -'emphases', -'emphasis', -'emphasize', -'emphatic', -'empirical', -'employed', -'employee', -'employer', -'emporium', -'empower', -'emptier', -'emptiness', -'empty', -'emu', -'enable', -'enactment', -'enamel', -'enchanted', -'enchilada', -'encircle', -'enclose', -'enclosure', -'encode', -'encore', -'encounter', -'encourage', -'encroach', -'encrust', -'encrypt', -'endanger', -'endeared', -'endearing', -'ended', -'ending', -'endless', -'endnote', -'endocrine', -'endorphin', -'endorse', -'endowment', -'endpoint', -'endurable', -'endurance', -'enduring', -'energetic', -'energize', -'energy', -'enforced', -'enforcer', -'engaged', -'engaging', -'engine', -'engorge', -'engraved', -'engraver', -'engraving', -'engross', -'engulf', -'enhance', -'enigmatic', -'enjoyable', -'enjoyably', -'enjoyer', -'enjoying', -'enjoyment', -'enlarged', -'enlarging', -'enlighten', -'enlisted', -'enquirer', -'enrage', -'enrich', -'enroll', -'enslave', -'ensnare', -'ensure', -'entail', -'entangled', -'entering', -'entertain', -'enticing', -'entire', -'entitle', -'entity', -'entomb', -'entourage', -'entrap', -'entree', -'entrench', -'entrust', -'entryway', -'entwine', -'enunciate', -'envelope', -'enviable', -'enviably', -'envious', -'envision', -'envoy', -'envy', -'enzyme', -'epic', -'epidemic', -'epidermal', -'epidermis', -'epidural', -'epilepsy', -'epileptic', -'epilogue', -'epiphany', -'episode', -'equal', -'equate', -'equation', -'equator', -'equinox', -'equipment', -'equity', -'equivocal', -'eradicate', -'erasable', -'erased', -'eraser', -'erasure', -'ergonomic', -'errand', -'errant', -'erratic', -'error', -'erupt', -'escalate', -'escalator', -'escapable', -'escapade', -'escapist', -'escargot', -'eskimo', -'esophagus', -'espionage', -'espresso', -'esquire', -'essay', -'essence', -'essential', -'establish', -'estate', -'esteemed', -'estimate', -'estimator', -'estranged', -'estrogen', -'etching', -'eternal', -'eternity', -'ethanol', -'ether', -'ethically', -'ethics', -'euphemism', -'evacuate', -'evacuee', -'evade', -'evaluate', -'evaluator', -'evaporate', -'evasion', -'evasive', -'even', -'everglade', -'evergreen', -'everybody', -'everyday', -'everyone', -'evict', -'evidence', -'evident', -'evil', -'evoke', -'evolution', -'evolve', -'exact', -'exalted', -'example', -'excavate', -'excavator', -'exceeding', -'exception', -'excess', -'exchange', -'excitable', -'exciting', -'exclaim', -'exclude', -'excluding', -'exclusion', -'exclusive', -'excretion', -'excretory', -'excursion', -'excusable', -'excusably', -'excuse', -'exemplary', -'exemplify', -'exemption', -'exerciser', -'exert', -'exes', -'exfoliate', -'exhale', -'exhaust', -'exhume', -'exile', -'existing', -'exit', -'exodus', -'exonerate', -'exorcism', -'exorcist', -'expand', -'expanse', -'expansion', -'expansive', -'expectant', -'expedited', -'expediter', -'expel', -'expend', -'expenses', -'expensive', -'expert', -'expire', -'expiring', -'explain', -'expletive', -'explicit', -'explode', -'exploit', -'explore', -'exploring', -'exponent', -'exporter', -'exposable', -'expose', -'exposure', -'express', -'expulsion', -'exquisite', -'extended', -'extending', -'extent', -'extenuate', -'exterior', -'external', -'extinct', -'extortion', -'extradite', -'extras', -'extrovert', -'extrude', -'extruding', -'exuberant', -'fable', -'fabric', -'fabulous', -'facebook', -'facecloth', -'facedown', -'faceless', -'facelift', -'faceplate', -'faceted', -'facial', -'facility', -'facing', -'facsimile', -'faction', -'factoid', -'factor', -'factsheet', -'factual', -'faculty', -'fade', -'fading', -'failing', -'falcon', -'fall', -'false', -'falsify', -'fame', -'familiar', -'family', -'famine', -'famished', -'fanatic', -'fancied', -'fanciness', -'fancy', -'fanfare', -'fang', -'fanning', -'fantasize', -'fantastic', -'fantasy', -'fascism', -'fastball', -'faster', -'fasting', -'fastness', -'faucet', -'favorable', -'favorably', -'favored', -'favoring', -'favorite', -'fax', -'feast', -'federal', -'fedora', -'feeble', -'feed', -'feel', -'feisty', -'feline', -'felt-tip', -'feminine', -'feminism', -'feminist', -'feminize', -'femur', -'fence', -'fencing', -'fender', -'ferment', -'fernlike', -'ferocious', -'ferocity', -'ferret', -'ferris', -'ferry', -'fervor', -'fester', -'festival', -'festive', -'festivity', -'fetal', -'fetch', -'fever', -'fiber', -'fiction', -'fiddle', -'fiddling', -'fidelity', -'fidgeting', -'fidgety', -'fifteen', -'fifth', -'fiftieth', -'fifty', -'figment', -'figure', -'figurine', -'filing', -'filled', -'filler', -'filling', -'film', -'filter', -'filth', -'filtrate', -'finale', -'finalist', -'finalize', -'finally', -'finance', -'financial', -'finch', -'fineness', -'finer', -'finicky', -'finished', -'finisher', -'finishing', -'finite', -'finless', -'finlike', -'fiscally', -'fit', -'five', -'flaccid', -'flagman', -'flagpole', -'flagship', -'flagstick', -'flagstone', -'flail', -'flakily', -'flaky', -'flame', -'flammable', -'flanked', -'flanking', -'flannels', -'flap', -'flaring', -'flashback', -'flashbulb', -'flashcard', -'flashily', -'flashing', -'flashy', -'flask', -'flatbed', -'flatfoot', -'flatly', -'flatness', -'flatten', -'flattered', -'flatterer', -'flattery', -'flattop', -'flatware', -'flatworm', -'flavored', -'flavorful', -'flavoring', -'flaxseed', -'fled', -'fleshed', -'fleshy', -'flick', -'flier', -'flight', -'flinch', -'fling', -'flint', -'flip', -'flirt', -'float', -'flock', -'flogging', -'flop', -'floral', -'florist', -'floss', -'flounder', -'flyable', -'flyaway', -'flyer', -'flying', -'flyover', -'flypaper', -'foam', -'foe', -'fog', -'foil', -'folic', -'folk', -'follicle', -'follow', -'fondling', -'fondly', -'fondness', -'fondue', -'font', -'food', -'fool', -'footage', -'football', -'footbath', -'footboard', -'footer', -'footgear', -'foothill', -'foothold', -'footing', -'footless', -'footman', -'footnote', -'footpad', -'footpath', -'footprint', -'footrest', -'footsie', -'footsore', -'footwear', -'footwork', -'fossil', -'foster', -'founder', -'founding', -'fountain', -'fox', -'foyer', -'fraction', -'fracture', -'fragile', -'fragility', -'fragment', -'fragrance', -'fragrant', -'frail', -'frame', -'framing', -'frantic', -'fraternal', -'frayed', -'fraying', -'frays', -'freckled', -'freckles', -'freebase', -'freebee', -'freebie', -'freedom', -'freefall', -'freehand', -'freeing', -'freeload', -'freely', -'freemason', -'freeness', -'freestyle', -'freeware', -'freeway', -'freewill', -'freezable', -'freezing', -'freight', -'french', -'frenzied', -'frenzy', -'frequency', -'frequent', -'fresh', -'fretful', -'fretted', -'friction', -'friday', -'fridge', -'fried', -'friend', -'frighten', -'frightful', -'frigidity', -'frigidly', -'frill', -'fringe', -'frisbee', -'frisk', -'fritter', -'frivolous', -'frolic', -'from', -'front', -'frostbite', -'frosted', -'frostily', -'frosting', -'frostlike', -'frosty', -'froth', -'frown', -'frozen', -'fructose', -'frugality', -'frugally', -'fruit', -'frustrate', -'frying', -'gab', -'gaffe', -'gag', -'gainfully', -'gaining', -'gains', -'gala', -'gallantly', -'galleria', -'gallery', -'galley', -'gallon', -'gallows', -'gallstone', -'galore', -'galvanize', -'gambling', -'game', -'gaming', -'gamma', -'gander', -'gangly', -'gangrene', -'gangway', -'gap', -'garage', -'garbage', -'garden', -'gargle', -'garland', -'garlic', -'garment', -'garnet', -'garnish', -'garter', -'gas', -'gatherer', -'gathering', -'gating', -'gauging', -'gauntlet', -'gauze', -'gave', -'gawk', -'gazing', -'gear', -'gecko', -'geek', -'geiger', -'gem', -'gender', -'generic', -'generous', -'genetics', -'genre', -'gentile', -'gentleman', -'gently', -'gents', -'geography', -'geologic', -'geologist', -'geology', -'geometric', -'geometry', -'geranium', -'gerbil', -'geriatric', -'germicide', -'germinate', -'germless', -'germproof', -'gestate', -'gestation', -'gesture', -'getaway', -'getting', -'getup', -'giant', -'gibberish', -'giblet', -'giddily', -'giddiness', -'giddy', -'gift', -'gigabyte', -'gigahertz', -'gigantic', -'giggle', -'giggling', -'giggly', -'gigolo', -'gilled', -'gills', -'gimmick', -'girdle', -'giveaway', -'given', -'giver', -'giving', -'gizmo', -'gizzard', -'glacial', -'glacier', -'glade', -'gladiator', -'gladly', -'glamorous', -'glamour', -'glance', -'glancing', -'glandular', -'glare', -'glaring', -'glass', -'glaucoma', -'glazing', -'gleaming', -'gleeful', -'glider', -'gliding', -'glimmer', -'glimpse', -'glisten', -'glitch', -'glitter', -'glitzy', -'gloater', -'gloating', -'gloomily', -'gloomy', -'glorified', -'glorifier', -'glorify', -'glorious', -'glory', -'gloss', -'glove', -'glowing', -'glowworm', -'glucose', -'glue', -'gluten', -'glutinous', -'glutton', -'gnarly', -'gnat', -'goal', -'goatskin', -'goes', -'goggles', -'going', -'goldfish', -'goldmine', -'goldsmith', -'golf', -'goliath', -'gonad', -'gondola', -'gone', -'gong', -'good', -'gooey', -'goofball', -'goofiness', -'goofy', -'google', -'goon', -'gopher', -'gore', -'gorged', -'gorgeous', -'gory', -'gosling', -'gossip', -'gothic', -'gotten', -'gout', -'gown', -'grab', -'graceful', -'graceless', -'gracious', -'gradation', -'graded', -'grader', -'gradient', -'grading', -'gradually', -'graduate', -'graffiti', -'grafted', -'grafting', -'grain', -'granddad', -'grandkid', -'grandly', -'grandma', -'grandpa', -'grandson', -'granite', -'granny', -'granola', -'grant', -'granular', -'grape', -'graph', -'grapple', -'grappling', -'grasp', -'grass', -'gratified', -'gratify', -'grating', -'gratitude', -'gratuity', -'gravel', -'graveness', -'graves', -'graveyard', -'gravitate', -'gravity', -'gravy', -'gray', -'grazing', -'greasily', -'greedily', -'greedless', -'greedy', -'green', -'greeter', -'greeting', -'grew', -'greyhound', -'grid', -'grief', -'grievance', -'grieving', -'grievous', -'grill', -'grimace', -'grimacing', -'grime', -'griminess', -'grimy', -'grinch', -'grinning', -'grip', -'gristle', -'grit', -'groggily', -'groggy', -'groin', -'groom', -'groove', -'grooving', -'groovy', -'grope', -'ground', -'grouped', -'grout', -'grove', -'grower', -'growing', -'growl', -'grub', -'grudge', -'grudging', -'grueling', -'gruffly', -'grumble', -'grumbling', -'grumbly', -'grumpily', -'grunge', -'grunt', -'guacamole', -'guidable', -'guidance', -'guide', -'guiding', -'guileless', -'guise', -'gulf', -'gullible', -'gully', -'gulp', -'gumball', -'gumdrop', -'gumminess', -'gumming', -'gummy', -'gurgle', -'gurgling', -'guru', -'gush', -'gusto', -'gusty', -'gutless', -'guts', -'gutter', -'guy', -'guzzler', -'gyration', -'habitable', -'habitant', -'habitat', -'habitual', -'hacked', -'hacker', -'hacking', -'hacksaw', -'had', -'haggler', -'haiku', -'half', -'halogen', -'halt', -'halved', -'halves', -'hamburger', -'hamlet', -'hammock', -'hamper', -'hamster', -'hamstring', -'handbag', -'handball', -'handbook', -'handbrake', -'handcart', -'handclap', -'handclasp', -'handcraft', -'handcuff', -'handed', -'handful', -'handgrip', -'handgun', -'handheld', -'handiness', -'handiwork', -'handlebar', -'handled', -'handler', -'handling', -'handmade', -'handoff', -'handpick', -'handprint', -'handrail', -'handsaw', -'handset', -'handsfree', -'handshake', -'handstand', -'handwash', -'handwork', -'handwoven', -'handwrite', -'handyman', -'hangnail', -'hangout', -'hangover', -'hangup', -'hankering', -'hankie', -'hanky', -'haphazard', -'happening', -'happier', -'happiest', -'happily', -'happiness', -'happy', -'harbor', -'hardcopy', -'hardcore', -'hardcover', -'harddisk', -'hardened', -'hardener', -'hardening', -'hardhat', -'hardhead', -'hardiness', -'hardly', -'hardness', -'hardship', -'hardware', -'hardwired', -'hardwood', -'hardy', -'harmful', -'harmless', -'harmonica', -'harmonics', -'harmonize', -'harmony', -'harness', -'harpist', -'harsh', -'harvest', -'hash', -'hassle', -'haste', -'hastily', -'hastiness', -'hasty', -'hatbox', -'hatchback', -'hatchery', -'hatchet', -'hatching', -'hatchling', -'hate', -'hatless', -'hatred', -'haunt', -'haven', -'hazard', -'hazelnut', -'hazily', -'haziness', -'hazing', -'hazy', -'headache', -'headband', -'headboard', -'headcount', -'headdress', -'headed', -'header', -'headfirst', -'headgear', -'heading', -'headlamp', -'headless', -'headlock', -'headphone', -'headpiece', -'headrest', -'headroom', -'headscarf', -'headset', -'headsman', -'headstand', -'headstone', -'headway', -'headwear', -'heap', -'heat', -'heave', -'heavily', -'heaviness', -'heaving', -'hedge', -'hedging', -'heftiness', -'hefty', -'helium', -'helmet', -'helper', -'helpful', -'helping', -'helpless', -'helpline', -'hemlock', -'hemstitch', -'hence', -'henchman', -'henna', -'herald', -'herbal', -'herbicide', -'herbs', -'heritage', -'hermit', -'heroics', -'heroism', -'herring', -'herself', -'hertz', -'hesitancy', -'hesitant', -'hesitate', -'hexagon', -'hexagram', -'hubcap', -'huddle', -'huddling', -'huff', -'hug', -'hula', -'hulk', -'hull', -'human', -'humble', -'humbling', -'humbly', -'humid', -'humiliate', -'humility', -'humming', -'hummus', -'humongous', -'humorist', -'humorless', -'humorous', -'humpback', -'humped', -'humvee', -'hunchback', -'hundredth', -'hunger', -'hungrily', -'hungry', -'hunk', -'hunter', -'hunting', -'huntress', -'huntsman', -'hurdle', -'hurled', -'hurler', -'hurling', -'hurray', -'hurricane', -'hurried', -'hurry', -'hurt', -'husband', -'hush', -'husked', -'huskiness', -'hut', -'hybrid', -'hydrant', -'hydrated', -'hydration', -'hydrogen', -'hydroxide', -'hyperlink', -'hypertext', -'hyphen', -'hypnoses', -'hypnosis', -'hypnotic', -'hypnotism', -'hypnotist', -'hypnotize', -'hypocrisy', -'hypocrite', -'ibuprofen', -'ice', -'iciness', -'icing', -'icky', -'icon', -'icy', -'idealism', -'idealist', -'idealize', -'ideally', -'idealness', -'identical', -'identify', -'identity', -'ideology', -'idiocy', -'idiom', -'idly', -'igloo', -'ignition', -'ignore', -'iguana', -'illicitly', -'illusion', -'illusive', -'image', -'imaginary', -'imagines', -'imaging', -'imbecile', -'imitate', -'imitation', -'immature', -'immerse', -'immersion', -'imminent', -'immobile', -'immodest', -'immorally', -'immortal', -'immovable', -'immovably', -'immunity', -'immunize', -'impaired', -'impale', -'impart', -'impatient', -'impeach', -'impeding', -'impending', -'imperfect', -'imperial', -'impish', -'implant', -'implement', -'implicate', -'implicit', -'implode', -'implosion', -'implosive', -'imply', -'impolite', -'important', -'importer', -'impose', -'imposing', -'impotence', -'impotency', -'impotent', -'impound', -'imprecise', -'imprint', -'imprison', -'impromptu', -'improper', -'improve', -'improving', -'improvise', -'imprudent', -'impulse', -'impulsive', -'impure', -'impurity', -'iodine', -'iodize', -'ion', -'ipad', -'iphone', -'ipod', -'irate', -'irk', -'iron', -'irregular', -'irrigate', -'irritable', -'irritably', -'irritant', -'irritate', -'islamic', -'islamist', -'isolated', -'isolating', -'isolation', -'isotope', -'issue', -'issuing', -'italicize', -'italics', -'item', -'itinerary', -'itunes', -'ivory', -'ivy', -'jab', -'jackal', -'jacket', -'jackknife', -'jackpot', -'jailbird', -'jailbreak', -'jailer', -'jailhouse', -'jalapeno', -'jam', -'janitor', -'january', -'jargon', -'jarring', -'jasmine', -'jaundice', -'jaunt', -'java', -'jawed', -'jawless', -'jawline', -'jaws', -'jaybird', -'jaywalker', -'jazz', -'jeep', -'jeeringly', -'jellied', -'jelly', -'jersey', -'jester', -'jet', -'jiffy', -'jigsaw', -'jimmy', -'jingle', -'jingling', -'jinx', -'jitters', -'jittery', -'job', -'jockey', -'jockstrap', -'jogger', -'jogging', -'john', -'joining', -'jokester', -'jokingly', -'jolliness', -'jolly', -'jolt', -'jot', -'jovial', -'joyfully', -'joylessly', -'joyous', -'joyride', -'joystick', -'jubilance', -'jubilant', -'judge', -'judgingly', -'judicial', -'judiciary', -'judo', -'juggle', -'juggling', -'jugular', -'juice', -'juiciness', -'juicy', -'jujitsu', -'jukebox', -'july', -'jumble', -'jumbo', -'jump', -'junction', -'juncture', -'june', -'junior', -'juniper', -'junkie', -'junkman', -'junkyard', -'jurist', -'juror', -'jury', -'justice', -'justifier', -'justify', -'justly', -'justness', -'juvenile', -'kabob', -'kangaroo', -'karaoke', -'karate', -'karma', -'kebab', -'keenly', -'keenness', -'keep', -'keg', -'kelp', -'kennel', -'kept', -'kerchief', -'kerosene', -'kettle', -'kick', -'kiln', -'kilobyte', -'kilogram', -'kilometer', -'kilowatt', -'kilt', -'kimono', -'kindle', -'kindling', -'kindly', -'kindness', -'kindred', -'kinetic', -'kinfolk', -'king', -'kinship', -'kinsman', -'kinswoman', -'kissable', -'kisser', -'kissing', -'kitchen', -'kite', -'kitten', -'kitty', -'kiwi', -'kleenex', -'knapsack', -'knee', -'knelt', -'knickers', -'knoll', -'koala', -'kooky', -'kosher', -'krypton', -'kudos', -'kung', -'labored', -'laborer', -'laboring', -'laborious', -'labrador', -'ladder', -'ladies', -'ladle', -'ladybug', -'ladylike', -'lagged', -'lagging', -'lagoon', -'lair', -'lake', -'lance', -'landed', -'landfall', -'landfill', -'landing', -'landlady', -'landless', -'landline', -'landlord', -'landmark', -'landmass', -'landmine', -'landowner', -'landscape', -'landside', -'landslide', -'language', -'lankiness', -'lanky', -'lantern', -'lapdog', -'lapel', -'lapped', -'lapping', -'laptop', -'lard', -'large', -'lark', -'lash', -'lasso', -'last', -'latch', -'late', -'lather', -'latitude', -'latrine', -'latter', -'latticed', -'launch', -'launder', -'laundry', -'laurel', -'lavender', -'lavish', -'laxative', -'lazily', -'laziness', -'lazy', -'lecturer', -'left', -'legacy', -'legal', -'legend', -'legged', -'leggings', -'legible', -'legibly', -'legislate', -'lego', -'legroom', -'legume', -'legwarmer', -'legwork', -'lemon', -'lend', -'length', -'lens', -'lent', -'leotard', -'lesser', -'letdown', -'lethargic', -'lethargy', -'letter', -'lettuce', -'level', -'leverage', -'levers', -'levitate', -'levitator', -'liability', -'liable', -'liberty', -'librarian', -'library', -'licking', -'licorice', -'lid', -'life', -'lifter', -'lifting', -'liftoff', -'ligament', -'likely', -'likeness', -'likewise', -'liking', -'lilac', -'lilly', -'lily', -'limb', -'limeade', -'limelight', -'limes', -'limit', -'limping', -'limpness', -'line', -'lingo', -'linguini', -'linguist', -'lining', -'linked', -'linoleum', -'linseed', -'lint', -'lion', -'lip', -'liquefy', -'liqueur', -'liquid', -'lisp', -'list', -'litigate', -'litigator', -'litmus', -'litter', -'little', -'livable', -'lived', -'lively', -'liver', -'livestock', -'lividly', -'living', -'lizard', -'lubricant', -'lubricate', -'lucid', -'luckily', -'luckiness', -'luckless', -'lucrative', -'ludicrous', -'lugged', -'lukewarm', -'lullaby', -'lumber', -'luminance', -'luminous', -'lumpiness', -'lumping', -'lumpish', -'lunacy', -'lunar', -'lunchbox', -'luncheon', -'lunchroom', -'lunchtime', -'lung', -'lurch', -'lure', -'luridness', -'lurk', -'lushly', -'lushness', -'luster', -'lustfully', -'lustily', -'lustiness', -'lustrous', -'lusty', -'luxurious', -'luxury', -'lying', -'lyrically', -'lyricism', -'lyricist', -'lyrics', -'macarena', -'macaroni', -'macaw', -'mace', -'machine', -'machinist', -'magazine', -'magenta', -'maggot', -'magical', -'magician', -'magma', -'magnesium', -'magnetic', -'magnetism', -'magnetize', -'magnifier', -'magnify', -'magnitude', -'magnolia', -'mahogany', -'maimed', -'majestic', -'majesty', -'majorette', -'majority', -'makeover', -'maker', -'makeshift', -'making', -'malformed', -'malt', -'mama', -'mammal', -'mammary', -'mammogram', -'manager', -'managing', -'manatee', -'mandarin', -'mandate', -'mandatory', -'mandolin', -'manger', -'mangle', -'mango', -'mangy', -'manhandle', -'manhole', -'manhood', -'manhunt', -'manicotti', -'manicure', -'manifesto', -'manila', -'mankind', -'manlike', -'manliness', -'manly', -'manmade', -'manned', -'mannish', -'manor', -'manpower', -'mantis', -'mantra', -'manual', -'many', -'map', -'marathon', -'marauding', -'marbled', -'marbles', -'marbling', -'march', -'mardi', -'margarine', -'margarita', -'margin', -'marigold', -'marina', -'marine', -'marital', -'maritime', -'marlin', -'marmalade', -'maroon', -'married', -'marrow', -'marry', -'marshland', -'marshy', -'marsupial', -'marvelous', -'marxism', -'mascot', -'masculine', -'mashed', -'mashing', -'massager', -'masses', -'massive', -'mastiff', -'matador', -'matchbook', -'matchbox', -'matcher', -'matching', -'matchless', -'material', -'maternal', -'maternity', -'math', -'mating', -'matriarch', -'matrimony', -'matrix', -'matron', -'matted', -'matter', -'maturely', -'maturing', -'maturity', -'mauve', -'maverick', -'maximize', -'maximum', -'maybe', -'mayday', -'mayflower', -'moaner', -'moaning', -'mobile', -'mobility', -'mobilize', -'mobster', -'mocha', -'mocker', -'mockup', -'modified', -'modify', -'modular', -'modulator', -'module', -'moisten', -'moistness', -'moisture', -'molar', -'molasses', -'mold', -'molecular', -'molecule', -'molehill', -'mollusk', -'mom', -'monastery', -'monday', -'monetary', -'monetize', -'moneybags', -'moneyless', -'moneywise', -'mongoose', -'mongrel', -'monitor', -'monkhood', -'monogamy', -'monogram', -'monologue', -'monopoly', -'monorail', -'monotone', -'monotype', -'monoxide', -'monsieur', -'monsoon', -'monstrous', -'monthly', -'monument', -'moocher', -'moodiness', -'moody', -'mooing', -'moonbeam', -'mooned', -'moonlight', -'moonlike', -'moonlit', -'moonrise', -'moonscape', -'moonshine', -'moonstone', -'moonwalk', -'mop', -'morale', -'morality', -'morally', -'morbidity', -'morbidly', -'morphine', -'morphing', -'morse', -'mortality', -'mortally', -'mortician', -'mortified', -'mortify', -'mortuary', -'mosaic', -'mossy', -'most', -'mothball', -'mothproof', -'motion', -'motivate', -'motivator', -'motive', -'motocross', -'motor', -'motto', -'mountable', -'mountain', -'mounted', -'mounting', -'mourner', -'mournful', -'mouse', -'mousiness', -'moustache', -'mousy', -'mouth', -'movable', -'move', -'movie', -'moving', -'mower', -'mowing', -'much', -'muck', -'mud', -'mug', -'mulberry', -'mulch', -'mule', -'mulled', -'mullets', -'multiple', -'multiply', -'multitask', -'multitude', -'mumble', -'mumbling', -'mumbo', -'mummified', -'mummify', -'mummy', -'mumps', -'munchkin', -'mundane', -'municipal', -'muppet', -'mural', -'murkiness', -'murky', -'murmuring', -'muscular', -'museum', -'mushily', -'mushiness', -'mushroom', -'mushy', -'music', -'musket', -'muskiness', -'musky', -'mustang', -'mustard', -'muster', -'mustiness', -'musty', -'mutable', -'mutate', -'mutation', -'mute', -'mutilated', -'mutilator', -'mutiny', -'mutt', -'mutual', -'muzzle', -'myself', -'myspace', -'mystified', -'mystify', -'myth', -'nacho', -'nag', -'nail', -'name', -'naming', -'nanny', -'nanometer', -'nape', -'napkin', -'napped', -'napping', -'nappy', -'narrow', -'nastily', -'nastiness', -'national', -'native', -'nativity', -'natural', -'nature', -'naturist', -'nautical', -'navigate', -'navigator', -'navy', -'nearby', -'nearest', -'nearly', -'nearness', -'neatly', -'neatness', -'nebula', -'nebulizer', -'nectar', -'negate', -'negation', -'negative', -'neglector', -'negligee', -'negligent', -'negotiate', -'nemeses', -'nemesis', -'neon', -'nephew', -'nerd', -'nervous', -'nervy', -'nest', -'net', -'neurology', -'neuron', -'neurosis', -'neurotic', -'neuter', -'neutron', -'never', -'next', -'nibble', -'nickname', -'nicotine', -'niece', -'nifty', -'nimble', -'nimbly', -'nineteen', -'ninetieth', -'ninja', -'nintendo', -'ninth', -'nuclear', -'nuclei', -'nucleus', -'nugget', -'nullify', -'number', -'numbing', -'numbly', -'numbness', -'numeral', -'numerate', -'numerator', -'numeric', -'numerous', -'nuptials', -'nursery', -'nursing', -'nurture', -'nutcase', -'nutlike', -'nutmeg', -'nutrient', -'nutshell', -'nuttiness', -'nutty', -'nuzzle', -'nylon', -'oaf', -'oak', -'oasis', -'oat', -'obedience', -'obedient', -'obituary', -'object', -'obligate', -'obliged', -'oblivion', -'oblivious', -'oblong', -'obnoxious', -'oboe', -'obscure', -'obscurity', -'observant', -'observer', -'observing', -'obsessed', -'obsession', -'obsessive', -'obsolete', -'obstacle', -'obstinate', -'obstruct', -'obtain', -'obtrusive', -'obtuse', -'obvious', -'occultist', -'occupancy', -'occupant', -'occupier', -'occupy', -'ocean', -'ocelot', -'octagon', -'octane', -'october', -'octopus', -'ogle', -'oil', -'oink', -'ointment', -'okay', -'old', -'olive', -'olympics', -'omega', -'omen', -'ominous', -'omission', -'omit', -'omnivore', -'onboard', -'oncoming', -'ongoing', -'onion', -'online', -'onlooker', -'only', -'onscreen', -'onset', -'onshore', -'onslaught', -'onstage', -'onto', -'onward', -'onyx', -'oops', -'ooze', -'oozy', -'opacity', -'opal', -'open', -'operable', -'operate', -'operating', -'operation', -'operative', -'operator', -'opium', -'opossum', -'opponent', -'oppose', -'opposing', -'opposite', -'oppressed', -'oppressor', -'opt', -'opulently', -'osmosis', -'other', -'otter', -'ouch', -'ought', -'ounce', -'outage', -'outback', -'outbid', -'outboard', -'outbound', -'outbreak', -'outburst', -'outcast', -'outclass', -'outcome', -'outdated', -'outdoors', -'outer', -'outfield', -'outfit', -'outflank', -'outgoing', -'outgrow', -'outhouse', -'outing', -'outlast', -'outlet', -'outline', -'outlook', -'outlying', -'outmatch', -'outmost', -'outnumber', -'outplayed', -'outpost', -'outpour', -'output', -'outrage', -'outrank', -'outreach', -'outright', -'outscore', -'outsell', -'outshine', -'outshoot', -'outsider', -'outskirts', -'outsmart', -'outsource', -'outspoken', -'outtakes', -'outthink', -'outward', -'outweigh', -'outwit', -'oval', -'ovary', -'oven', -'overact', -'overall', -'overarch', -'overbid', -'overbill', -'overbite', -'overblown', -'overboard', -'overbook', -'overbuilt', -'overcast', -'overcoat', -'overcome', -'overcook', -'overcrowd', -'overdraft', -'overdrawn', -'overdress', -'overdrive', -'overdue', -'overeager', -'overeater', -'overexert', -'overfed', -'overfeed', -'overfill', -'overflow', -'overfull', -'overgrown', -'overhand', -'overhang', -'overhaul', -'overhead', -'overhear', -'overheat', -'overhung', -'overjoyed', -'overkill', -'overlabor', -'overlaid', -'overlap', -'overlay', -'overload', -'overlook', -'overlord', -'overlying', -'overnight', -'overpass', -'overpay', -'overplant', -'overplay', -'overpower', -'overprice', -'overrate', -'overreach', -'overreact', -'override', -'overripe', -'overrule', -'overrun', -'overshoot', -'overshot', -'oversight', -'oversized', -'oversleep', -'oversold', -'overspend', -'overstate', -'overstay', -'overstep', -'overstock', -'overstuff', -'oversweet', -'overtake', -'overthrow', -'overtime', -'overtly', -'overtone', -'overture', -'overturn', -'overuse', -'overvalue', -'overview', -'overwrite', -'owl', -'oxford', -'oxidant', -'oxidation', -'oxidize', -'oxidizing', -'oxygen', -'oxymoron', -'oyster', -'ozone', -'paced', -'pacemaker', -'pacific', -'pacifier', -'pacifism', -'pacifist', -'pacify', -'padded', -'padding', -'paddle', -'paddling', -'padlock', -'pagan', -'pager', -'paging', -'pajamas', -'palace', -'palatable', -'palm', -'palpable', -'palpitate', -'paltry', -'pampered', -'pamperer', -'pampers', -'pamphlet', -'panama', -'pancake', -'pancreas', -'panda', -'pandemic', -'pang', -'panhandle', -'panic', -'panning', -'panorama', -'panoramic', -'panther', -'pantomime', -'pantry', -'pants', -'pantyhose', -'paparazzi', -'papaya', -'paper', -'paprika', -'papyrus', -'parabola', -'parachute', -'parade', -'paradox', -'paragraph', -'parakeet', -'paralegal', -'paralyses', -'paralysis', -'paralyze', -'paramedic', -'parameter', -'paramount', -'parasail', -'parasite', -'parasitic', -'parcel', -'parched', -'parchment', -'pardon', -'parish', -'parka', -'parking', -'parkway', -'parlor', -'parmesan', -'parole', -'parrot', -'parsley', -'parsnip', -'partake', -'parted', -'parting', -'partition', -'partly', -'partner', -'partridge', -'party', -'passable', -'passably', -'passage', -'passcode', -'passenger', -'passerby', -'passing', -'passion', -'passive', -'passivism', -'passover', -'passport', -'password', -'pasta', -'pasted', -'pastel', -'pastime', -'pastor', -'pastrami', -'pasture', -'pasty', -'patchwork', -'patchy', -'paternal', -'paternity', -'path', -'patience', -'patient', -'patio', -'patriarch', -'patriot', -'patrol', -'patronage', -'patronize', -'pauper', -'pavement', -'paver', -'pavestone', -'pavilion', -'paving', -'pawing', -'payable', -'payback', -'paycheck', -'payday', -'payee', -'payer', -'paying', -'payment', -'payphone', -'payroll', -'pebble', -'pebbly', -'pecan', -'pectin', -'peculiar', -'peddling', -'pediatric', -'pedicure', -'pedigree', -'pedometer', -'pegboard', -'pelican', -'pellet', -'pelt', -'pelvis', -'penalize', -'penalty', -'pencil', -'pendant', -'pending', -'penholder', -'penknife', -'pennant', -'penniless', -'penny', -'penpal', -'pension', -'pentagon', -'pentagram', -'pep', -'perceive', -'percent', -'perch', -'percolate', -'perennial', -'perfected', -'perfectly', -'perfume', -'periscope', -'perish', -'perjurer', -'perjury', -'perkiness', -'perky', -'perm', -'peroxide', -'perpetual', -'perplexed', -'persecute', -'persevere', -'persuaded', -'persuader', -'pesky', -'peso', -'pessimism', -'pessimist', -'pester', -'pesticide', -'petal', -'petite', -'petition', -'petri', -'petroleum', -'petted', -'petticoat', -'pettiness', -'petty', -'petunia', -'phantom', -'phobia', -'phoenix', -'phonebook', -'phoney', -'phonics', -'phoniness', -'phony', -'phosphate', -'photo', -'phrase', -'phrasing', -'placard', -'placate', -'placidly', -'plank', -'planner', -'plant', -'plasma', -'plaster', -'plastic', -'plated', -'platform', -'plating', -'platinum', -'platonic', -'platter', -'platypus', -'plausible', -'plausibly', -'playable', -'playback', -'player', -'playful', -'playgroup', -'playhouse', -'playing', -'playlist', -'playmaker', -'playmate', -'playoff', -'playpen', -'playroom', -'playset', -'plaything', -'playtime', -'plaza', -'pleading', -'pleat', -'pledge', -'plentiful', -'plenty', -'plethora', -'plexiglas', -'pliable', -'plod', -'plop', -'plot', -'plow', -'ploy', -'pluck', -'plug', -'plunder', -'plunging', -'plural', -'plus', -'plutonium', -'plywood', -'poach', -'pod', -'poem', -'poet', -'pogo', -'pointed', -'pointer', -'pointing', -'pointless', -'pointy', -'poise', -'poison', -'poker', -'poking', -'polar', -'police', -'policy', -'polio', -'polish', -'politely', -'polka', -'polo', -'polyester', -'polygon', -'polygraph', -'polymer', -'poncho', -'pond', -'pony', -'popcorn', -'pope', -'poplar', -'popper', -'poppy', -'popsicle', -'populace', -'popular', -'populate', -'porcupine', -'pork', -'porous', -'porridge', -'portable', -'portal', -'portfolio', -'porthole', -'portion', -'portly', -'portside', -'poser', -'posh', -'posing', -'possible', -'possibly', -'possum', -'postage', -'postal', -'postbox', -'postcard', -'posted', -'poster', -'posting', -'postnasal', -'posture', -'postwar', -'pouch', -'pounce', -'pouncing', -'pound', -'pouring', -'pout', -'powdered', -'powdering', -'powdery', -'power', -'powwow', -'pox', -'praising', -'prance', -'prancing', -'pranker', -'prankish', -'prankster', -'prayer', -'praying', -'preacher', -'preaching', -'preachy', -'preamble', -'precinct', -'precise', -'precision', -'precook', -'precut', -'predator', -'predefine', -'predict', -'preface', -'prefix', -'preflight', -'preformed', -'pregame', -'pregnancy', -'pregnant', -'preheated', -'prelaunch', -'prelaw', -'prelude', -'premiere', -'premises', -'premium', -'prenatal', -'preoccupy', -'preorder', -'prepaid', -'prepay', -'preplan', -'preppy', -'preschool', -'prescribe', -'preseason', -'preset', -'preshow', -'president', -'presoak', -'press', -'presume', -'presuming', -'preteen', -'pretended', -'pretender', -'pretense', -'pretext', -'pretty', -'pretzel', -'prevail', -'prevalent', -'prevent', -'preview', -'previous', -'prewar', -'prewashed', -'prideful', -'pried', -'primal', -'primarily', -'primary', -'primate', -'primer', -'primp', -'princess', -'print', -'prior', -'prism', -'prison', -'prissy', -'pristine', -'privacy', -'private', -'privatize', -'prize', -'proactive', -'probable', -'probably', -'probation', -'probe', -'probing', -'probiotic', -'problem', -'procedure', -'process', -'proclaim', -'procreate', -'procurer', -'prodigal', -'prodigy', -'produce', -'product', -'profane', -'profanity', -'professed', -'professor', -'profile', -'profound', -'profusely', -'progeny', -'prognosis', -'program', -'progress', -'projector', -'prologue', -'prolonged', -'promenade', -'prominent', -'promoter', -'promotion', -'prompter', -'promptly', -'prone', -'prong', -'pronounce', -'pronto', -'proofing', -'proofread', -'proofs', -'propeller', -'properly', -'property', -'proponent', -'proposal', -'propose', -'props', -'prorate', -'protector', -'protegee', -'proton', -'prototype', -'protozoan', -'protract', -'protrude', -'proud', -'provable', -'proved', -'proven', -'provided', -'provider', -'providing', -'province', -'proving', -'provoke', -'provoking', -'provolone', -'prowess', -'prowler', -'prowling', -'proximity', -'proxy', -'prozac', -'prude', -'prudishly', -'prune', -'pruning', -'pry', -'psychic', -'public', -'publisher', -'pucker', -'pueblo', -'pug', -'pull', -'pulmonary', -'pulp', -'pulsate', -'pulse', -'pulverize', -'puma', -'pumice', -'pummel', -'punch', -'punctual', -'punctuate', -'punctured', -'pungent', -'punisher', -'punk', -'pupil', -'puppet', -'puppy', -'purchase', -'pureblood', -'purebred', -'purely', -'pureness', -'purgatory', -'purge', -'purging', -'purifier', -'purify', -'purist', -'puritan', -'purity', -'purple', -'purplish', -'purposely', -'purr', -'purse', -'pursuable', -'pursuant', -'pursuit', -'purveyor', -'pushcart', -'pushchair', -'pusher', -'pushiness', -'pushing', -'pushover', -'pushpin', -'pushup', -'pushy', -'putdown', -'putt', -'puzzle', -'puzzling', -'pyramid', -'pyromania', -'python', -'quack', -'quadrant', -'quail', -'quaintly', -'quake', -'quaking', -'qualified', -'qualifier', -'qualify', -'quality', -'qualm', -'quantum', -'quarrel', -'quarry', -'quartered', -'quarterly', -'quarters', -'quartet', -'quench', -'query', -'quicken', -'quickly', -'quickness', -'quicksand', -'quickstep', -'quiet', -'quill', -'quilt', -'quintet', -'quintuple', -'quirk', -'quit', -'quiver', -'quizzical', -'quotable', -'quotation', -'quote', -'rabid', -'race', -'racing', -'racism', -'rack', -'racoon', -'radar', -'radial', -'radiance', -'radiantly', -'radiated', -'radiation', -'radiator', -'radio', -'radish', -'raffle', -'raft', -'rage', -'ragged', -'raging', -'ragweed', -'raider', -'railcar', -'railing', -'railroad', -'railway', -'raisin', -'rake', -'raking', -'rally', -'ramble', -'rambling', -'ramp', -'ramrod', -'ranch', -'rancidity', -'random', -'ranged', -'ranger', -'ranging', -'ranked', -'ranking', -'ransack', -'ranting', -'rants', -'rare', -'rarity', -'rascal', -'rash', -'rasping', -'ravage', -'raven', -'ravine', -'raving', -'ravioli', -'ravishing', -'reabsorb', -'reach', -'reacquire', -'reaction', -'reactive', -'reactor', -'reaffirm', -'ream', -'reanalyze', -'reappear', -'reapply', -'reappoint', -'reapprove', -'rearrange', -'rearview', -'reason', -'reassign', -'reassure', -'reattach', -'reawake', -'rebalance', -'rebate', -'rebel', -'rebirth', -'reboot', -'reborn', -'rebound', -'rebuff', -'rebuild', -'rebuilt', -'reburial', -'rebuttal', -'recall', -'recant', -'recapture', -'recast', -'recede', -'recent', -'recess', -'recharger', -'recipient', -'recital', -'recite', -'reckless', -'reclaim', -'recliner', -'reclining', -'recluse', -'reclusive', -'recognize', -'recoil', -'recollect', -'recolor', -'reconcile', -'reconfirm', -'reconvene', -'recopy', -'record', -'recount', -'recoup', -'recovery', -'recreate', -'rectal', -'rectangle', -'rectified', -'rectify', -'recycled', -'recycler', -'recycling', -'reemerge', -'reenact', -'reenter', -'reentry', -'reexamine', -'referable', -'referee', -'reference', -'refill', -'refinance', -'refined', -'refinery', -'refining', -'refinish', -'reflected', -'reflector', -'reflex', -'reflux', -'refocus', -'refold', -'reforest', -'reformat', -'reformed', -'reformer', -'reformist', -'refract', -'refrain', -'refreeze', -'refresh', -'refried', -'refueling', -'refund', -'refurbish', -'refurnish', -'refusal', -'refuse', -'refusing', -'refutable', -'refute', -'regain', -'regalia', -'regally', -'reggae', -'regime', -'region', -'register', -'registrar', -'registry', -'regress', -'regretful', -'regroup', -'regular', -'regulate', -'regulator', -'rehab', -'reheat', -'rehire', -'rehydrate', -'reimburse', -'reissue', -'reiterate', -'rejoice', -'rejoicing', -'rejoin', -'rekindle', -'relapse', -'relapsing', -'relatable', -'related', -'relation', -'relative', -'relax', -'relay', -'relearn', -'release', -'relenting', -'reliable', -'reliably', -'reliance', -'reliant', -'relic', -'relieve', -'relieving', -'relight', -'relish', -'relive', -'reload', -'relocate', -'relock', -'reluctant', -'rely', -'remake', -'remark', -'remarry', -'rematch', -'remedial', -'remedy', -'remember', -'reminder', -'remindful', -'remission', -'remix', -'remnant', -'remodeler', -'remold', -'remorse', -'remote', -'removable', -'removal', -'removed', -'remover', -'removing', -'rename', -'renderer', -'rendering', -'rendition', -'renegade', -'renewable', -'renewably', -'renewal', -'renewed', -'renounce', -'renovate', -'renovator', -'rentable', -'rental', -'rented', -'renter', -'reoccupy', -'reoccur', -'reopen', -'reorder', -'repackage', -'repacking', -'repaint', -'repair', -'repave', -'repaying', -'repayment', -'repeal', -'repeated', -'repeater', -'repent', -'rephrase', -'replace', -'replay', -'replica', -'reply', -'reporter', -'repose', -'repossess', -'repost', -'repressed', -'reprimand', -'reprint', -'reprise', -'reproach', -'reprocess', -'reproduce', -'reprogram', -'reps', -'reptile', -'reptilian', -'repugnant', -'repulsion', -'repulsive', -'repurpose', -'reputable', -'reputably', -'request', -'require', -'requisite', -'reroute', -'rerun', -'resale', -'resample', -'rescuer', -'reseal', -'research', -'reselect', -'reseller', -'resemble', -'resend', -'resent', -'reset', -'reshape', -'reshoot', -'reshuffle', -'residence', -'residency', -'resident', -'residual', -'residue', -'resigned', -'resilient', -'resistant', -'resisting', -'resize', -'resolute', -'resolved', -'resonant', -'resonate', -'resort', -'resource', -'respect', -'resubmit', -'result', -'resume', -'resupply', -'resurface', -'resurrect', -'retail', -'retainer', -'retaining', -'retake', -'retaliate', -'retention', -'rethink', -'retinal', -'retired', -'retiree', -'retiring', -'retold', -'retool', -'retorted', -'retouch', -'retrace', -'retract', -'retrain', -'retread', -'retreat', -'retrial', -'retrieval', -'retriever', -'retry', -'return', -'retying', -'retype', -'reunion', -'reunite', -'reusable', -'reuse', -'reveal', -'reveler', -'revenge', -'revenue', -'reverb', -'revered', -'reverence', -'reverend', -'reversal', -'reverse', -'reversing', -'reversion', -'revert', -'revisable', -'revise', -'revision', -'revisit', -'revivable', -'revival', -'reviver', -'reviving', -'revocable', -'revoke', -'revolt', -'revolver', -'revolving', -'reward', -'rewash', -'rewind', -'rewire', -'reword', -'rework', -'rewrap', -'rewrite', -'rhyme', -'ribbon', -'ribcage', -'rice', -'riches', -'richly', -'richness', -'rickety', -'ricotta', -'riddance', -'ridden', -'ride', -'riding', -'rifling', -'rift', -'rigging', -'rigid', -'rigor', -'rimless', -'rimmed', -'rind', -'rink', -'rinse', -'rinsing', -'riot', -'ripcord', -'ripeness', -'ripening', -'ripping', -'ripple', -'rippling', -'riptide', -'rise', -'rising', -'risk', -'risotto', -'ritalin', -'ritzy', -'rival', -'riverbank', -'riverbed', -'riverboat', -'riverside', -'riveter', -'riveting', -'roamer', -'roaming', -'roast', -'robbing', -'robe', -'robin', -'robotics', -'robust', -'rockband', -'rocker', -'rocket', -'rockfish', -'rockiness', -'rocking', -'rocklike', -'rockslide', -'rockstar', -'rocky', -'rogue', -'roman', -'romp', -'rope', -'roping', -'roster', -'rosy', -'rotten', -'rotting', -'rotunda', -'roulette', -'rounding', -'roundish', -'roundness', -'roundup', -'roundworm', -'routine', -'routing', -'rover', -'roving', -'royal', -'rubbed', -'rubber', -'rubbing', -'rubble', -'rubdown', -'ruby', -'ruckus', -'rudder', -'rug', -'ruined', -'rule', -'rumble', -'rumbling', -'rummage', -'rumor', -'runaround', -'rundown', -'runner', -'running', -'runny', -'runt', -'runway', -'rupture', -'rural', -'ruse', -'rush', -'rust', -'rut', -'sabbath', -'sabotage', -'sacrament', -'sacred', -'sacrifice', -'sadden', -'saddlebag', -'saddled', -'saddling', -'sadly', -'sadness', -'safari', -'safeguard', -'safehouse', -'safely', -'safeness', -'saffron', -'saga', -'sage', -'sagging', -'saggy', -'said', -'saint', -'sake', -'salad', -'salami', -'salaried', -'salary', -'saline', -'salon', -'saloon', -'salsa', -'salt', -'salutary', -'salute', -'salvage', -'salvaging', -'salvation', -'same', -'sample', -'sampling', -'sanction', -'sanctity', -'sanctuary', -'sandal', -'sandbag', -'sandbank', -'sandbar', -'sandblast', -'sandbox', -'sanded', -'sandfish', -'sanding', -'sandlot', -'sandpaper', -'sandpit', -'sandstone', -'sandstorm', -'sandworm', -'sandy', -'sanitary', -'sanitizer', -'sank', -'santa', -'sapling', -'sappiness', -'sappy', -'sarcasm', -'sarcastic', -'sardine', -'sash', -'sasquatch', -'sassy', -'satchel', -'satiable', -'satin', -'satirical', -'satisfied', -'satisfy', -'saturate', -'saturday', -'sauciness', -'saucy', -'sauna', -'savage', -'savanna', -'saved', -'savings', -'savior', -'savor', -'saxophone', -'say', -'scabbed', -'scabby', -'scalded', -'scalding', -'scale', -'scaling', -'scallion', -'scallop', -'scalping', -'scam', -'scandal', -'scanner', -'scanning', -'scant', -'scapegoat', -'scarce', -'scarcity', -'scarecrow', -'scared', -'scarf', -'scarily', -'scariness', -'scarring', -'scary', -'scavenger', -'scenic', -'schedule', -'schematic', -'scheme', -'scheming', -'schilling', -'schnapps', -'scholar', -'science', -'scientist', -'scion', -'scoff', -'scolding', -'scone', -'scoop', -'scooter', -'scope', -'scorch', -'scorebook', -'scorecard', -'scored', -'scoreless', -'scorer', -'scoring', -'scorn', -'scorpion', -'scotch', -'scoundrel', -'scoured', -'scouring', -'scouting', -'scouts', -'scowling', -'scrabble', -'scraggly', -'scrambled', -'scrambler', -'scrap', -'scratch', -'scrawny', -'screen', -'scribble', -'scribe', -'scribing', -'scrimmage', -'script', -'scroll', -'scrooge', -'scrounger', -'scrubbed', -'scrubber', -'scruffy', -'scrunch', -'scrutiny', -'scuba', -'scuff', -'sculptor', -'sculpture', -'scurvy', -'scuttle', -'secluded', -'secluding', -'seclusion', -'second', -'secrecy', -'secret', -'sectional', -'sector', -'secular', -'securely', -'security', -'sedan', -'sedate', -'sedation', -'sedative', -'sediment', -'seduce', -'seducing', -'segment', -'seismic', -'seizing', -'seldom', -'selected', -'selection', -'selective', -'selector', -'self', -'seltzer', -'semantic', -'semester', -'semicolon', -'semifinal', -'seminar', -'semisoft', -'semisweet', -'senate', -'senator', -'send', -'senior', -'senorita', -'sensation', -'sensitive', -'sensitize', -'sensually', -'sensuous', -'sepia', -'september', -'septic', -'septum', -'sequel', -'sequence', -'sequester', -'series', -'sermon', -'serotonin', -'serpent', -'serrated', -'serve', -'service', -'serving', -'sesame', -'sessions', -'setback', -'setting', -'settle', -'settling', -'setup', -'sevenfold', -'seventeen', -'seventh', -'seventy', -'severity', -'shabby', -'shack', -'shaded', -'shadily', -'shadiness', -'shading', -'shadow', -'shady', -'shaft', -'shakable', -'shakily', -'shakiness', -'shaking', -'shaky', -'shale', -'shallot', -'shallow', -'shame', -'shampoo', -'shamrock', -'shank', -'shanty', -'shape', -'shaping', -'share', -'sharpener', -'sharper', -'sharpie', -'sharply', -'sharpness', -'shawl', -'sheath', -'shed', -'sheep', -'sheet', -'shelf', -'shell', -'shelter', -'shelve', -'shelving', -'sherry', -'shield', -'shifter', -'shifting', -'shiftless', -'shifty', -'shimmer', -'shimmy', -'shindig', -'shine', -'shingle', -'shininess', -'shining', -'shiny', -'ship', -'shirt', -'shivering', -'shock', -'shone', -'shoplift', -'shopper', -'shopping', -'shoptalk', -'shore', -'shortage', -'shortcake', -'shortcut', -'shorten', -'shorter', -'shorthand', -'shortlist', -'shortly', -'shortness', -'shorts', -'shortwave', -'shorty', -'shout', -'shove', -'showbiz', -'showcase', -'showdown', -'shower', -'showgirl', -'showing', -'showman', -'shown', -'showoff', -'showpiece', -'showplace', -'showroom', -'showy', -'shrank', -'shrapnel', -'shredder', -'shredding', -'shrewdly', -'shriek', -'shrill', -'shrimp', -'shrine', -'shrink', -'shrivel', -'shrouded', -'shrubbery', -'shrubs', -'shrug', -'shrunk', -'shucking', -'shudder', -'shuffle', -'shuffling', -'shun', -'shush', -'shut', -'shy', -'siamese', -'siberian', -'sibling', -'siding', -'sierra', -'siesta', -'sift', -'sighing', -'silenced', -'silencer', -'silent', -'silica', -'silicon', -'silk', -'silliness', -'silly', -'silo', -'silt', -'silver', -'similarly', -'simile', -'simmering', -'simple', -'simplify', -'simply', -'sincere', -'sincerity', -'singer', -'singing', -'single', -'singular', -'sinister', -'sinless', -'sinner', -'sinuous', -'sip', -'siren', -'sister', -'sitcom', -'sitter', -'sitting', -'situated', -'situation', -'sixfold', -'sixteen', -'sixth', -'sixties', -'sixtieth', -'sixtyfold', -'sizable', -'sizably', -'size', -'sizing', -'sizzle', -'sizzling', -'skater', -'skating', -'skedaddle', -'skeletal', -'skeleton', -'skeptic', -'sketch', -'skewed', -'skewer', -'skid', -'skied', -'skier', -'skies', -'skiing', -'skilled', -'skillet', -'skillful', -'skimmed', -'skimmer', -'skimming', -'skimpily', -'skincare', -'skinhead', -'skinless', -'skinning', -'skinny', -'skintight', -'skipper', -'skipping', -'skirmish', -'skirt', -'skittle', -'skydiver', -'skylight', -'skyline', -'skype', -'skyrocket', -'skyward', -'slab', -'slacked', -'slacker', -'slacking', -'slackness', -'slacks', -'slain', -'slam', -'slander', -'slang', -'slapping', -'slapstick', -'slashed', -'slashing', -'slate', -'slather', -'slaw', -'sled', -'sleek', -'sleep', -'sleet', -'sleeve', -'slept', -'sliceable', -'sliced', -'slicer', -'slicing', -'slick', -'slider', -'slideshow', -'sliding', -'slighted', -'slighting', -'slightly', -'slimness', -'slimy', -'slinging', -'slingshot', -'slinky', -'slip', -'slit', -'sliver', -'slobbery', -'slogan', -'sloped', -'sloping', -'sloppily', -'sloppy', -'slot', -'slouching', -'slouchy', -'sludge', -'slug', -'slum', -'slurp', -'slush', -'sly', -'small', -'smartly', -'smartness', -'smasher', -'smashing', -'smashup', -'smell', -'smelting', -'smile', -'smilingly', -'smirk', -'smite', -'smith', -'smitten', -'smock', -'smog', -'smoked', -'smokeless', -'smokiness', -'smoking', -'smoky', -'smolder', -'smooth', -'smother', -'smudge', -'smudgy', -'smuggler', -'smuggling', -'smugly', -'smugness', -'snack', -'snagged', -'snaking', -'snap', -'snare', -'snarl', -'snazzy', -'sneak', -'sneer', -'sneeze', -'sneezing', -'snide', -'sniff', -'snippet', -'snipping', -'snitch', -'snooper', -'snooze', -'snore', -'snoring', -'snorkel', -'snort', -'snout', -'snowbird', -'snowboard', -'snowbound', -'snowcap', -'snowdrift', -'snowdrop', -'snowfall', -'snowfield', -'snowflake', -'snowiness', -'snowless', -'snowman', -'snowplow', -'snowshoe', -'snowstorm', -'snowsuit', -'snowy', -'snub', -'snuff', -'snuggle', -'snugly', -'snugness', -'speak', -'spearfish', -'spearhead', -'spearman', -'spearmint', -'species', -'specimen', -'specked', -'speckled', -'specks', -'spectacle', -'spectator', -'spectrum', -'speculate', -'speech', -'speed', -'spellbind', -'speller', -'spelling', -'spendable', -'spender', -'spending', -'spent', -'spew', -'sphere', -'spherical', -'sphinx', -'spider', -'spied', -'spiffy', -'spill', -'spilt', -'spinach', -'spinal', -'spindle', -'spinner', -'spinning', -'spinout', -'spinster', -'spiny', -'spiral', -'spirited', -'spiritism', -'spirits', -'spiritual', -'splashed', -'splashing', -'splashy', -'splatter', -'spleen', -'splendid', -'splendor', -'splice', -'splicing', -'splinter', -'splotchy', -'splurge', -'spoilage', -'spoiled', -'spoiler', -'spoiling', -'spoils', -'spoken', -'spokesman', -'sponge', -'spongy', -'sponsor', -'spoof', -'spookily', -'spooky', -'spool', -'spoon', -'spore', -'sporting', -'sports', -'sporty', -'spotless', -'spotlight', -'spotted', -'spotter', -'spotting', -'spotty', -'spousal', -'spouse', -'spout', -'sprain', -'sprang', -'sprawl', -'spray', -'spree', -'sprig', -'spring', -'sprinkled', -'sprinkler', -'sprint', -'sprite', -'sprout', -'spruce', -'sprung', -'spry', -'spud', -'spur', -'sputter', -'spyglass', -'squabble', -'squad', -'squall', -'squander', -'squash', -'squatted', -'squatter', -'squatting', -'squeak', -'squealer', -'squealing', -'squeamish', -'squeegee', -'squeeze', -'squeezing', -'squid', -'squiggle', -'squiggly', -'squint', -'squire', -'squirt', -'squishier', -'squishy', -'stability', -'stabilize', -'stable', -'stack', -'stadium', -'staff', -'stage', -'staging', -'stagnant', -'stagnate', -'stainable', -'stained', -'staining', -'stainless', -'stalemate', -'staleness', -'stalling', -'stallion', -'stamina', -'stammer', -'stamp', -'stand', -'stank', -'staple', -'stapling', -'starboard', -'starch', -'stardom', -'stardust', -'starfish', -'stargazer', -'staring', -'stark', -'starless', -'starlet', -'starlight', -'starlit', -'starring', -'starry', -'starship', -'starter', -'starting', -'startle', -'startling', -'startup', -'starved', -'starving', -'stash', -'state', -'static', -'statistic', -'statue', -'stature', -'status', -'statute', -'statutory', -'staunch', -'stays', -'steadfast', -'steadier', -'steadily', -'steadying', -'steam', -'steed', -'steep', -'steerable', -'steering', -'steersman', -'stegosaur', -'stellar', -'stem', -'stench', -'stencil', -'step', -'stereo', -'sterile', -'sterility', -'sterilize', -'sterling', -'sternness', -'sternum', -'stew', -'stick', -'stiffen', -'stiffly', -'stiffness', -'stifle', -'stifling', -'stillness', -'stilt', -'stimulant', -'stimulate', -'stimuli', -'stimulus', -'stinger', -'stingily', -'stinging', -'stingray', -'stingy', -'stinking', -'stinky', -'stipend', -'stipulate', -'stir', -'stitch', -'stock', -'stoic', -'stoke', -'stole', -'stomp', -'stonewall', -'stoneware', -'stonework', -'stoning', -'stony', -'stood', -'stooge', -'stool', -'stoop', -'stoplight', -'stoppable', -'stoppage', -'stopped', -'stopper', -'stopping', -'stopwatch', -'storable', -'storage', -'storeroom', -'storewide', -'storm', -'stout', -'stove', -'stowaway', -'stowing', -'straddle', -'straggler', -'strained', -'strainer', -'straining', -'strangely', -'stranger', -'strangle', -'strategic', -'strategy', -'stratus', -'straw', -'stray', -'streak', -'stream', -'street', -'strength', -'strenuous', -'strep', -'stress', -'stretch', -'strewn', -'stricken', -'strict', -'stride', -'strife', -'strike', -'striking', -'strive', -'striving', -'strobe', -'strode', -'stroller', -'strongbox', -'strongly', -'strongman', -'struck', -'structure', -'strudel', -'struggle', -'strum', -'strung', -'strut', -'stubbed', -'stubble', -'stubbly', -'stubborn', -'stucco', -'stuck', -'student', -'studied', -'studio', -'study', -'stuffed', -'stuffing', -'stuffy', -'stumble', -'stumbling', -'stump', -'stung', -'stunned', -'stunner', -'stunning', -'stunt', -'stupor', -'sturdily', -'sturdy', -'styling', -'stylishly', -'stylist', -'stylized', -'stylus', -'suave', -'subarctic', -'subatomic', -'subdivide', -'subdued', -'subduing', -'subfloor', -'subgroup', -'subheader', -'subject', -'sublease', -'sublet', -'sublevel', -'sublime', -'submarine', -'submerge', -'submersed', -'submitter', -'subpanel', -'subpar', -'subplot', -'subprime', -'subscribe', -'subscript', -'subsector', -'subside', -'subsiding', -'subsidize', -'subsidy', -'subsoil', -'subsonic', -'substance', -'subsystem', -'subtext', -'subtitle', -'subtly', -'subtotal', -'subtract', -'subtype', -'suburb', -'subway', -'subwoofer', -'subzero', -'succulent', -'such', -'suction', -'sudden', -'sudoku', -'suds', -'sufferer', -'suffering', -'suffice', -'suffix', -'suffocate', -'suffrage', -'sugar', -'suggest', -'suing', -'suitable', -'suitably', -'suitcase', -'suitor', -'sulfate', -'sulfide', -'sulfite', -'sulfur', -'sulk', -'sullen', -'sulphate', -'sulphuric', -'sultry', -'superbowl', -'superglue', -'superhero', -'superior', -'superjet', -'superman', -'supermom', -'supernova', -'supervise', -'supper', -'supplier', -'supply', -'support', -'supremacy', -'supreme', -'surcharge', -'surely', -'sureness', -'surface', -'surfacing', -'surfboard', -'surfer', -'surgery', -'surgical', -'surging', -'surname', -'surpass', -'surplus', -'surprise', -'surreal', -'surrender', -'surrogate', -'surround', -'survey', -'survival', -'survive', -'surviving', -'survivor', -'sushi', -'suspect', -'suspend', -'suspense', -'sustained', -'sustainer', -'swab', -'swaddling', -'swagger', -'swampland', -'swan', -'swapping', -'swarm', -'sway', -'swear', -'sweat', -'sweep', -'swell', -'swept', -'swerve', -'swifter', -'swiftly', -'swiftness', -'swimmable', -'swimmer', -'swimming', -'swimsuit', -'swimwear', -'swinger', -'swinging', -'swipe', -'swirl', -'switch', -'swivel', -'swizzle', -'swooned', -'swoop', -'swoosh', -'swore', -'sworn', -'swung', -'sycamore', -'sympathy', -'symphonic', -'symphony', -'symptom', -'synapse', -'syndrome', -'synergy', -'synopses', -'synopsis', -'synthesis', -'synthetic', -'syrup', -'system', -'t-shirt', -'tabasco', -'tabby', -'tableful', -'tables', -'tablet', -'tableware', -'tabloid', -'tackiness', -'tacking', -'tackle', -'tackling', -'tacky', -'taco', -'tactful', -'tactical', -'tactics', -'tactile', -'tactless', -'tadpole', -'taekwondo', -'tag', -'tainted', -'take', -'taking', -'talcum', -'talisman', -'tall', -'talon', -'tamale', -'tameness', -'tamer', -'tamper', -'tank', -'tanned', -'tannery', -'tanning', -'tantrum', -'tapeless', -'tapered', -'tapering', -'tapestry', -'tapioca', -'tapping', -'taps', -'tarantula', -'target', -'tarmac', -'tarnish', -'tarot', -'tartar', -'tartly', -'tartness', -'task', -'tassel', -'taste', -'tastiness', -'tasting', -'tasty', -'tattered', -'tattle', -'tattling', -'tattoo', -'taunt', -'tavern', -'thank', -'that', -'thaw', -'theater', -'theatrics', -'thee', -'theft', -'theme', -'theology', -'theorize', -'thermal', -'thermos', -'thesaurus', -'these', -'thesis', -'thespian', -'thicken', -'thicket', -'thickness', -'thieving', -'thievish', -'thigh', -'thimble', -'thing', -'think', -'thinly', -'thinner', -'thinness', -'thinning', -'thirstily', -'thirsting', -'thirsty', -'thirteen', -'thirty', -'thong', -'thorn', -'those', -'thousand', -'thrash', -'thread', -'threaten', -'threefold', -'thrift', -'thrill', -'thrive', -'thriving', -'throat', -'throbbing', -'throng', -'throttle', -'throwaway', -'throwback', -'thrower', -'throwing', -'thud', -'thumb', -'thumping', -'thursday', -'thus', -'thwarting', -'thyself', -'tiara', -'tibia', -'tidal', -'tidbit', -'tidiness', -'tidings', -'tidy', -'tiger', -'tighten', -'tightly', -'tightness', -'tightrope', -'tightwad', -'tigress', -'tile', -'tiling', -'till', -'tilt', -'timid', -'timing', -'timothy', -'tinderbox', -'tinfoil', -'tingle', -'tingling', -'tingly', -'tinker', -'tinkling', -'tinsel', -'tinsmith', -'tint', -'tinwork', -'tiny', -'tipoff', -'tipped', -'tipper', -'tipping', -'tiptoeing', -'tiptop', -'tiring', -'tissue', -'trace', -'tracing', -'track', -'traction', -'tractor', -'trade', -'trading', -'tradition', -'traffic', -'tragedy', -'trailing', -'trailside', -'train', -'traitor', -'trance', -'tranquil', -'transfer', -'transform', -'translate', -'transpire', -'transport', -'transpose', -'trapdoor', -'trapeze', -'trapezoid', -'trapped', -'trapper', -'trapping', -'traps', -'trash', -'travel', -'traverse', -'travesty', -'tray', -'treachery', -'treading', -'treadmill', -'treason', -'treat', -'treble', -'tree', -'trekker', -'tremble', -'trembling', -'tremor', -'trench', -'trend', -'trespass', -'triage', -'trial', -'triangle', -'tribesman', -'tribunal', -'tribune', -'tributary', -'tribute', -'triceps', -'trickery', -'trickily', -'tricking', -'trickle', -'trickster', -'tricky', -'tricolor', -'tricycle', -'trident', -'tried', -'trifle', -'trifocals', -'trillion', -'trilogy', -'trimester', -'trimmer', -'trimming', -'trimness', -'trinity', -'trio', -'tripod', -'tripping', -'triumph', -'trivial', -'trodden', -'trolling', -'trombone', -'trophy', -'tropical', -'tropics', -'trouble', -'troubling', -'trough', -'trousers', -'trout', -'trowel', -'truce', -'truck', -'truffle', -'trump', -'trunks', -'trustable', -'trustee', -'trustful', -'trusting', -'trustless', -'truth', -'try', -'tubby', -'tubeless', -'tubular', -'tucking', -'tuesday', -'tug', -'tuition', -'tulip', -'tumble', -'tumbling', -'tummy', -'turban', -'turbine', -'turbofan', -'turbojet', -'turbulent', -'turf', -'turkey', -'turmoil', -'turret', -'turtle', -'tusk', -'tutor', -'tutu', -'tux', -'tweak', -'tweed', -'tweet', -'tweezers', -'twelve', -'twentieth', -'twenty', -'twerp', -'twice', -'twiddle', -'twiddling', -'twig', -'twilight', -'twine', -'twins', -'twirl', -'twistable', -'twisted', -'twister', -'twisting', -'twisty', -'twitch', -'twitter', -'tycoon', -'tying', -'tyke', -'udder', -'ultimate', -'ultimatum', -'ultra', -'umbilical', -'umbrella', -'umpire', -'unabashed', -'unable', -'unadorned', -'unadvised', -'unafraid', -'unaired', -'unaligned', -'unaltered', -'unarmored', -'unashamed', -'unaudited', -'unawake', -'unaware', -'unbaked', -'unbalance', -'unbeaten', -'unbend', -'unbent', -'unbiased', -'unbitten', -'unblended', -'unblessed', -'unblock', -'unbolted', -'unbounded', -'unboxed', -'unbraided', -'unbridle', -'unbroken', -'unbuckled', -'unbundle', -'unburned', -'unbutton', -'uncanny', -'uncapped', -'uncaring', -'uncertain', -'unchain', -'unchanged', -'uncharted', -'uncheck', -'uncivil', -'unclad', -'unclaimed', -'unclamped', -'unclasp', -'uncle', -'unclip', -'uncloak', -'unclog', -'unclothed', -'uncoated', -'uncoiled', -'uncolored', -'uncombed', -'uncommon', -'uncooked', -'uncork', -'uncorrupt', -'uncounted', -'uncouple', -'uncouth', -'uncover', -'uncross', -'uncrown', -'uncrushed', -'uncured', -'uncurious', -'uncurled', -'uncut', -'undamaged', -'undated', -'undaunted', -'undead', -'undecided', -'undefined', -'underage', -'underarm', -'undercoat', -'undercook', -'undercut', -'underdog', -'underdone', -'underfed', -'underfeed', -'underfoot', -'undergo', -'undergrad', -'underhand', -'underline', -'underling', -'undermine', -'undermost', -'underpaid', -'underpass', -'underpay', -'underrate', -'undertake', -'undertone', -'undertook', -'undertow', -'underuse', -'underwear', -'underwent', -'underwire', -'undesired', -'undiluted', -'undivided', -'undocked', -'undoing', -'undone', -'undrafted', -'undress', -'undrilled', -'undusted', -'undying', -'unearned', -'unearth', -'unease', -'uneasily', -'uneasy', -'uneatable', -'uneaten', -'unedited', -'unelected', -'unending', -'unengaged', -'unenvied', -'unequal', -'unethical', -'uneven', -'unexpired', -'unexposed', -'unfailing', -'unfair', -'unfasten', -'unfazed', -'unfeeling', -'unfiled', -'unfilled', -'unfitted', -'unfitting', -'unfixable', -'unfixed', -'unflawed', -'unfocused', -'unfold', -'unfounded', -'unframed', -'unfreeze', -'unfrosted', -'unfrozen', -'unfunded', -'unglazed', -'ungloved', -'unglue', -'ungodly', -'ungraded', -'ungreased', -'unguarded', -'unguided', -'unhappily', -'unhappy', -'unharmed', -'unhealthy', -'unheard', -'unhearing', -'unheated', -'unhelpful', -'unhidden', -'unhinge', -'unhitched', -'unholy', -'unhook', -'unicorn', -'unicycle', -'unified', -'unifier', -'uniformed', -'uniformly', -'unify', -'unimpeded', -'uninjured', -'uninstall', -'uninsured', -'uninvited', -'union', -'uniquely', -'unisexual', -'unison', -'unissued', -'unit', -'universal', -'universe', -'unjustly', -'unkempt', -'unkind', -'unknotted', -'unknowing', -'unknown', -'unlaced', -'unlatch', -'unlawful', -'unleaded', -'unlearned', -'unleash', -'unless', -'unleveled', -'unlighted', -'unlikable', -'unlimited', -'unlined', -'unlinked', -'unlisted', -'unlit', -'unlivable', -'unloaded', -'unloader', -'unlocked', -'unlocking', -'unlovable', -'unloved', -'unlovely', -'unloving', -'unluckily', -'unlucky', -'unmade', -'unmanaged', -'unmanned', -'unmapped', -'unmarked', -'unmasked', -'unmasking', -'unmatched', -'unmindful', -'unmixable', -'unmixed', -'unmolded', -'unmoral', -'unmovable', -'unmoved', -'unmoving', -'unnamable', -'unnamed', -'unnatural', -'unneeded', -'unnerve', -'unnerving', -'unnoticed', -'unopened', -'unopposed', -'unpack', -'unpadded', -'unpaid', -'unpainted', -'unpaired', -'unpaved', -'unpeeled', -'unpicked', -'unpiloted', -'unpinned', -'unplanned', -'unplanted', -'unpleased', -'unpledged', -'unplowed', -'unplug', -'unpopular', -'unproven', -'unquote', -'unranked', -'unrated', -'unraveled', -'unreached', -'unread', -'unreal', -'unreeling', -'unrefined', -'unrelated', -'unrented', -'unrest', -'unretired', -'unrevised', -'unrigged', -'unripe', -'unrivaled', -'unroasted', -'unrobed', -'unroll', -'unruffled', -'unruly', -'unrushed', -'unsaddle', -'unsafe', -'unsaid', -'unsalted', -'unsaved', -'unsavory', -'unscathed', -'unscented', -'unscrew', -'unsealed', -'unseated', -'unsecured', -'unseeing', -'unseemly', -'unseen', -'unselect', -'unselfish', -'unsent', -'unsettled', -'unshackle', -'unshaken', -'unshaved', -'unshaven', -'unsheathe', -'unshipped', -'unsightly', -'unsigned', -'unskilled', -'unsliced', -'unsmooth', -'unsnap', -'unsocial', -'unsoiled', -'unsold', -'unsolved', -'unsorted', -'unspoiled', -'unspoken', -'unstable', -'unstaffed', -'unstamped', -'unsteady', -'unsterile', -'unstirred', -'unstitch', -'unstopped', -'unstuck', -'unstuffed', -'unstylish', -'unsubtle', -'unsubtly', -'unsuited', -'unsure', -'unsworn', -'untagged', -'untainted', -'untaken', -'untamed', -'untangled', -'untapped', -'untaxed', -'unthawed', -'unthread', -'untidy', -'untie', -'until', -'untimed', -'untimely', -'untitled', -'untoasted', -'untold', -'untouched', -'untracked', -'untrained', -'untreated', -'untried', -'untrimmed', -'untrue', -'untruth', -'unturned', -'untwist', -'untying', -'unusable', -'unused', -'unusual', -'unvalued', -'unvaried', -'unvarying', -'unveiled', -'unveiling', -'unvented', -'unviable', -'unvisited', -'unvocal', -'unwanted', -'unwarlike', -'unwary', -'unwashed', -'unwatched', -'unweave', -'unwed', -'unwelcome', -'unwell', -'unwieldy', -'unwilling', -'unwind', -'unwired', -'unwitting', -'unwomanly', -'unworldly', -'unworn', -'unworried', -'unworthy', -'unwound', -'unwoven', -'unwrapped', -'unwritten', -'unzip', -'upbeat', -'upchuck', -'upcoming', -'upcountry', -'update', -'upfront', -'upgrade', -'upheaval', -'upheld', -'uphill', -'uphold', -'uplifted', -'uplifting', -'upload', -'upon', -'upper', -'upright', -'uprising', -'upriver', -'uproar', -'uproot', -'upscale', -'upside', -'upstage', -'upstairs', -'upstart', -'upstate', -'upstream', -'upstroke', -'upswing', -'uptake', -'uptight', -'uptown', -'upturned', -'upward', -'upwind', -'uranium', -'urban', -'urchin', -'urethane', -'urgency', -'urgent', -'urging', -'urologist', -'urology', -'usable', -'usage', -'useable', -'used', -'uselessly', -'user', -'usher', -'usual', -'utensil', -'utility', -'utilize', -'utmost', -'utopia', -'utter', -'vacancy', -'vacant', -'vacate', -'vacation', -'vagabond', -'vagrancy', -'vagrantly', -'vaguely', -'vagueness', -'valiant', -'valid', -'valium', -'valley', -'valuables', -'value', -'vanilla', -'vanish', -'vanity', -'vanquish', -'vantage', -'vaporizer', -'variable', -'variably', -'varied', -'variety', -'various', -'varmint', -'varnish', -'varsity', -'varying', -'vascular', -'vaseline', -'vastly', -'vastness', -'veal', -'vegan', -'veggie', -'vehicular', -'velcro', -'velocity', -'velvet', -'vendetta', -'vending', -'vendor', -'veneering', -'vengeful', -'venomous', -'ventricle', -'venture', -'venue', -'venus', -'verbalize', -'verbally', -'verbose', -'verdict', -'verify', -'verse', -'version', -'versus', -'vertebrae', -'vertical', -'vertigo', -'very', -'vessel', -'vest', -'veteran', -'veto', -'vexingly', -'viability', -'viable', -'vibes', -'vice', -'vicinity', -'victory', -'video', -'viewable', -'viewer', -'viewing', -'viewless', -'viewpoint', -'vigorous', -'village', -'villain', -'vindicate', -'vineyard', -'vintage', -'violate', -'violation', -'violator', -'violet', -'violin', -'viper', -'viral', -'virtual', -'virtuous', -'virus', -'visa', -'viscosity', -'viscous', -'viselike', -'visible', -'visibly', -'vision', -'visiting', -'visitor', -'visor', -'vista', -'vitality', -'vitalize', -'vitally', -'vitamins', -'vivacious', -'vividly', -'vividness', -'vixen', -'vocalist', -'vocalize', -'vocally', -'vocation', -'voice', -'voicing', -'void', -'volatile', -'volley', -'voltage', -'volumes', -'voter', -'voting', -'voucher', -'vowed', -'vowel', -'voyage', -'wackiness', -'wad', -'wafer', -'waffle', -'waged', -'wager', -'wages', -'waggle', -'wagon', -'wake', -'waking', -'walk', -'walmart', -'walnut', -'walrus', -'waltz', -'wand', -'wannabe', -'wanted', -'wanting', -'wasabi', -'washable', -'washbasin', -'washboard', -'washbowl', -'washcloth', -'washday', -'washed', -'washer', -'washhouse', -'washing', -'washout', -'washroom', -'washstand', -'washtub', -'wasp', -'wasting', -'watch', -'water', -'waviness', -'waving', -'wavy', -'whacking', -'whacky', -'wham', -'wharf', -'wheat', -'whenever', -'whiff', -'whimsical', -'whinny', -'whiny', -'whisking', -'whoever', -'whole', -'whomever', -'whoopee', -'whooping', -'whoops', -'why', -'wick', -'widely', -'widen', -'widget', -'widow', -'width', -'wieldable', -'wielder', -'wife', -'wifi', -'wikipedia', -'wildcard', -'wildcat', -'wilder', -'wildfire', -'wildfowl', -'wildland', -'wildlife', -'wildly', -'wildness', -'willed', -'willfully', -'willing', -'willow', -'willpower', -'wilt', -'wimp', -'wince', -'wincing', -'wind', -'wing', -'winking', -'winner', -'winnings', -'winter', -'wipe', -'wired', -'wireless', -'wiring', -'wiry', -'wisdom', -'wise', -'wish', -'wisplike', -'wispy', -'wistful', -'wizard', -'wobble', -'wobbling', -'wobbly', -'wok', -'wolf', -'wolverine', -'womanhood', -'womankind', -'womanless', -'womanlike', -'womanly', -'womb', -'woof', -'wooing', -'wool', -'woozy', -'word', -'work', -'worried', -'worrier', -'worrisome', -'worry', -'worsening', -'worshiper', -'worst', -'wound', -'woven', -'wow', -'wrangle', -'wrath', -'wreath', -'wreckage', -'wrecker', -'wrecking', -'wrench', -'wriggle', -'wriggly', -'wrinkle', -'wrinkly', -'wrist', -'writing', -'written', -'wrongdoer', -'wronged', -'wrongful', -'wrongly', -'wrongness', -'wrought', -'xbox', -'xerox', -'yahoo', -'yam', -'yanking', -'yapping', -'yard', -'yarn', -'yeah', -'yearbook', -'yearling', -'yearly', -'yearning', -'yeast', -'yelling', -'yelp', -'yen', -'yesterday', -'yiddish', -'yield', -'yin', -'yippee', -'yo-yo', -'yodel', -'yoga', -'yogurt', -'yonder', -'yoyo', -'yummy', -'zap', -'zealous', -'zebra', -'zen', -'zeppelin', -'zero', -'zestfully', -'zesty', -'zigzagged', -'zipfile', -'zipping', -'zippy', -'zips', -'zit', -'zodiac', -'zombie', -'zone', -'zoning', -'zookeeper', -'zoologist', -'zoology', -'zoom']; +export const EEFLongWordList = [ + 'abacus', + 'abdomen', + 'abdominal', + 'abide', + 'abiding', + 'ability', + 'ablaze', + 'able', + 'abnormal', + 'abrasion', + 'abrasive', + 'abreast', + 'abridge', + 'abroad', + 'abruptly', + 'absence', + 'absentee', + 'absently', + 'absinthe', + 'absolute', + 'absolve', + 'abstain', + 'abstract', + 'absurd', + 'accent', + 'acclaim', + 'acclimate', + 'accompany', + 'account', + 'accuracy', + 'accurate', + 'accustom', + 'acetone', + 'achiness', + 'aching', + 'acid', + 'acorn', + 'acquaint', + 'acquire', + 'acre', + 'acrobat', + 'acronym', + 'acting', + 'action', + 'activate', + 'activator', + 'active', + 'activism', + 'activist', + 'activity', + 'actress', + 'acts', + 'acutely', + 'acuteness', + 'aeration', + 'aerobics', + 'aerosol', + 'aerospace', + 'afar', + 'affair', + 'affected', + 'affecting', + 'affection', + 'affidavit', + 'affiliate', + 'affirm', + 'affix', + 'afflicted', + 'affluent', + 'afford', + 'affront', + 'aflame', + 'afloat', + 'aflutter', + 'afoot', + 'afraid', + 'afterglow', + 'afterlife', + 'aftermath', + 'aftermost', + 'afternoon', + 'aged', + 'ageless', + 'agency', + 'agenda', + 'agent', + 'aggregate', + 'aghast', + 'agile', + 'agility', + 'aging', + 'agnostic', + 'agonize', + 'agonizing', + 'agony', + 'agreeable', + 'agreeably', + 'agreed', + 'agreeing', + 'agreement', + 'aground', + 'ahead', + 'ahoy', + 'aide', + 'aids', + 'aim', + 'ajar', + 'alabaster', + 'alarm', + 'albatross', + 'album', + 'alfalfa', + 'algebra', + 'algorithm', + 'alias', + 'alibi', + 'alienable', + 'alienate', + 'aliens', + 'alike', + 'alive', + 'alkaline', + 'alkalize', + 'almanac', + 'almighty', + 'almost', + 'aloe', + 'aloft', + 'aloha', + 'alone', + 'alongside', + 'aloof', + 'alphabet', + 'alright', + 'although', + 'altitude', + 'alto', + 'aluminum', + 'alumni', + 'always', + 'amaretto', + 'amaze', + 'amazingly', + 'amber', + 'ambiance', + 'ambiguity', + 'ambiguous', + 'ambition', + 'ambitious', + 'ambulance', + 'ambush', + 'amendable', + 'amendment', + 'amends', + 'amenity', + 'amiable', + 'amicably', + 'amid', + 'amigo', + 'amino', + 'amiss', + 'ammonia', + 'ammonium', + 'amnesty', + 'amniotic', + 'among', + 'amount', + 'amperage', + 'ample', + 'amplifier', + 'amplify', + 'amply', + 'amuck', + 'amulet', + 'amusable', + 'amused', + 'amusement', + 'amuser', + 'amusing', + 'anaconda', + 'anaerobic', + 'anagram', + 'anatomist', + 'anatomy', + 'anchor', + 'anchovy', + 'ancient', + 'android', + 'anemia', + 'anemic', + 'aneurism', + 'anew', + 'angelfish', + 'angelic', + 'anger', + 'angled', + 'angler', + 'angles', + 'angling', + 'angrily', + 'angriness', + 'anguished', + 'angular', + 'animal', + 'animate', + 'animating', + 'animation', + 'animator', + 'anime', + 'animosity', + 'ankle', + 'annex', + 'annotate', + 'announcer', + 'annoying', + 'annually', + 'annuity', + 'anointer', + 'another', + 'answering', + 'antacid', + 'antarctic', + 'anteater', + 'antelope', + 'antennae', + 'anthem', + 'anthill', + 'anthology', + 'antibody', + 'antics', + 'antidote', + 'antihero', + 'antiquely', + 'antiques', + 'antiquity', + 'antirust', + 'antitoxic', + 'antitrust', + 'antiviral', + 'antivirus', + 'antler', + 'antonym', + 'antsy', + 'anvil', + 'anybody', + 'anyhow', + 'anymore', + 'anyone', + 'anyplace', + 'anything', + 'anytime', + 'anyway', + 'anywhere', + 'aorta', + 'apache', + 'apostle', + 'appealing', + 'appear', + 'appease', + 'appeasing', + 'appendage', + 'appendix', + 'appetite', + 'appetizer', + 'applaud', + 'applause', + 'apple', + 'appliance', + 'applicant', + 'applied', + 'apply', + 'appointee', + 'appraisal', + 'appraiser', + 'apprehend', + 'approach', + 'approval', + 'approve', + 'apricot', + 'april', + 'apron', + 'aptitude', + 'aptly', + 'aqua', + 'aqueduct', + 'arbitrary', + 'arbitrate', + 'ardently', + 'area', + 'arena', + 'arguable', + 'arguably', + 'argue', + 'arise', + 'armadillo', + 'armband', + 'armchair', + 'armed', + 'armful', + 'armhole', + 'arming', + 'armless', + 'armoire', + 'armored', + 'armory', + 'armrest', + 'army', + 'aroma', + 'arose', + 'around', + 'arousal', + 'arrange', + 'array', + 'arrest', + 'arrival', + 'arrive', + 'arrogance', + 'arrogant', + 'arson', + 'art', + 'ascend', + 'ascension', + 'ascent', + 'ascertain', + 'ashamed', + 'ashen', + 'ashes', + 'ashy', + 'aside', + 'askew', + 'asleep', + 'asparagus', + 'aspect', + 'aspirate', + 'aspire', + 'aspirin', + 'astonish', + 'astound', + 'astride', + 'astrology', + 'astronaut', + 'astronomy', + 'astute', + 'atlantic', + 'atlas', + 'atom', + 'atonable', + 'atop', + 'atrium', + 'atrocious', + 'atrophy', + 'attach', + 'attain', + 'attempt', + 'attendant', + 'attendee', + 'attention', + 'attentive', + 'attest', + 'attic', + 'attire', + 'attitude', + 'attractor', + 'attribute', + 'atypical', + 'auction', + 'audacious', + 'audacity', + 'audible', + 'audibly', + 'audience', + 'audio', + 'audition', + 'augmented', + 'august', + 'authentic', + 'author', + 'autism', + 'autistic', + 'autograph', + 'automaker', + 'automated', + 'automatic', + 'autopilot', + 'available', + 'avalanche', + 'avatar', + 'avenge', + 'avenging', + 'avenue', + 'average', + 'aversion', + 'avert', + 'aviation', + 'aviator', + 'avid', + 'avoid', + 'await', + 'awaken', + 'award', + 'aware', + 'awhile', + 'awkward', + 'awning', + 'awoke', + 'awry', + 'axis', + 'babble', + 'babbling', + 'babied', + 'baboon', + 'backache', + 'backboard', + 'backboned', + 'backdrop', + 'backed', + 'backer', + 'backfield', + 'backfire', + 'backhand', + 'backing', + 'backlands', + 'backlash', + 'backless', + 'backlight', + 'backlit', + 'backlog', + 'backpack', + 'backpedal', + 'backrest', + 'backroom', + 'backshift', + 'backside', + 'backslid', + 'backspace', + 'backspin', + 'backstab', + 'backstage', + 'backtalk', + 'backtrack', + 'backup', + 'backward', + 'backwash', + 'backwater', + 'backyard', + 'bacon', + 'bacteria', + 'bacterium', + 'badass', + 'badge', + 'badland', + 'badly', + 'badness', + 'baffle', + 'baffling', + 'bagel', + 'bagful', + 'baggage', + 'bagged', + 'baggie', + 'bagginess', + 'bagging', + 'baggy', + 'bagpipe', + 'baguette', + 'baked', + 'bakery', + 'bakeshop', + 'baking', + 'balance', + 'balancing', + 'balcony', + 'balmy', + 'balsamic', + 'bamboo', + 'banana', + 'banish', + 'banister', + 'banjo', + 'bankable', + 'bankbook', + 'banked', + 'banker', + 'banking', + 'banknote', + 'bankroll', + 'banner', + 'bannister', + 'banshee', + 'banter', + 'barbecue', + 'barbed', + 'barbell', + 'barber', + 'barcode', + 'barge', + 'bargraph', + 'barista', + 'baritone', + 'barley', + 'barmaid', + 'barman', + 'barn', + 'barometer', + 'barrack', + 'barracuda', + 'barrel', + 'barrette', + 'barricade', + 'barrier', + 'barstool', + 'bartender', + 'barterer', + 'bash', + 'basically', + 'basics', + 'basil', + 'basin', + 'basis', + 'basket', + 'batboy', + 'batch', + 'bath', + 'baton', + 'bats', + 'battalion', + 'battered', + 'battering', + 'battery', + 'batting', + 'battle', + 'bauble', + 'bazooka', + 'blabber', + 'bladder', + 'blade', + 'blah', + 'blame', + 'blaming', + 'blanching', + 'blandness', + 'blank', + 'blaspheme', + 'blasphemy', + 'blast', + 'blatancy', + 'blatantly', + 'blazer', + 'blazing', + 'bleach', + 'bleak', + 'bleep', + 'blemish', + 'blend', + 'bless', + 'blighted', + 'blimp', + 'bling', + 'blinked', + 'blinker', + 'blinking', + 'blinks', + 'blip', + 'blissful', + 'blitz', + 'blizzard', + 'bloated', + 'bloating', + 'blob', + 'blog', + 'bloomers', + 'blooming', + 'blooper', + 'blot', + 'blouse', + 'blubber', + 'bluff', + 'bluish', + 'blunderer', + 'blunt', + 'blurb', + 'blurred', + 'blurry', + 'blurt', + 'blush', + 'blustery', + 'boaster', + 'boastful', + 'boasting', + 'boat', + 'bobbed', + 'bobbing', + 'bobble', + 'bobcat', + 'bobsled', + 'bobtail', + 'bodacious', + 'body', + 'bogged', + 'boggle', + 'bogus', + 'boil', + 'bok', + 'bolster', + 'bolt', + 'bonanza', + 'bonded', + 'bonding', + 'bondless', + 'boned', + 'bonehead', + 'boneless', + 'bonelike', + 'boney', + 'bonfire', + 'bonnet', + 'bonsai', + 'bonus', + 'bony', + 'boogeyman', + 'boogieman', + 'book', + 'boondocks', + 'booted', + 'booth', + 'bootie', + 'booting', + 'bootlace', + 'bootleg', + 'boots', + 'boozy', + 'borax', + 'boring', + 'borough', + 'borrower', + 'borrowing', + 'boss', + 'botanical', + 'botanist', + 'botany', + 'botch', + 'both', + 'bottle', + 'bottling', + 'bottom', + 'bounce', + 'bouncing', + 'bouncy', + 'bounding', + 'boundless', + 'bountiful', + 'bovine', + 'boxcar', + 'boxer', + 'boxing', + 'boxlike', + 'boxy', + 'breach', + 'breath', + 'breeches', + 'breeching', + 'breeder', + 'breeding', + 'breeze', + 'breezy', + 'brethren', + 'brewery', + 'brewing', + 'briar', + 'bribe', + 'brick', + 'bride', + 'bridged', + 'brigade', + 'bright', + 'brilliant', + 'brim', + 'bring', + 'brink', + 'brisket', + 'briskly', + 'briskness', + 'bristle', + 'brittle', + 'broadband', + 'broadcast', + 'broaden', + 'broadly', + 'broadness', + 'broadside', + 'broadways', + 'broiler', + 'broiling', + 'broken', + 'broker', + 'bronchial', + 'bronco', + 'bronze', + 'bronzing', + 'brook', + 'broom', + 'brought', + 'browbeat', + 'brownnose', + 'browse', + 'browsing', + 'bruising', + 'brunch', + 'brunette', + 'brunt', + 'brush', + 'brussels', + 'brute', + 'brutishly', + 'bubble', + 'bubbling', + 'bubbly', + 'buccaneer', + 'bucked', + 'bucket', + 'buckle', + 'buckshot', + 'buckskin', + 'bucktooth', + 'buckwheat', + 'buddhism', + 'buddhist', + 'budding', + 'buddy', + 'budget', + 'buffalo', + 'buffed', + 'buffer', + 'buffing', + 'buffoon', + 'buggy', + 'bulb', + 'bulge', + 'bulginess', + 'bulgur', + 'bulk', + 'bulldog', + 'bulldozer', + 'bullfight', + 'bullfrog', + 'bullhorn', + 'bullion', + 'bullish', + 'bullpen', + 'bullring', + 'bullseye', + 'bullwhip', + 'bully', + 'bunch', + 'bundle', + 'bungee', + 'bunion', + 'bunkbed', + 'bunkhouse', + 'bunkmate', + 'bunny', + 'bunt', + 'busboy', + 'bush', + 'busily', + 'busload', + 'bust', + 'busybody', + 'buzz', + 'cabana', + 'cabbage', + 'cabbie', + 'cabdriver', + 'cable', + 'caboose', + 'cache', + 'cackle', + 'cacti', + 'cactus', + 'caddie', + 'caddy', + 'cadet', + 'cadillac', + 'cadmium', + 'cage', + 'cahoots', + 'cake', + 'calamari', + 'calamity', + 'calcium', + 'calculate', + 'calculus', + 'caliber', + 'calibrate', + 'calm', + 'caloric', + 'calorie', + 'calzone', + 'camcorder', + 'cameo', + 'camera', + 'camisole', + 'camper', + 'campfire', + 'camping', + 'campsite', + 'campus', + 'canal', + 'canary', + 'cancel', + 'candied', + 'candle', + 'candy', + 'cane', + 'canine', + 'canister', + 'cannabis', + 'canned', + 'canning', + 'cannon', + 'cannot', + 'canola', + 'canon', + 'canopener', + 'canopy', + 'canteen', + 'canyon', + 'capable', + 'capably', + 'capacity', + 'cape', + 'capillary', + 'capital', + 'capitol', + 'capped', + 'capricorn', + 'capsize', + 'capsule', + 'caption', + 'captivate', + 'captive', + 'captivity', + 'capture', + 'caramel', + 'carat', + 'caravan', + 'carbon', + 'cardboard', + 'carded', + 'cardiac', + 'cardigan', + 'cardinal', + 'cardstock', + 'carefully', + 'caregiver', + 'careless', + 'caress', + 'caretaker', + 'cargo', + 'caring', + 'carless', + 'carload', + 'carmaker', + 'carnage', + 'carnation', + 'carnival', + 'carnivore', + 'carol', + 'carpenter', + 'carpentry', + 'carpool', + 'carport', + 'carried', + 'carrot', + 'carrousel', + 'carry', + 'cartel', + 'cartload', + 'carton', + 'cartoon', + 'cartridge', + 'cartwheel', + 'carve', + 'carving', + 'carwash', + 'cascade', + 'case', + 'cash', + 'casing', + 'casino', + 'casket', + 'cassette', + 'casually', + 'casualty', + 'catacomb', + 'catalog', + 'catalyst', + 'catalyze', + 'catapult', + 'cataract', + 'catatonic', + 'catcall', + 'catchable', + 'catcher', + 'catching', + 'catchy', + 'caterer', + 'catering', + 'catfight', + 'catfish', + 'cathedral', + 'cathouse', + 'catlike', + 'catnap', + 'catnip', + 'catsup', + 'cattail', + 'cattishly', + 'cattle', + 'catty', + 'catwalk', + 'caucasian', + 'caucus', + 'causal', + 'causation', + 'cause', + 'causing', + 'cauterize', + 'caution', + 'cautious', + 'cavalier', + 'cavalry', + 'caviar', + 'cavity', + 'cedar', + 'celery', + 'celestial', + 'celibacy', + 'celibate', + 'celtic', + 'cement', + 'census', + 'ceramics', + 'ceremony', + 'certainly', + 'certainty', + 'certified', + 'certify', + 'cesarean', + 'cesspool', + 'chafe', + 'chaffing', + 'chain', + 'chair', + 'chalice', + 'challenge', + 'chamber', + 'chamomile', + 'champion', + 'chance', + 'change', + 'channel', + 'chant', + 'chaos', + 'chaperone', + 'chaplain', + 'chapped', + 'chaps', + 'chapter', + 'character', + 'charbroil', + 'charcoal', + 'charger', + 'charging', + 'chariot', + 'charity', + 'charm', + 'charred', + 'charter', + 'charting', + 'chase', + 'chasing', + 'chaste', + 'chastise', + 'chastity', + 'chatroom', + 'chatter', + 'chatting', + 'chatty', + 'cheating', + 'cheddar', + 'cheek', + 'cheer', + 'cheese', + 'cheesy', + 'chef', + 'chemicals', + 'chemist', + 'chemo', + 'cherisher', + 'cherub', + 'chess', + 'chest', + 'chevron', + 'chevy', + 'chewable', + 'chewer', + 'chewing', + 'chewy', + 'chief', + 'chihuahua', + 'childcare', + 'childhood', + 'childish', + 'childless', + 'childlike', + 'chili', + 'chill', + 'chimp', + 'chip', + 'chirping', + 'chirpy', + 'chitchat', + 'chivalry', + 'chive', + 'chloride', + 'chlorine', + 'choice', + 'chokehold', + 'choking', + 'chomp', + 'chooser', + 'choosing', + 'choosy', + 'chop', + 'chosen', + 'chowder', + 'chowtime', + 'chrome', + 'chubby', + 'chuck', + 'chug', + 'chummy', + 'chump', + 'chunk', + 'churn', + 'chute', + 'cider', + 'cilantro', + 'cinch', + 'cinema', + 'cinnamon', + 'circle', + 'circling', + 'circular', + 'circulate', + 'circus', + 'citable', + 'citadel', + 'citation', + 'citizen', + 'citric', + 'citrus', + 'city', + 'civic', + 'civil', + 'clad', + 'claim', + 'clambake', + 'clammy', + 'clamor', + 'clamp', + 'clamshell', + 'clang', + 'clanking', + 'clapped', + 'clapper', + 'clapping', + 'clarify', + 'clarinet', + 'clarity', + 'clash', + 'clasp', + 'class', + 'clatter', + 'clause', + 'clavicle', + 'claw', + 'clay', + 'clean', + 'clear', + 'cleat', + 'cleaver', + 'cleft', + 'clench', + 'clergyman', + 'clerical', + 'clerk', + 'clever', + 'clicker', + 'client', + 'climate', + 'climatic', + 'cling', + 'clinic', + 'clinking', + 'clip', + 'clique', + 'cloak', + 'clobber', + 'clock', + 'clone', + 'cloning', + 'closable', + 'closure', + 'clothes', + 'clothing', + 'cloud', + 'clover', + 'clubbed', + 'clubbing', + 'clubhouse', + 'clump', + 'clumsily', + 'clumsy', + 'clunky', + 'clustered', + 'clutch', + 'clutter', + 'coach', + 'coagulant', + 'coastal', + 'coaster', + 'coasting', + 'coastland', + 'coastline', + 'coat', + 'coauthor', + 'cobalt', + 'cobbler', + 'cobweb', + 'cocoa', + 'coconut', + 'cod', + 'coeditor', + 'coerce', + 'coexist', + 'coffee', + 'cofounder', + 'cognition', + 'cognitive', + 'cogwheel', + 'coherence', + 'coherent', + 'cohesive', + 'coil', + 'coke', + 'cola', + 'cold', + 'coleslaw', + 'coliseum', + 'collage', + 'collapse', + 'collar', + 'collected', + 'collector', + 'collide', + 'collie', + 'collision', + 'colonial', + 'colonist', + 'colonize', + 'colony', + 'colossal', + 'colt', + 'coma', + 'come', + 'comfort', + 'comfy', + 'comic', + 'coming', + 'comma', + 'commence', + 'commend', + 'comment', + 'commerce', + 'commode', + 'commodity', + 'commodore', + 'common', + 'commotion', + 'commute', + 'commuting', + 'compacted', + 'compacter', + 'compactly', + 'compactor', + 'companion', + 'company', + 'compare', + 'compel', + 'compile', + 'comply', + 'component', + 'composed', + 'composer', + 'composite', + 'compost', + 'composure', + 'compound', + 'compress', + 'comprised', + 'computer', + 'computing', + 'comrade', + 'concave', + 'conceal', + 'conceded', + 'concept', + 'concerned', + 'concert', + 'conch', + 'concierge', + 'concise', + 'conclude', + 'concrete', + 'concur', + 'condense', + 'condiment', + 'condition', + 'condone', + 'conducive', + 'conductor', + 'conduit', + 'cone', + 'confess', + 'confetti', + 'confidant', + 'confident', + 'confider', + 'confiding', + 'configure', + 'confined', + 'confining', + 'confirm', + 'conflict', + 'conform', + 'confound', + 'confront', + 'confused', + 'confusing', + 'confusion', + 'congenial', + 'congested', + 'congrats', + 'congress', + 'conical', + 'conjoined', + 'conjure', + 'conjuror', + 'connected', + 'connector', + 'consensus', + 'consent', + 'console', + 'consoling', + 'consonant', + 'constable', + 'constant', + 'constrain', + 'constrict', + 'construct', + 'consult', + 'consumer', + 'consuming', + 'contact', + 'container', + 'contempt', + 'contend', + 'contented', + 'contently', + 'contents', + 'contest', + 'context', + 'contort', + 'contour', + 'contrite', + 'control', + 'contusion', + 'convene', + 'convent', + 'copartner', + 'cope', + 'copied', + 'copier', + 'copilot', + 'coping', + 'copious', + 'copper', + 'copy', + 'coral', + 'cork', + 'cornball', + 'cornbread', + 'corncob', + 'cornea', + 'corned', + 'corner', + 'cornfield', + 'cornflake', + 'cornhusk', + 'cornmeal', + 'cornstalk', + 'corny', + 'coronary', + 'coroner', + 'corporal', + 'corporate', + 'corral', + 'correct', + 'corridor', + 'corrode', + 'corroding', + 'corrosive', + 'corsage', + 'corset', + 'cortex', + 'cosigner', + 'cosmetics', + 'cosmic', + 'cosmos', + 'cosponsor', + 'cost', + 'cottage', + 'cotton', + 'couch', + 'cough', + 'could', + 'countable', + 'countdown', + 'counting', + 'countless', + 'country', + 'county', + 'courier', + 'covenant', + 'cover', + 'coveted', + 'coveting', + 'coyness', + 'cozily', + 'coziness', + 'cozy', + 'crabbing', + 'crabgrass', + 'crablike', + 'crabmeat', + 'cradle', + 'cradling', + 'crafter', + 'craftily', + 'craftsman', + 'craftwork', + 'crafty', + 'cramp', + 'cranberry', + 'crane', + 'cranial', + 'cranium', + 'crank', + 'crate', + 'crave', + 'craving', + 'crawfish', + 'crawlers', + 'crawling', + 'crayfish', + 'crayon', + 'crazed', + 'crazily', + 'craziness', + 'crazy', + 'creamed', + 'creamer', + 'creamlike', + 'crease', + 'creasing', + 'creatable', + 'create', + 'creation', + 'creative', + 'creature', + 'credible', + 'credibly', + 'credit', + 'creed', + 'creme', + 'creole', + 'crepe', + 'crept', + 'crescent', + 'crested', + 'cresting', + 'crestless', + 'crevice', + 'crewless', + 'crewman', + 'crewmate', + 'crib', + 'cricket', + 'cried', + 'crier', + 'crimp', + 'crimson', + 'cringe', + 'cringing', + 'crinkle', + 'crinkly', + 'crisped', + 'crisping', + 'crisply', + 'crispness', + 'crispy', + 'criteria', + 'critter', + 'croak', + 'crock', + 'crook', + 'croon', + 'crop', + 'cross', + 'crouch', + 'crouton', + 'crowbar', + 'crowd', + 'crown', + 'crucial', + 'crudely', + 'crudeness', + 'cruelly', + 'cruelness', + 'cruelty', + 'crumb', + 'crummiest', + 'crummy', + 'crumpet', + 'crumpled', + 'cruncher', + 'crunching', + 'crunchy', + 'crusader', + 'crushable', + 'crushed', + 'crusher', + 'crushing', + 'crust', + 'crux', + 'crying', + 'cryptic', + 'crystal', + 'cubbyhole', + 'cube', + 'cubical', + 'cubicle', + 'cucumber', + 'cuddle', + 'cuddly', + 'cufflink', + 'culinary', + 'culminate', + 'culpable', + 'culprit', + 'cultivate', + 'cultural', + 'culture', + 'cupbearer', + 'cupcake', + 'cupid', + 'cupped', + 'cupping', + 'curable', + 'curator', + 'curdle', + 'cure', + 'curfew', + 'curing', + 'curled', + 'curler', + 'curliness', + 'curling', + 'curly', + 'curry', + 'curse', + 'cursive', + 'cursor', + 'curtain', + 'curtly', + 'curtsy', + 'curvature', + 'curve', + 'curvy', + 'cushy', + 'cusp', + 'cussed', + 'custard', + 'custodian', + 'custody', + 'customary', + 'customer', + 'customize', + 'customs', + 'cut', + 'cycle', + 'cyclic', + 'cycling', + 'cyclist', + 'cylinder', + 'cymbal', + 'cytoplasm', + 'cytoplast', + 'dab', + 'dad', + 'daffodil', + 'dagger', + 'daily', + 'daintily', + 'dainty', + 'dairy', + 'daisy', + 'dallying', + 'dance', + 'dancing', + 'dandelion', + 'dander', + 'dandruff', + 'dandy', + 'danger', + 'dangle', + 'dangling', + 'daredevil', + 'dares', + 'daringly', + 'darkened', + 'darkening', + 'darkish', + 'darkness', + 'darkroom', + 'darling', + 'darn', + 'dart', + 'darwinism', + 'dash', + 'dastardly', + 'data', + 'datebook', + 'dating', + 'daughter', + 'daunting', + 'dawdler', + 'dawn', + 'daybed', + 'daybreak', + 'daycare', + 'daydream', + 'daylight', + 'daylong', + 'dayroom', + 'daytime', + 'dazzler', + 'dazzling', + 'deacon', + 'deafening', + 'deafness', + 'dealer', + 'dealing', + 'dealmaker', + 'dealt', + 'dean', + 'debatable', + 'debate', + 'debating', + 'debit', + 'debrief', + 'debtless', + 'debtor', + 'debug', + 'debunk', + 'decade', + 'decaf', + 'decal', + 'decathlon', + 'decay', + 'deceased', + 'deceit', + 'deceiver', + 'deceiving', + 'december', + 'decency', + 'decent', + 'deception', + 'deceptive', + 'decibel', + 'decidable', + 'decimal', + 'decimeter', + 'decipher', + 'deck', + 'declared', + 'decline', + 'decode', + 'decompose', + 'decorated', + 'decorator', + 'decoy', + 'decrease', + 'decree', + 'dedicate', + 'dedicator', + 'deduce', + 'deduct', + 'deed', + 'deem', + 'deepen', + 'deeply', + 'deepness', + 'deface', + 'defacing', + 'defame', + 'default', + 'defeat', + 'defection', + 'defective', + 'defendant', + 'defender', + 'defense', + 'defensive', + 'deferral', + 'deferred', + 'defiance', + 'defiant', + 'defile', + 'defiling', + 'define', + 'definite', + 'deflate', + 'deflation', + 'deflator', + 'deflected', + 'deflector', + 'defog', + 'deforest', + 'defraud', + 'defrost', + 'deftly', + 'defuse', + 'defy', + 'degraded', + 'degrading', + 'degrease', + 'degree', + 'dehydrate', + 'deity', + 'dejected', + 'delay', + 'delegate', + 'delegator', + 'delete', + 'deletion', + 'delicacy', + 'delicate', + 'delicious', + 'delighted', + 'delirious', + 'delirium', + 'deliverer', + 'delivery', + 'delouse', + 'delta', + 'deluge', + 'delusion', + 'deluxe', + 'demanding', + 'demeaning', + 'demeanor', + 'demise', + 'democracy', + 'democrat', + 'demote', + 'demotion', + 'demystify', + 'denatured', + 'deniable', + 'denial', + 'denim', + 'denote', + 'dense', + 'density', + 'dental', + 'dentist', + 'denture', + 'deny', + 'deodorant', + 'deodorize', + 'departed', + 'departure', + 'depict', + 'deplete', + 'depletion', + 'deplored', + 'deploy', + 'deport', + 'depose', + 'depraved', + 'depravity', + 'deprecate', + 'depress', + 'deprive', + 'depth', + 'deputize', + 'deputy', + 'derail', + 'deranged', + 'derby', + 'derived', + 'desecrate', + 'deserve', + 'deserving', + 'designate', + 'designed', + 'designer', + 'designing', + 'deskbound', + 'desktop', + 'deskwork', + 'desolate', + 'despair', + 'despise', + 'despite', + 'destiny', + 'destitute', + 'destruct', + 'detached', + 'detail', + 'detection', + 'detective', + 'detector', + 'detention', + 'detergent', + 'detest', + 'detonate', + 'detonator', + 'detoxify', + 'detract', + 'deuce', + 'devalue', + 'deviancy', + 'deviant', + 'deviate', + 'deviation', + 'deviator', + 'device', + 'devious', + 'devotedly', + 'devotee', + 'devotion', + 'devourer', + 'devouring', + 'devoutly', + 'dexterity', + 'dexterous', + 'diabetes', + 'diabetic', + 'diabolic', + 'diagnoses', + 'diagnosis', + 'diagram', + 'dial', + 'diameter', + 'diaper', + 'diaphragm', + 'diary', + 'dice', + 'dicing', + 'dictate', + 'dictation', + 'dictator', + 'difficult', + 'diffused', + 'diffuser', + 'diffusion', + 'diffusive', + 'dig', + 'dilation', + 'diligence', + 'diligent', + 'dill', + 'dilute', + 'dime', + 'diminish', + 'dimly', + 'dimmed', + 'dimmer', + 'dimness', + 'dimple', + 'diner', + 'dingbat', + 'dinghy', + 'dinginess', + 'dingo', + 'dingy', + 'dining', + 'dinner', + 'diocese', + 'dioxide', + 'diploma', + 'dipped', + 'dipper', + 'dipping', + 'directed', + 'direction', + 'directive', + 'directly', + 'directory', + 'direness', + 'dirtiness', + 'disabled', + 'disagree', + 'disallow', + 'disarm', + 'disarray', + 'disaster', + 'disband', + 'disbelief', + 'disburse', + 'discard', + 'discern', + 'discharge', + 'disclose', + 'discolor', + 'discount', + 'discourse', + 'discover', + 'discuss', + 'disdain', + 'disengage', + 'disfigure', + 'disgrace', + 'dish', + 'disinfect', + 'disjoin', + 'disk', + 'dislike', + 'disliking', + 'dislocate', + 'dislodge', + 'disloyal', + 'dismantle', + 'dismay', + 'dismiss', + 'dismount', + 'disobey', + 'disorder', + 'disown', + 'disparate', + 'disparity', + 'dispatch', + 'dispense', + 'dispersal', + 'dispersed', + 'disperser', + 'displace', + 'display', + 'displease', + 'disposal', + 'dispose', + 'disprove', + 'dispute', + 'disregard', + 'disrupt', + 'dissuade', + 'distance', + 'distant', + 'distaste', + 'distill', + 'distinct', + 'distort', + 'distract', + 'distress', + 'district', + 'distrust', + 'ditch', + 'ditto', + 'ditzy', + 'dividable', + 'divided', + 'dividend', + 'dividers', + 'dividing', + 'divinely', + 'diving', + 'divinity', + 'divisible', + 'divisibly', + 'division', + 'divisive', + 'divorcee', + 'dizziness', + 'dizzy', + 'doable', + 'docile', + 'dock', + 'doctrine', + 'document', + 'dodge', + 'dodgy', + 'doily', + 'doing', + 'dole', + 'dollar', + 'dollhouse', + 'dollop', + 'dolly', + 'dolphin', + 'domain', + 'domelike', + 'domestic', + 'dominion', + 'dominoes', + 'donated', + 'donation', + 'donator', + 'donor', + 'donut', + 'doodle', + 'doorbell', + 'doorframe', + 'doorknob', + 'doorman', + 'doormat', + 'doornail', + 'doorpost', + 'doorstep', + 'doorstop', + 'doorway', + 'doozy', + 'dork', + 'dormitory', + 'dorsal', + 'dosage', + 'dose', + 'dotted', + 'doubling', + 'douche', + 'dove', + 'down', + 'dowry', + 'doze', + 'drab', + 'dragging', + 'dragonfly', + 'dragonish', + 'dragster', + 'drainable', + 'drainage', + 'drained', + 'drainer', + 'drainpipe', + 'dramatic', + 'dramatize', + 'drank', + 'drapery', + 'drastic', + 'draw', + 'dreaded', + 'dreadful', + 'dreadlock', + 'dreamboat', + 'dreamily', + 'dreamland', + 'dreamless', + 'dreamlike', + 'dreamt', + 'dreamy', + 'drearily', + 'dreary', + 'drench', + 'dress', + 'drew', + 'dribble', + 'dried', + 'drier', + 'drift', + 'driller', + 'drilling', + 'drinkable', + 'drinking', + 'dripping', + 'drippy', + 'drivable', + 'driven', + 'driver', + 'driveway', + 'driving', + 'drizzle', + 'drizzly', + 'drone', + 'drool', + 'droop', + 'drop-down', + 'dropbox', + 'dropkick', + 'droplet', + 'dropout', + 'dropper', + 'drove', + 'drown', + 'drowsily', + 'drudge', + 'drum', + 'dry', + 'dubbed', + 'dubiously', + 'duchess', + 'duckbill', + 'ducking', + 'duckling', + 'ducktail', + 'ducky', + 'duct', + 'dude', + 'duffel', + 'dugout', + 'duh', + 'duke', + 'duller', + 'dullness', + 'duly', + 'dumping', + 'dumpling', + 'dumpster', + 'duo', + 'dupe', + 'duplex', + 'duplicate', + 'duplicity', + 'durable', + 'durably', + 'duration', + 'duress', + 'during', + 'dusk', + 'dust', + 'dutiful', + 'duty', + 'duvet', + 'dwarf', + 'dweeb', + 'dwelled', + 'dweller', + 'dwelling', + 'dwindle', + 'dwindling', + 'dynamic', + 'dynamite', + 'dynasty', + 'dyslexia', + 'dyslexic', + 'each', + 'eagle', + 'earache', + 'eardrum', + 'earflap', + 'earful', + 'earlobe', + 'early', + 'earmark', + 'earmuff', + 'earphone', + 'earpiece', + 'earplugs', + 'earring', + 'earshot', + 'earthen', + 'earthlike', + 'earthling', + 'earthly', + 'earthworm', + 'earthy', + 'earwig', + 'easeful', + 'easel', + 'easiest', + 'easily', + 'easiness', + 'easing', + 'eastbound', + 'eastcoast', + 'easter', + 'eastward', + 'eatable', + 'eaten', + 'eatery', + 'eating', + 'eats', + 'ebay', + 'ebony', + 'ebook', + 'ecard', + 'eccentric', + 'echo', + 'eclair', + 'eclipse', + 'ecologist', + 'ecology', + 'economic', + 'economist', + 'economy', + 'ecosphere', + 'ecosystem', + 'edge', + 'edginess', + 'edging', + 'edgy', + 'edition', + 'editor', + 'educated', + 'education', + 'educator', + 'eel', + 'effective', + 'effects', + 'efficient', + 'effort', + 'eggbeater', + 'egging', + 'eggnog', + 'eggplant', + 'eggshell', + 'egomaniac', + 'egotism', + 'egotistic', + 'either', + 'eject', + 'elaborate', + 'elastic', + 'elated', + 'elbow', + 'eldercare', + 'elderly', + 'eldest', + 'electable', + 'election', + 'elective', + 'elephant', + 'elevate', + 'elevating', + 'elevation', + 'elevator', + 'eleven', + 'elf', + 'eligible', + 'eligibly', + 'eliminate', + 'elite', + 'elitism', + 'elixir', + 'elk', + 'ellipse', + 'elliptic', + 'elm', + 'elongated', + 'elope', + 'eloquence', + 'eloquent', + 'elsewhere', + 'elude', + 'elusive', + 'elves', + 'email', + 'embargo', + 'embark', + 'embassy', + 'embattled', + 'embellish', + 'ember', + 'embezzle', + 'emblaze', + 'emblem', + 'embody', + 'embolism', + 'emboss', + 'embroider', + 'emcee', + 'emerald', + 'emergency', + 'emission', + 'emit', + 'emote', + 'emoticon', + 'emotion', + 'empathic', + 'empathy', + 'emperor', + 'emphases', + 'emphasis', + 'emphasize', + 'emphatic', + 'empirical', + 'employed', + 'employee', + 'employer', + 'emporium', + 'empower', + 'emptier', + 'emptiness', + 'empty', + 'emu', + 'enable', + 'enactment', + 'enamel', + 'enchanted', + 'enchilada', + 'encircle', + 'enclose', + 'enclosure', + 'encode', + 'encore', + 'encounter', + 'encourage', + 'encroach', + 'encrust', + 'encrypt', + 'endanger', + 'endeared', + 'endearing', + 'ended', + 'ending', + 'endless', + 'endnote', + 'endocrine', + 'endorphin', + 'endorse', + 'endowment', + 'endpoint', + 'endurable', + 'endurance', + 'enduring', + 'energetic', + 'energize', + 'energy', + 'enforced', + 'enforcer', + 'engaged', + 'engaging', + 'engine', + 'engorge', + 'engraved', + 'engraver', + 'engraving', + 'engross', + 'engulf', + 'enhance', + 'enigmatic', + 'enjoyable', + 'enjoyably', + 'enjoyer', + 'enjoying', + 'enjoyment', + 'enlarged', + 'enlarging', + 'enlighten', + 'enlisted', + 'enquirer', + 'enrage', + 'enrich', + 'enroll', + 'enslave', + 'ensnare', + 'ensure', + 'entail', + 'entangled', + 'entering', + 'entertain', + 'enticing', + 'entire', + 'entitle', + 'entity', + 'entomb', + 'entourage', + 'entrap', + 'entree', + 'entrench', + 'entrust', + 'entryway', + 'entwine', + 'enunciate', + 'envelope', + 'enviable', + 'enviably', + 'envious', + 'envision', + 'envoy', + 'envy', + 'enzyme', + 'epic', + 'epidemic', + 'epidermal', + 'epidermis', + 'epidural', + 'epilepsy', + 'epileptic', + 'epilogue', + 'epiphany', + 'episode', + 'equal', + 'equate', + 'equation', + 'equator', + 'equinox', + 'equipment', + 'equity', + 'equivocal', + 'eradicate', + 'erasable', + 'erased', + 'eraser', + 'erasure', + 'ergonomic', + 'errand', + 'errant', + 'erratic', + 'error', + 'erupt', + 'escalate', + 'escalator', + 'escapable', + 'escapade', + 'escapist', + 'escargot', + 'eskimo', + 'esophagus', + 'espionage', + 'espresso', + 'esquire', + 'essay', + 'essence', + 'essential', + 'establish', + 'estate', + 'esteemed', + 'estimate', + 'estimator', + 'estranged', + 'estrogen', + 'etching', + 'eternal', + 'eternity', + 'ethanol', + 'ether', + 'ethically', + 'ethics', + 'euphemism', + 'evacuate', + 'evacuee', + 'evade', + 'evaluate', + 'evaluator', + 'evaporate', + 'evasion', + 'evasive', + 'even', + 'everglade', + 'evergreen', + 'everybody', + 'everyday', + 'everyone', + 'evict', + 'evidence', + 'evident', + 'evil', + 'evoke', + 'evolution', + 'evolve', + 'exact', + 'exalted', + 'example', + 'excavate', + 'excavator', + 'exceeding', + 'exception', + 'excess', + 'exchange', + 'excitable', + 'exciting', + 'exclaim', + 'exclude', + 'excluding', + 'exclusion', + 'exclusive', + 'excretion', + 'excretory', + 'excursion', + 'excusable', + 'excusably', + 'excuse', + 'exemplary', + 'exemplify', + 'exemption', + 'exerciser', + 'exert', + 'exes', + 'exfoliate', + 'exhale', + 'exhaust', + 'exhume', + 'exile', + 'existing', + 'exit', + 'exodus', + 'exonerate', + 'exorcism', + 'exorcist', + 'expand', + 'expanse', + 'expansion', + 'expansive', + 'expectant', + 'expedited', + 'expediter', + 'expel', + 'expend', + 'expenses', + 'expensive', + 'expert', + 'expire', + 'expiring', + 'explain', + 'expletive', + 'explicit', + 'explode', + 'exploit', + 'explore', + 'exploring', + 'exponent', + 'exporter', + 'exposable', + 'expose', + 'exposure', + 'express', + 'expulsion', + 'exquisite', + 'extended', + 'extending', + 'extent', + 'extenuate', + 'exterior', + 'external', + 'extinct', + 'extortion', + 'extradite', + 'extras', + 'extrovert', + 'extrude', + 'extruding', + 'exuberant', + 'fable', + 'fabric', + 'fabulous', + 'facebook', + 'facecloth', + 'facedown', + 'faceless', + 'facelift', + 'faceplate', + 'faceted', + 'facial', + 'facility', + 'facing', + 'facsimile', + 'faction', + 'factoid', + 'factor', + 'factsheet', + 'factual', + 'faculty', + 'fade', + 'fading', + 'failing', + 'falcon', + 'fall', + 'false', + 'falsify', + 'fame', + 'familiar', + 'family', + 'famine', + 'famished', + 'fanatic', + 'fancied', + 'fanciness', + 'fancy', + 'fanfare', + 'fang', + 'fanning', + 'fantasize', + 'fantastic', + 'fantasy', + 'fascism', + 'fastball', + 'faster', + 'fasting', + 'fastness', + 'faucet', + 'favorable', + 'favorably', + 'favored', + 'favoring', + 'favorite', + 'fax', + 'feast', + 'federal', + 'fedora', + 'feeble', + 'feed', + 'feel', + 'feisty', + 'feline', + 'felt-tip', + 'feminine', + 'feminism', + 'feminist', + 'feminize', + 'femur', + 'fence', + 'fencing', + 'fender', + 'ferment', + 'fernlike', + 'ferocious', + 'ferocity', + 'ferret', + 'ferris', + 'ferry', + 'fervor', + 'fester', + 'festival', + 'festive', + 'festivity', + 'fetal', + 'fetch', + 'fever', + 'fiber', + 'fiction', + 'fiddle', + 'fiddling', + 'fidelity', + 'fidgeting', + 'fidgety', + 'fifteen', + 'fifth', + 'fiftieth', + 'fifty', + 'figment', + 'figure', + 'figurine', + 'filing', + 'filled', + 'filler', + 'filling', + 'film', + 'filter', + 'filth', + 'filtrate', + 'finale', + 'finalist', + 'finalize', + 'finally', + 'finance', + 'financial', + 'finch', + 'fineness', + 'finer', + 'finicky', + 'finished', + 'finisher', + 'finishing', + 'finite', + 'finless', + 'finlike', + 'fiscally', + 'fit', + 'five', + 'flaccid', + 'flagman', + 'flagpole', + 'flagship', + 'flagstick', + 'flagstone', + 'flail', + 'flakily', + 'flaky', + 'flame', + 'flammable', + 'flanked', + 'flanking', + 'flannels', + 'flap', + 'flaring', + 'flashback', + 'flashbulb', + 'flashcard', + 'flashily', + 'flashing', + 'flashy', + 'flask', + 'flatbed', + 'flatfoot', + 'flatly', + 'flatness', + 'flatten', + 'flattered', + 'flatterer', + 'flattery', + 'flattop', + 'flatware', + 'flatworm', + 'flavored', + 'flavorful', + 'flavoring', + 'flaxseed', + 'fled', + 'fleshed', + 'fleshy', + 'flick', + 'flier', + 'flight', + 'flinch', + 'fling', + 'flint', + 'flip', + 'flirt', + 'float', + 'flock', + 'flogging', + 'flop', + 'floral', + 'florist', + 'floss', + 'flounder', + 'flyable', + 'flyaway', + 'flyer', + 'flying', + 'flyover', + 'flypaper', + 'foam', + 'foe', + 'fog', + 'foil', + 'folic', + 'folk', + 'follicle', + 'follow', + 'fondling', + 'fondly', + 'fondness', + 'fondue', + 'font', + 'food', + 'fool', + 'footage', + 'football', + 'footbath', + 'footboard', + 'footer', + 'footgear', + 'foothill', + 'foothold', + 'footing', + 'footless', + 'footman', + 'footnote', + 'footpad', + 'footpath', + 'footprint', + 'footrest', + 'footsie', + 'footsore', + 'footwear', + 'footwork', + 'fossil', + 'foster', + 'founder', + 'founding', + 'fountain', + 'fox', + 'foyer', + 'fraction', + 'fracture', + 'fragile', + 'fragility', + 'fragment', + 'fragrance', + 'fragrant', + 'frail', + 'frame', + 'framing', + 'frantic', + 'fraternal', + 'frayed', + 'fraying', + 'frays', + 'freckled', + 'freckles', + 'freebase', + 'freebee', + 'freebie', + 'freedom', + 'freefall', + 'freehand', + 'freeing', + 'freeload', + 'freely', + 'freemason', + 'freeness', + 'freestyle', + 'freeware', + 'freeway', + 'freewill', + 'freezable', + 'freezing', + 'freight', + 'french', + 'frenzied', + 'frenzy', + 'frequency', + 'frequent', + 'fresh', + 'fretful', + 'fretted', + 'friction', + 'friday', + 'fridge', + 'fried', + 'friend', + 'frighten', + 'frightful', + 'frigidity', + 'frigidly', + 'frill', + 'fringe', + 'frisbee', + 'frisk', + 'fritter', + 'frivolous', + 'frolic', + 'from', + 'front', + 'frostbite', + 'frosted', + 'frostily', + 'frosting', + 'frostlike', + 'frosty', + 'froth', + 'frown', + 'frozen', + 'fructose', + 'frugality', + 'frugally', + 'fruit', + 'frustrate', + 'frying', + 'gab', + 'gaffe', + 'gag', + 'gainfully', + 'gaining', + 'gains', + 'gala', + 'gallantly', + 'galleria', + 'gallery', + 'galley', + 'gallon', + 'gallows', + 'gallstone', + 'galore', + 'galvanize', + 'gambling', + 'game', + 'gaming', + 'gamma', + 'gander', + 'gangly', + 'gangrene', + 'gangway', + 'gap', + 'garage', + 'garbage', + 'garden', + 'gargle', + 'garland', + 'garlic', + 'garment', + 'garnet', + 'garnish', + 'garter', + 'gas', + 'gatherer', + 'gathering', + 'gating', + 'gauging', + 'gauntlet', + 'gauze', + 'gave', + 'gawk', + 'gazing', + 'gear', + 'gecko', + 'geek', + 'geiger', + 'gem', + 'gender', + 'generic', + 'generous', + 'genetics', + 'genre', + 'gentile', + 'gentleman', + 'gently', + 'gents', + 'geography', + 'geologic', + 'geologist', + 'geology', + 'geometric', + 'geometry', + 'geranium', + 'gerbil', + 'geriatric', + 'germicide', + 'germinate', + 'germless', + 'germproof', + 'gestate', + 'gestation', + 'gesture', + 'getaway', + 'getting', + 'getup', + 'giant', + 'gibberish', + 'giblet', + 'giddily', + 'giddiness', + 'giddy', + 'gift', + 'gigabyte', + 'gigahertz', + 'gigantic', + 'giggle', + 'giggling', + 'giggly', + 'gigolo', + 'gilled', + 'gills', + 'gimmick', + 'girdle', + 'giveaway', + 'given', + 'giver', + 'giving', + 'gizmo', + 'gizzard', + 'glacial', + 'glacier', + 'glade', + 'gladiator', + 'gladly', + 'glamorous', + 'glamour', + 'glance', + 'glancing', + 'glandular', + 'glare', + 'glaring', + 'glass', + 'glaucoma', + 'glazing', + 'gleaming', + 'gleeful', + 'glider', + 'gliding', + 'glimmer', + 'glimpse', + 'glisten', + 'glitch', + 'glitter', + 'glitzy', + 'gloater', + 'gloating', + 'gloomily', + 'gloomy', + 'glorified', + 'glorifier', + 'glorify', + 'glorious', + 'glory', + 'gloss', + 'glove', + 'glowing', + 'glowworm', + 'glucose', + 'glue', + 'gluten', + 'glutinous', + 'glutton', + 'gnarly', + 'gnat', + 'goal', + 'goatskin', + 'goes', + 'goggles', + 'going', + 'goldfish', + 'goldmine', + 'goldsmith', + 'golf', + 'goliath', + 'gonad', + 'gondola', + 'gone', + 'gong', + 'good', + 'gooey', + 'goofball', + 'goofiness', + 'goofy', + 'google', + 'goon', + 'gopher', + 'gore', + 'gorged', + 'gorgeous', + 'gory', + 'gosling', + 'gossip', + 'gothic', + 'gotten', + 'gout', + 'gown', + 'grab', + 'graceful', + 'graceless', + 'gracious', + 'gradation', + 'graded', + 'grader', + 'gradient', + 'grading', + 'gradually', + 'graduate', + 'graffiti', + 'grafted', + 'grafting', + 'grain', + 'granddad', + 'grandkid', + 'grandly', + 'grandma', + 'grandpa', + 'grandson', + 'granite', + 'granny', + 'granola', + 'grant', + 'granular', + 'grape', + 'graph', + 'grapple', + 'grappling', + 'grasp', + 'grass', + 'gratified', + 'gratify', + 'grating', + 'gratitude', + 'gratuity', + 'gravel', + 'graveness', + 'graves', + 'graveyard', + 'gravitate', + 'gravity', + 'gravy', + 'gray', + 'grazing', + 'greasily', + 'greedily', + 'greedless', + 'greedy', + 'green', + 'greeter', + 'greeting', + 'grew', + 'greyhound', + 'grid', + 'grief', + 'grievance', + 'grieving', + 'grievous', + 'grill', + 'grimace', + 'grimacing', + 'grime', + 'griminess', + 'grimy', + 'grinch', + 'grinning', + 'grip', + 'gristle', + 'grit', + 'groggily', + 'groggy', + 'groin', + 'groom', + 'groove', + 'grooving', + 'groovy', + 'grope', + 'ground', + 'grouped', + 'grout', + 'grove', + 'grower', + 'growing', + 'growl', + 'grub', + 'grudge', + 'grudging', + 'grueling', + 'gruffly', + 'grumble', + 'grumbling', + 'grumbly', + 'grumpily', + 'grunge', + 'grunt', + 'guacamole', + 'guidable', + 'guidance', + 'guide', + 'guiding', + 'guileless', + 'guise', + 'gulf', + 'gullible', + 'gully', + 'gulp', + 'gumball', + 'gumdrop', + 'gumminess', + 'gumming', + 'gummy', + 'gurgle', + 'gurgling', + 'guru', + 'gush', + 'gusto', + 'gusty', + 'gutless', + 'guts', + 'gutter', + 'guy', + 'guzzler', + 'gyration', + 'habitable', + 'habitant', + 'habitat', + 'habitual', + 'hacked', + 'hacker', + 'hacking', + 'hacksaw', + 'had', + 'haggler', + 'haiku', + 'half', + 'halogen', + 'halt', + 'halved', + 'halves', + 'hamburger', + 'hamlet', + 'hammock', + 'hamper', + 'hamster', + 'hamstring', + 'handbag', + 'handball', + 'handbook', + 'handbrake', + 'handcart', + 'handclap', + 'handclasp', + 'handcraft', + 'handcuff', + 'handed', + 'handful', + 'handgrip', + 'handgun', + 'handheld', + 'handiness', + 'handiwork', + 'handlebar', + 'handled', + 'handler', + 'handling', + 'handmade', + 'handoff', + 'handpick', + 'handprint', + 'handrail', + 'handsaw', + 'handset', + 'handsfree', + 'handshake', + 'handstand', + 'handwash', + 'handwork', + 'handwoven', + 'handwrite', + 'handyman', + 'hangnail', + 'hangout', + 'hangover', + 'hangup', + 'hankering', + 'hankie', + 'hanky', + 'haphazard', + 'happening', + 'happier', + 'happiest', + 'happily', + 'happiness', + 'happy', + 'harbor', + 'hardcopy', + 'hardcore', + 'hardcover', + 'harddisk', + 'hardened', + 'hardener', + 'hardening', + 'hardhat', + 'hardhead', + 'hardiness', + 'hardly', + 'hardness', + 'hardship', + 'hardware', + 'hardwired', + 'hardwood', + 'hardy', + 'harmful', + 'harmless', + 'harmonica', + 'harmonics', + 'harmonize', + 'harmony', + 'harness', + 'harpist', + 'harsh', + 'harvest', + 'hash', + 'hassle', + 'haste', + 'hastily', + 'hastiness', + 'hasty', + 'hatbox', + 'hatchback', + 'hatchery', + 'hatchet', + 'hatching', + 'hatchling', + 'hate', + 'hatless', + 'hatred', + 'haunt', + 'haven', + 'hazard', + 'hazelnut', + 'hazily', + 'haziness', + 'hazing', + 'hazy', + 'headache', + 'headband', + 'headboard', + 'headcount', + 'headdress', + 'headed', + 'header', + 'headfirst', + 'headgear', + 'heading', + 'headlamp', + 'headless', + 'headlock', + 'headphone', + 'headpiece', + 'headrest', + 'headroom', + 'headscarf', + 'headset', + 'headsman', + 'headstand', + 'headstone', + 'headway', + 'headwear', + 'heap', + 'heat', + 'heave', + 'heavily', + 'heaviness', + 'heaving', + 'hedge', + 'hedging', + 'heftiness', + 'hefty', + 'helium', + 'helmet', + 'helper', + 'helpful', + 'helping', + 'helpless', + 'helpline', + 'hemlock', + 'hemstitch', + 'hence', + 'henchman', + 'henna', + 'herald', + 'herbal', + 'herbicide', + 'herbs', + 'heritage', + 'hermit', + 'heroics', + 'heroism', + 'herring', + 'herself', + 'hertz', + 'hesitancy', + 'hesitant', + 'hesitate', + 'hexagon', + 'hexagram', + 'hubcap', + 'huddle', + 'huddling', + 'huff', + 'hug', + 'hula', + 'hulk', + 'hull', + 'human', + 'humble', + 'humbling', + 'humbly', + 'humid', + 'humiliate', + 'humility', + 'humming', + 'hummus', + 'humongous', + 'humorist', + 'humorless', + 'humorous', + 'humpback', + 'humped', + 'humvee', + 'hunchback', + 'hundredth', + 'hunger', + 'hungrily', + 'hungry', + 'hunk', + 'hunter', + 'hunting', + 'huntress', + 'huntsman', + 'hurdle', + 'hurled', + 'hurler', + 'hurling', + 'hurray', + 'hurricane', + 'hurried', + 'hurry', + 'hurt', + 'husband', + 'hush', + 'husked', + 'huskiness', + 'hut', + 'hybrid', + 'hydrant', + 'hydrated', + 'hydration', + 'hydrogen', + 'hydroxide', + 'hyperlink', + 'hypertext', + 'hyphen', + 'hypnoses', + 'hypnosis', + 'hypnotic', + 'hypnotism', + 'hypnotist', + 'hypnotize', + 'hypocrisy', + 'hypocrite', + 'ibuprofen', + 'ice', + 'iciness', + 'icing', + 'icky', + 'icon', + 'icy', + 'idealism', + 'idealist', + 'idealize', + 'ideally', + 'idealness', + 'identical', + 'identify', + 'identity', + 'ideology', + 'idiocy', + 'idiom', + 'idly', + 'igloo', + 'ignition', + 'ignore', + 'iguana', + 'illicitly', + 'illusion', + 'illusive', + 'image', + 'imaginary', + 'imagines', + 'imaging', + 'imbecile', + 'imitate', + 'imitation', + 'immature', + 'immerse', + 'immersion', + 'imminent', + 'immobile', + 'immodest', + 'immorally', + 'immortal', + 'immovable', + 'immovably', + 'immunity', + 'immunize', + 'impaired', + 'impale', + 'impart', + 'impatient', + 'impeach', + 'impeding', + 'impending', + 'imperfect', + 'imperial', + 'impish', + 'implant', + 'implement', + 'implicate', + 'implicit', + 'implode', + 'implosion', + 'implosive', + 'imply', + 'impolite', + 'important', + 'importer', + 'impose', + 'imposing', + 'impotence', + 'impotency', + 'impotent', + 'impound', + 'imprecise', + 'imprint', + 'imprison', + 'impromptu', + 'improper', + 'improve', + 'improving', + 'improvise', + 'imprudent', + 'impulse', + 'impulsive', + 'impure', + 'impurity', + 'iodine', + 'iodize', + 'ion', + 'ipad', + 'iphone', + 'ipod', + 'irate', + 'irk', + 'iron', + 'irregular', + 'irrigate', + 'irritable', + 'irritably', + 'irritant', + 'irritate', + 'islamic', + 'islamist', + 'isolated', + 'isolating', + 'isolation', + 'isotope', + 'issue', + 'issuing', + 'italicize', + 'italics', + 'item', + 'itinerary', + 'itunes', + 'ivory', + 'ivy', + 'jab', + 'jackal', + 'jacket', + 'jackknife', + 'jackpot', + 'jailbird', + 'jailbreak', + 'jailer', + 'jailhouse', + 'jalapeno', + 'jam', + 'janitor', + 'january', + 'jargon', + 'jarring', + 'jasmine', + 'jaundice', + 'jaunt', + 'java', + 'jawed', + 'jawless', + 'jawline', + 'jaws', + 'jaybird', + 'jaywalker', + 'jazz', + 'jeep', + 'jeeringly', + 'jellied', + 'jelly', + 'jersey', + 'jester', + 'jet', + 'jiffy', + 'jigsaw', + 'jimmy', + 'jingle', + 'jingling', + 'jinx', + 'jitters', + 'jittery', + 'job', + 'jockey', + 'jockstrap', + 'jogger', + 'jogging', + 'john', + 'joining', + 'jokester', + 'jokingly', + 'jolliness', + 'jolly', + 'jolt', + 'jot', + 'jovial', + 'joyfully', + 'joylessly', + 'joyous', + 'joyride', + 'joystick', + 'jubilance', + 'jubilant', + 'judge', + 'judgingly', + 'judicial', + 'judiciary', + 'judo', + 'juggle', + 'juggling', + 'jugular', + 'juice', + 'juiciness', + 'juicy', + 'jujitsu', + 'jukebox', + 'july', + 'jumble', + 'jumbo', + 'jump', + 'junction', + 'juncture', + 'june', + 'junior', + 'juniper', + 'junkie', + 'junkman', + 'junkyard', + 'jurist', + 'juror', + 'jury', + 'justice', + 'justifier', + 'justify', + 'justly', + 'justness', + 'juvenile', + 'kabob', + 'kangaroo', + 'karaoke', + 'karate', + 'karma', + 'kebab', + 'keenly', + 'keenness', + 'keep', + 'keg', + 'kelp', + 'kennel', + 'kept', + 'kerchief', + 'kerosene', + 'kettle', + 'kick', + 'kiln', + 'kilobyte', + 'kilogram', + 'kilometer', + 'kilowatt', + 'kilt', + 'kimono', + 'kindle', + 'kindling', + 'kindly', + 'kindness', + 'kindred', + 'kinetic', + 'kinfolk', + 'king', + 'kinship', + 'kinsman', + 'kinswoman', + 'kissable', + 'kisser', + 'kissing', + 'kitchen', + 'kite', + 'kitten', + 'kitty', + 'kiwi', + 'kleenex', + 'knapsack', + 'knee', + 'knelt', + 'knickers', + 'knoll', + 'koala', + 'kooky', + 'kosher', + 'krypton', + 'kudos', + 'kung', + 'labored', + 'laborer', + 'laboring', + 'laborious', + 'labrador', + 'ladder', + 'ladies', + 'ladle', + 'ladybug', + 'ladylike', + 'lagged', + 'lagging', + 'lagoon', + 'lair', + 'lake', + 'lance', + 'landed', + 'landfall', + 'landfill', + 'landing', + 'landlady', + 'landless', + 'landline', + 'landlord', + 'landmark', + 'landmass', + 'landmine', + 'landowner', + 'landscape', + 'landside', + 'landslide', + 'language', + 'lankiness', + 'lanky', + 'lantern', + 'lapdog', + 'lapel', + 'lapped', + 'lapping', + 'laptop', + 'lard', + 'large', + 'lark', + 'lash', + 'lasso', + 'last', + 'latch', + 'late', + 'lather', + 'latitude', + 'latrine', + 'latter', + 'latticed', + 'launch', + 'launder', + 'laundry', + 'laurel', + 'lavender', + 'lavish', + 'laxative', + 'lazily', + 'laziness', + 'lazy', + 'lecturer', + 'left', + 'legacy', + 'legal', + 'legend', + 'legged', + 'leggings', + 'legible', + 'legibly', + 'legislate', + 'lego', + 'legroom', + 'legume', + 'legwarmer', + 'legwork', + 'lemon', + 'lend', + 'length', + 'lens', + 'lent', + 'leotard', + 'lesser', + 'letdown', + 'lethargic', + 'lethargy', + 'letter', + 'lettuce', + 'level', + 'leverage', + 'levers', + 'levitate', + 'levitator', + 'liability', + 'liable', + 'liberty', + 'librarian', + 'library', + 'licking', + 'licorice', + 'lid', + 'life', + 'lifter', + 'lifting', + 'liftoff', + 'ligament', + 'likely', + 'likeness', + 'likewise', + 'liking', + 'lilac', + 'lilly', + 'lily', + 'limb', + 'limeade', + 'limelight', + 'limes', + 'limit', + 'limping', + 'limpness', + 'line', + 'lingo', + 'linguini', + 'linguist', + 'lining', + 'linked', + 'linoleum', + 'linseed', + 'lint', + 'lion', + 'lip', + 'liquefy', + 'liqueur', + 'liquid', + 'lisp', + 'list', + 'litigate', + 'litigator', + 'litmus', + 'litter', + 'little', + 'livable', + 'lived', + 'lively', + 'liver', + 'livestock', + 'lividly', + 'living', + 'lizard', + 'lubricant', + 'lubricate', + 'lucid', + 'luckily', + 'luckiness', + 'luckless', + 'lucrative', + 'ludicrous', + 'lugged', + 'lukewarm', + 'lullaby', + 'lumber', + 'luminance', + 'luminous', + 'lumpiness', + 'lumping', + 'lumpish', + 'lunacy', + 'lunar', + 'lunchbox', + 'luncheon', + 'lunchroom', + 'lunchtime', + 'lung', + 'lurch', + 'lure', + 'luridness', + 'lurk', + 'lushly', + 'lushness', + 'luster', + 'lustfully', + 'lustily', + 'lustiness', + 'lustrous', + 'lusty', + 'luxurious', + 'luxury', + 'lying', + 'lyrically', + 'lyricism', + 'lyricist', + 'lyrics', + 'macarena', + 'macaroni', + 'macaw', + 'mace', + 'machine', + 'machinist', + 'magazine', + 'magenta', + 'maggot', + 'magical', + 'magician', + 'magma', + 'magnesium', + 'magnetic', + 'magnetism', + 'magnetize', + 'magnifier', + 'magnify', + 'magnitude', + 'magnolia', + 'mahogany', + 'maimed', + 'majestic', + 'majesty', + 'majorette', + 'majority', + 'makeover', + 'maker', + 'makeshift', + 'making', + 'malformed', + 'malt', + 'mama', + 'mammal', + 'mammary', + 'mammogram', + 'manager', + 'managing', + 'manatee', + 'mandarin', + 'mandate', + 'mandatory', + 'mandolin', + 'manger', + 'mangle', + 'mango', + 'mangy', + 'manhandle', + 'manhole', + 'manhood', + 'manhunt', + 'manicotti', + 'manicure', + 'manifesto', + 'manila', + 'mankind', + 'manlike', + 'manliness', + 'manly', + 'manmade', + 'manned', + 'mannish', + 'manor', + 'manpower', + 'mantis', + 'mantra', + 'manual', + 'many', + 'map', + 'marathon', + 'marauding', + 'marbled', + 'marbles', + 'marbling', + 'march', + 'mardi', + 'margarine', + 'margarita', + 'margin', + 'marigold', + 'marina', + 'marine', + 'marital', + 'maritime', + 'marlin', + 'marmalade', + 'maroon', + 'married', + 'marrow', + 'marry', + 'marshland', + 'marshy', + 'marsupial', + 'marvelous', + 'marxism', + 'mascot', + 'masculine', + 'mashed', + 'mashing', + 'massager', + 'masses', + 'massive', + 'mastiff', + 'matador', + 'matchbook', + 'matchbox', + 'matcher', + 'matching', + 'matchless', + 'material', + 'maternal', + 'maternity', + 'math', + 'mating', + 'matriarch', + 'matrimony', + 'matrix', + 'matron', + 'matted', + 'matter', + 'maturely', + 'maturing', + 'maturity', + 'mauve', + 'maverick', + 'maximize', + 'maximum', + 'maybe', + 'mayday', + 'mayflower', + 'moaner', + 'moaning', + 'mobile', + 'mobility', + 'mobilize', + 'mobster', + 'mocha', + 'mocker', + 'mockup', + 'modified', + 'modify', + 'modular', + 'modulator', + 'module', + 'moisten', + 'moistness', + 'moisture', + 'molar', + 'molasses', + 'mold', + 'molecular', + 'molecule', + 'molehill', + 'mollusk', + 'mom', + 'monastery', + 'monday', + 'monetary', + 'monetize', + 'moneybags', + 'moneyless', + 'moneywise', + 'mongoose', + 'mongrel', + 'monitor', + 'monkhood', + 'monogamy', + 'monogram', + 'monologue', + 'monopoly', + 'monorail', + 'monotone', + 'monotype', + 'monoxide', + 'monsieur', + 'monsoon', + 'monstrous', + 'monthly', + 'monument', + 'moocher', + 'moodiness', + 'moody', + 'mooing', + 'moonbeam', + 'mooned', + 'moonlight', + 'moonlike', + 'moonlit', + 'moonrise', + 'moonscape', + 'moonshine', + 'moonstone', + 'moonwalk', + 'mop', + 'morale', + 'morality', + 'morally', + 'morbidity', + 'morbidly', + 'morphine', + 'morphing', + 'morse', + 'mortality', + 'mortally', + 'mortician', + 'mortified', + 'mortify', + 'mortuary', + 'mosaic', + 'mossy', + 'most', + 'mothball', + 'mothproof', + 'motion', + 'motivate', + 'motivator', + 'motive', + 'motocross', + 'motor', + 'motto', + 'mountable', + 'mountain', + 'mounted', + 'mounting', + 'mourner', + 'mournful', + 'mouse', + 'mousiness', + 'moustache', + 'mousy', + 'mouth', + 'movable', + 'move', + 'movie', + 'moving', + 'mower', + 'mowing', + 'much', + 'muck', + 'mud', + 'mug', + 'mulberry', + 'mulch', + 'mule', + 'mulled', + 'mullets', + 'multiple', + 'multiply', + 'multitask', + 'multitude', + 'mumble', + 'mumbling', + 'mumbo', + 'mummified', + 'mummify', + 'mummy', + 'mumps', + 'munchkin', + 'mundane', + 'municipal', + 'muppet', + 'mural', + 'murkiness', + 'murky', + 'murmuring', + 'muscular', + 'museum', + 'mushily', + 'mushiness', + 'mushroom', + 'mushy', + 'music', + 'musket', + 'muskiness', + 'musky', + 'mustang', + 'mustard', + 'muster', + 'mustiness', + 'musty', + 'mutable', + 'mutate', + 'mutation', + 'mute', + 'mutilated', + 'mutilator', + 'mutiny', + 'mutt', + 'mutual', + 'muzzle', + 'myself', + 'myspace', + 'mystified', + 'mystify', + 'myth', + 'nacho', + 'nag', + 'nail', + 'name', + 'naming', + 'nanny', + 'nanometer', + 'nape', + 'napkin', + 'napped', + 'napping', + 'nappy', + 'narrow', + 'nastily', + 'nastiness', + 'national', + 'native', + 'nativity', + 'natural', + 'nature', + 'naturist', + 'nautical', + 'navigate', + 'navigator', + 'navy', + 'nearby', + 'nearest', + 'nearly', + 'nearness', + 'neatly', + 'neatness', + 'nebula', + 'nebulizer', + 'nectar', + 'negate', + 'negation', + 'negative', + 'neglector', + 'negligee', + 'negligent', + 'negotiate', + 'nemeses', + 'nemesis', + 'neon', + 'nephew', + 'nerd', + 'nervous', + 'nervy', + 'nest', + 'net', + 'neurology', + 'neuron', + 'neurosis', + 'neurotic', + 'neuter', + 'neutron', + 'never', + 'next', + 'nibble', + 'nickname', + 'nicotine', + 'niece', + 'nifty', + 'nimble', + 'nimbly', + 'nineteen', + 'ninetieth', + 'ninja', + 'nintendo', + 'ninth', + 'nuclear', + 'nuclei', + 'nucleus', + 'nugget', + 'nullify', + 'number', + 'numbing', + 'numbly', + 'numbness', + 'numeral', + 'numerate', + 'numerator', + 'numeric', + 'numerous', + 'nuptials', + 'nursery', + 'nursing', + 'nurture', + 'nutcase', + 'nutlike', + 'nutmeg', + 'nutrient', + 'nutshell', + 'nuttiness', + 'nutty', + 'nuzzle', + 'nylon', + 'oaf', + 'oak', + 'oasis', + 'oat', + 'obedience', + 'obedient', + 'obituary', + 'object', + 'obligate', + 'obliged', + 'oblivion', + 'oblivious', + 'oblong', + 'obnoxious', + 'oboe', + 'obscure', + 'obscurity', + 'observant', + 'observer', + 'observing', + 'obsessed', + 'obsession', + 'obsessive', + 'obsolete', + 'obstacle', + 'obstinate', + 'obstruct', + 'obtain', + 'obtrusive', + 'obtuse', + 'obvious', + 'occultist', + 'occupancy', + 'occupant', + 'occupier', + 'occupy', + 'ocean', + 'ocelot', + 'octagon', + 'octane', + 'october', + 'octopus', + 'ogle', + 'oil', + 'oink', + 'ointment', + 'okay', + 'old', + 'olive', + 'olympics', + 'omega', + 'omen', + 'ominous', + 'omission', + 'omit', + 'omnivore', + 'onboard', + 'oncoming', + 'ongoing', + 'onion', + 'online', + 'onlooker', + 'only', + 'onscreen', + 'onset', + 'onshore', + 'onslaught', + 'onstage', + 'onto', + 'onward', + 'onyx', + 'oops', + 'ooze', + 'oozy', + 'opacity', + 'opal', + 'open', + 'operable', + 'operate', + 'operating', + 'operation', + 'operative', + 'operator', + 'opium', + 'opossum', + 'opponent', + 'oppose', + 'opposing', + 'opposite', + 'oppressed', + 'oppressor', + 'opt', + 'opulently', + 'osmosis', + 'other', + 'otter', + 'ouch', + 'ought', + 'ounce', + 'outage', + 'outback', + 'outbid', + 'outboard', + 'outbound', + 'outbreak', + 'outburst', + 'outcast', + 'outclass', + 'outcome', + 'outdated', + 'outdoors', + 'outer', + 'outfield', + 'outfit', + 'outflank', + 'outgoing', + 'outgrow', + 'outhouse', + 'outing', + 'outlast', + 'outlet', + 'outline', + 'outlook', + 'outlying', + 'outmatch', + 'outmost', + 'outnumber', + 'outplayed', + 'outpost', + 'outpour', + 'output', + 'outrage', + 'outrank', + 'outreach', + 'outright', + 'outscore', + 'outsell', + 'outshine', + 'outshoot', + 'outsider', + 'outskirts', + 'outsmart', + 'outsource', + 'outspoken', + 'outtakes', + 'outthink', + 'outward', + 'outweigh', + 'outwit', + 'oval', + 'ovary', + 'oven', + 'overact', + 'overall', + 'overarch', + 'overbid', + 'overbill', + 'overbite', + 'overblown', + 'overboard', + 'overbook', + 'overbuilt', + 'overcast', + 'overcoat', + 'overcome', + 'overcook', + 'overcrowd', + 'overdraft', + 'overdrawn', + 'overdress', + 'overdrive', + 'overdue', + 'overeager', + 'overeater', + 'overexert', + 'overfed', + 'overfeed', + 'overfill', + 'overflow', + 'overfull', + 'overgrown', + 'overhand', + 'overhang', + 'overhaul', + 'overhead', + 'overhear', + 'overheat', + 'overhung', + 'overjoyed', + 'overkill', + 'overlabor', + 'overlaid', + 'overlap', + 'overlay', + 'overload', + 'overlook', + 'overlord', + 'overlying', + 'overnight', + 'overpass', + 'overpay', + 'overplant', + 'overplay', + 'overpower', + 'overprice', + 'overrate', + 'overreach', + 'overreact', + 'override', + 'overripe', + 'overrule', + 'overrun', + 'overshoot', + 'overshot', + 'oversight', + 'oversized', + 'oversleep', + 'oversold', + 'overspend', + 'overstate', + 'overstay', + 'overstep', + 'overstock', + 'overstuff', + 'oversweet', + 'overtake', + 'overthrow', + 'overtime', + 'overtly', + 'overtone', + 'overture', + 'overturn', + 'overuse', + 'overvalue', + 'overview', + 'overwrite', + 'owl', + 'oxford', + 'oxidant', + 'oxidation', + 'oxidize', + 'oxidizing', + 'oxygen', + 'oxymoron', + 'oyster', + 'ozone', + 'paced', + 'pacemaker', + 'pacific', + 'pacifier', + 'pacifism', + 'pacifist', + 'pacify', + 'padded', + 'padding', + 'paddle', + 'paddling', + 'padlock', + 'pagan', + 'pager', + 'paging', + 'pajamas', + 'palace', + 'palatable', + 'palm', + 'palpable', + 'palpitate', + 'paltry', + 'pampered', + 'pamperer', + 'pampers', + 'pamphlet', + 'panama', + 'pancake', + 'pancreas', + 'panda', + 'pandemic', + 'pang', + 'panhandle', + 'panic', + 'panning', + 'panorama', + 'panoramic', + 'panther', + 'pantomime', + 'pantry', + 'pants', + 'pantyhose', + 'paparazzi', + 'papaya', + 'paper', + 'paprika', + 'papyrus', + 'parabola', + 'parachute', + 'parade', + 'paradox', + 'paragraph', + 'parakeet', + 'paralegal', + 'paralyses', + 'paralysis', + 'paralyze', + 'paramedic', + 'parameter', + 'paramount', + 'parasail', + 'parasite', + 'parasitic', + 'parcel', + 'parched', + 'parchment', + 'pardon', + 'parish', + 'parka', + 'parking', + 'parkway', + 'parlor', + 'parmesan', + 'parole', + 'parrot', + 'parsley', + 'parsnip', + 'partake', + 'parted', + 'parting', + 'partition', + 'partly', + 'partner', + 'partridge', + 'party', + 'passable', + 'passably', + 'passage', + 'passcode', + 'passenger', + 'passerby', + 'passing', + 'passion', + 'passive', + 'passivism', + 'passover', + 'passport', + 'password', + 'pasta', + 'pasted', + 'pastel', + 'pastime', + 'pastor', + 'pastrami', + 'pasture', + 'pasty', + 'patchwork', + 'patchy', + 'paternal', + 'paternity', + 'path', + 'patience', + 'patient', + 'patio', + 'patriarch', + 'patriot', + 'patrol', + 'patronage', + 'patronize', + 'pauper', + 'pavement', + 'paver', + 'pavestone', + 'pavilion', + 'paving', + 'pawing', + 'payable', + 'payback', + 'paycheck', + 'payday', + 'payee', + 'payer', + 'paying', + 'payment', + 'payphone', + 'payroll', + 'pebble', + 'pebbly', + 'pecan', + 'pectin', + 'peculiar', + 'peddling', + 'pediatric', + 'pedicure', + 'pedigree', + 'pedometer', + 'pegboard', + 'pelican', + 'pellet', + 'pelt', + 'pelvis', + 'penalize', + 'penalty', + 'pencil', + 'pendant', + 'pending', + 'penholder', + 'penknife', + 'pennant', + 'penniless', + 'penny', + 'penpal', + 'pension', + 'pentagon', + 'pentagram', + 'pep', + 'perceive', + 'percent', + 'perch', + 'percolate', + 'perennial', + 'perfected', + 'perfectly', + 'perfume', + 'periscope', + 'perish', + 'perjurer', + 'perjury', + 'perkiness', + 'perky', + 'perm', + 'peroxide', + 'perpetual', + 'perplexed', + 'persecute', + 'persevere', + 'persuaded', + 'persuader', + 'pesky', + 'peso', + 'pessimism', + 'pessimist', + 'pester', + 'pesticide', + 'petal', + 'petite', + 'petition', + 'petri', + 'petroleum', + 'petted', + 'petticoat', + 'pettiness', + 'petty', + 'petunia', + 'phantom', + 'phobia', + 'phoenix', + 'phonebook', + 'phoney', + 'phonics', + 'phoniness', + 'phony', + 'phosphate', + 'photo', + 'phrase', + 'phrasing', + 'placard', + 'placate', + 'placidly', + 'plank', + 'planner', + 'plant', + 'plasma', + 'plaster', + 'plastic', + 'plated', + 'platform', + 'plating', + 'platinum', + 'platonic', + 'platter', + 'platypus', + 'plausible', + 'plausibly', + 'playable', + 'playback', + 'player', + 'playful', + 'playgroup', + 'playhouse', + 'playing', + 'playlist', + 'playmaker', + 'playmate', + 'playoff', + 'playpen', + 'playroom', + 'playset', + 'plaything', + 'playtime', + 'plaza', + 'pleading', + 'pleat', + 'pledge', + 'plentiful', + 'plenty', + 'plethora', + 'plexiglas', + 'pliable', + 'plod', + 'plop', + 'plot', + 'plow', + 'ploy', + 'pluck', + 'plug', + 'plunder', + 'plunging', + 'plural', + 'plus', + 'plutonium', + 'plywood', + 'poach', + 'pod', + 'poem', + 'poet', + 'pogo', + 'pointed', + 'pointer', + 'pointing', + 'pointless', + 'pointy', + 'poise', + 'poison', + 'poker', + 'poking', + 'polar', + 'police', + 'policy', + 'polio', + 'polish', + 'politely', + 'polka', + 'polo', + 'polyester', + 'polygon', + 'polygraph', + 'polymer', + 'poncho', + 'pond', + 'pony', + 'popcorn', + 'pope', + 'poplar', + 'popper', + 'poppy', + 'popsicle', + 'populace', + 'popular', + 'populate', + 'porcupine', + 'pork', + 'porous', + 'porridge', + 'portable', + 'portal', + 'portfolio', + 'porthole', + 'portion', + 'portly', + 'portside', + 'poser', + 'posh', + 'posing', + 'possible', + 'possibly', + 'possum', + 'postage', + 'postal', + 'postbox', + 'postcard', + 'posted', + 'poster', + 'posting', + 'postnasal', + 'posture', + 'postwar', + 'pouch', + 'pounce', + 'pouncing', + 'pound', + 'pouring', + 'pout', + 'powdered', + 'powdering', + 'powdery', + 'power', + 'powwow', + 'pox', + 'praising', + 'prance', + 'prancing', + 'pranker', + 'prankish', + 'prankster', + 'prayer', + 'praying', + 'preacher', + 'preaching', + 'preachy', + 'preamble', + 'precinct', + 'precise', + 'precision', + 'precook', + 'precut', + 'predator', + 'predefine', + 'predict', + 'preface', + 'prefix', + 'preflight', + 'preformed', + 'pregame', + 'pregnancy', + 'pregnant', + 'preheated', + 'prelaunch', + 'prelaw', + 'prelude', + 'premiere', + 'premises', + 'premium', + 'prenatal', + 'preoccupy', + 'preorder', + 'prepaid', + 'prepay', + 'preplan', + 'preppy', + 'preschool', + 'prescribe', + 'preseason', + 'preset', + 'preshow', + 'president', + 'presoak', + 'press', + 'presume', + 'presuming', + 'preteen', + 'pretended', + 'pretender', + 'pretense', + 'pretext', + 'pretty', + 'pretzel', + 'prevail', + 'prevalent', + 'prevent', + 'preview', + 'previous', + 'prewar', + 'prewashed', + 'prideful', + 'pried', + 'primal', + 'primarily', + 'primary', + 'primate', + 'primer', + 'primp', + 'princess', + 'print', + 'prior', + 'prism', + 'prison', + 'prissy', + 'pristine', + 'privacy', + 'private', + 'privatize', + 'prize', + 'proactive', + 'probable', + 'probably', + 'probation', + 'probe', + 'probing', + 'probiotic', + 'problem', + 'procedure', + 'process', + 'proclaim', + 'procreate', + 'procurer', + 'prodigal', + 'prodigy', + 'produce', + 'product', + 'profane', + 'profanity', + 'professed', + 'professor', + 'profile', + 'profound', + 'profusely', + 'progeny', + 'prognosis', + 'program', + 'progress', + 'projector', + 'prologue', + 'prolonged', + 'promenade', + 'prominent', + 'promoter', + 'promotion', + 'prompter', + 'promptly', + 'prone', + 'prong', + 'pronounce', + 'pronto', + 'proofing', + 'proofread', + 'proofs', + 'propeller', + 'properly', + 'property', + 'proponent', + 'proposal', + 'propose', + 'props', + 'prorate', + 'protector', + 'protegee', + 'proton', + 'prototype', + 'protozoan', + 'protract', + 'protrude', + 'proud', + 'provable', + 'proved', + 'proven', + 'provided', + 'provider', + 'providing', + 'province', + 'proving', + 'provoke', + 'provoking', + 'provolone', + 'prowess', + 'prowler', + 'prowling', + 'proximity', + 'proxy', + 'prozac', + 'prude', + 'prudishly', + 'prune', + 'pruning', + 'pry', + 'psychic', + 'public', + 'publisher', + 'pucker', + 'pueblo', + 'pug', + 'pull', + 'pulmonary', + 'pulp', + 'pulsate', + 'pulse', + 'pulverize', + 'puma', + 'pumice', + 'pummel', + 'punch', + 'punctual', + 'punctuate', + 'punctured', + 'pungent', + 'punisher', + 'punk', + 'pupil', + 'puppet', + 'puppy', + 'purchase', + 'pureblood', + 'purebred', + 'purely', + 'pureness', + 'purgatory', + 'purge', + 'purging', + 'purifier', + 'purify', + 'purist', + 'puritan', + 'purity', + 'purple', + 'purplish', + 'purposely', + 'purr', + 'purse', + 'pursuable', + 'pursuant', + 'pursuit', + 'purveyor', + 'pushcart', + 'pushchair', + 'pusher', + 'pushiness', + 'pushing', + 'pushover', + 'pushpin', + 'pushup', + 'pushy', + 'putdown', + 'putt', + 'puzzle', + 'puzzling', + 'pyramid', + 'pyromania', + 'python', + 'quack', + 'quadrant', + 'quail', + 'quaintly', + 'quake', + 'quaking', + 'qualified', + 'qualifier', + 'qualify', + 'quality', + 'qualm', + 'quantum', + 'quarrel', + 'quarry', + 'quartered', + 'quarterly', + 'quarters', + 'quartet', + 'quench', + 'query', + 'quicken', + 'quickly', + 'quickness', + 'quicksand', + 'quickstep', + 'quiet', + 'quill', + 'quilt', + 'quintet', + 'quintuple', + 'quirk', + 'quit', + 'quiver', + 'quizzical', + 'quotable', + 'quotation', + 'quote', + 'rabid', + 'race', + 'racing', + 'racism', + 'rack', + 'racoon', + 'radar', + 'radial', + 'radiance', + 'radiantly', + 'radiated', + 'radiation', + 'radiator', + 'radio', + 'radish', + 'raffle', + 'raft', + 'rage', + 'ragged', + 'raging', + 'ragweed', + 'raider', + 'railcar', + 'railing', + 'railroad', + 'railway', + 'raisin', + 'rake', + 'raking', + 'rally', + 'ramble', + 'rambling', + 'ramp', + 'ramrod', + 'ranch', + 'rancidity', + 'random', + 'ranged', + 'ranger', + 'ranging', + 'ranked', + 'ranking', + 'ransack', + 'ranting', + 'rants', + 'rare', + 'rarity', + 'rascal', + 'rash', + 'rasping', + 'ravage', + 'raven', + 'ravine', + 'raving', + 'ravioli', + 'ravishing', + 'reabsorb', + 'reach', + 'reacquire', + 'reaction', + 'reactive', + 'reactor', + 'reaffirm', + 'ream', + 'reanalyze', + 'reappear', + 'reapply', + 'reappoint', + 'reapprove', + 'rearrange', + 'rearview', + 'reason', + 'reassign', + 'reassure', + 'reattach', + 'reawake', + 'rebalance', + 'rebate', + 'rebel', + 'rebirth', + 'reboot', + 'reborn', + 'rebound', + 'rebuff', + 'rebuild', + 'rebuilt', + 'reburial', + 'rebuttal', + 'recall', + 'recant', + 'recapture', + 'recast', + 'recede', + 'recent', + 'recess', + 'recharger', + 'recipient', + 'recital', + 'recite', + 'reckless', + 'reclaim', + 'recliner', + 'reclining', + 'recluse', + 'reclusive', + 'recognize', + 'recoil', + 'recollect', + 'recolor', + 'reconcile', + 'reconfirm', + 'reconvene', + 'recopy', + 'record', + 'recount', + 'recoup', + 'recovery', + 'recreate', + 'rectal', + 'rectangle', + 'rectified', + 'rectify', + 'recycled', + 'recycler', + 'recycling', + 'reemerge', + 'reenact', + 'reenter', + 'reentry', + 'reexamine', + 'referable', + 'referee', + 'reference', + 'refill', + 'refinance', + 'refined', + 'refinery', + 'refining', + 'refinish', + 'reflected', + 'reflector', + 'reflex', + 'reflux', + 'refocus', + 'refold', + 'reforest', + 'reformat', + 'reformed', + 'reformer', + 'reformist', + 'refract', + 'refrain', + 'refreeze', + 'refresh', + 'refried', + 'refueling', + 'refund', + 'refurbish', + 'refurnish', + 'refusal', + 'refuse', + 'refusing', + 'refutable', + 'refute', + 'regain', + 'regalia', + 'regally', + 'reggae', + 'regime', + 'region', + 'register', + 'registrar', + 'registry', + 'regress', + 'regretful', + 'regroup', + 'regular', + 'regulate', + 'regulator', + 'rehab', + 'reheat', + 'rehire', + 'rehydrate', + 'reimburse', + 'reissue', + 'reiterate', + 'rejoice', + 'rejoicing', + 'rejoin', + 'rekindle', + 'relapse', + 'relapsing', + 'relatable', + 'related', + 'relation', + 'relative', + 'relax', + 'relay', + 'relearn', + 'release', + 'relenting', + 'reliable', + 'reliably', + 'reliance', + 'reliant', + 'relic', + 'relieve', + 'relieving', + 'relight', + 'relish', + 'relive', + 'reload', + 'relocate', + 'relock', + 'reluctant', + 'rely', + 'remake', + 'remark', + 'remarry', + 'rematch', + 'remedial', + 'remedy', + 'remember', + 'reminder', + 'remindful', + 'remission', + 'remix', + 'remnant', + 'remodeler', + 'remold', + 'remorse', + 'remote', + 'removable', + 'removal', + 'removed', + 'remover', + 'removing', + 'rename', + 'renderer', + 'rendering', + 'rendition', + 'renegade', + 'renewable', + 'renewably', + 'renewal', + 'renewed', + 'renounce', + 'renovate', + 'renovator', + 'rentable', + 'rental', + 'rented', + 'renter', + 'reoccupy', + 'reoccur', + 'reopen', + 'reorder', + 'repackage', + 'repacking', + 'repaint', + 'repair', + 'repave', + 'repaying', + 'repayment', + 'repeal', + 'repeated', + 'repeater', + 'repent', + 'rephrase', + 'replace', + 'replay', + 'replica', + 'reply', + 'reporter', + 'repose', + 'repossess', + 'repost', + 'repressed', + 'reprimand', + 'reprint', + 'reprise', + 'reproach', + 'reprocess', + 'reproduce', + 'reprogram', + 'reps', + 'reptile', + 'reptilian', + 'repugnant', + 'repulsion', + 'repulsive', + 'repurpose', + 'reputable', + 'reputably', + 'request', + 'require', + 'requisite', + 'reroute', + 'rerun', + 'resale', + 'resample', + 'rescuer', + 'reseal', + 'research', + 'reselect', + 'reseller', + 'resemble', + 'resend', + 'resent', + 'reset', + 'reshape', + 'reshoot', + 'reshuffle', + 'residence', + 'residency', + 'resident', + 'residual', + 'residue', + 'resigned', + 'resilient', + 'resistant', + 'resisting', + 'resize', + 'resolute', + 'resolved', + 'resonant', + 'resonate', + 'resort', + 'resource', + 'respect', + 'resubmit', + 'result', + 'resume', + 'resupply', + 'resurface', + 'resurrect', + 'retail', + 'retainer', + 'retaining', + 'retake', + 'retaliate', + 'retention', + 'rethink', + 'retinal', + 'retired', + 'retiree', + 'retiring', + 'retold', + 'retool', + 'retorted', + 'retouch', + 'retrace', + 'retract', + 'retrain', + 'retread', + 'retreat', + 'retrial', + 'retrieval', + 'retriever', + 'retry', + 'return', + 'retying', + 'retype', + 'reunion', + 'reunite', + 'reusable', + 'reuse', + 'reveal', + 'reveler', + 'revenge', + 'revenue', + 'reverb', + 'revered', + 'reverence', + 'reverend', + 'reversal', + 'reverse', + 'reversing', + 'reversion', + 'revert', + 'revisable', + 'revise', + 'revision', + 'revisit', + 'revivable', + 'revival', + 'reviver', + 'reviving', + 'revocable', + 'revoke', + 'revolt', + 'revolver', + 'revolving', + 'reward', + 'rewash', + 'rewind', + 'rewire', + 'reword', + 'rework', + 'rewrap', + 'rewrite', + 'rhyme', + 'ribbon', + 'ribcage', + 'rice', + 'riches', + 'richly', + 'richness', + 'rickety', + 'ricotta', + 'riddance', + 'ridden', + 'ride', + 'riding', + 'rifling', + 'rift', + 'rigging', + 'rigid', + 'rigor', + 'rimless', + 'rimmed', + 'rind', + 'rink', + 'rinse', + 'rinsing', + 'riot', + 'ripcord', + 'ripeness', + 'ripening', + 'ripping', + 'ripple', + 'rippling', + 'riptide', + 'rise', + 'rising', + 'risk', + 'risotto', + 'ritalin', + 'ritzy', + 'rival', + 'riverbank', + 'riverbed', + 'riverboat', + 'riverside', + 'riveter', + 'riveting', + 'roamer', + 'roaming', + 'roast', + 'robbing', + 'robe', + 'robin', + 'robotics', + 'robust', + 'rockband', + 'rocker', + 'rocket', + 'rockfish', + 'rockiness', + 'rocking', + 'rocklike', + 'rockslide', + 'rockstar', + 'rocky', + 'rogue', + 'roman', + 'romp', + 'rope', + 'roping', + 'roster', + 'rosy', + 'rotten', + 'rotting', + 'rotunda', + 'roulette', + 'rounding', + 'roundish', + 'roundness', + 'roundup', + 'roundworm', + 'routine', + 'routing', + 'rover', + 'roving', + 'royal', + 'rubbed', + 'rubber', + 'rubbing', + 'rubble', + 'rubdown', + 'ruby', + 'ruckus', + 'rudder', + 'rug', + 'ruined', + 'rule', + 'rumble', + 'rumbling', + 'rummage', + 'rumor', + 'runaround', + 'rundown', + 'runner', + 'running', + 'runny', + 'runt', + 'runway', + 'rupture', + 'rural', + 'ruse', + 'rush', + 'rust', + 'rut', + 'sabbath', + 'sabotage', + 'sacrament', + 'sacred', + 'sacrifice', + 'sadden', + 'saddlebag', + 'saddled', + 'saddling', + 'sadly', + 'sadness', + 'safari', + 'safeguard', + 'safehouse', + 'safely', + 'safeness', + 'saffron', + 'saga', + 'sage', + 'sagging', + 'saggy', + 'said', + 'saint', + 'sake', + 'salad', + 'salami', + 'salaried', + 'salary', + 'saline', + 'salon', + 'saloon', + 'salsa', + 'salt', + 'salutary', + 'salute', + 'salvage', + 'salvaging', + 'salvation', + 'same', + 'sample', + 'sampling', + 'sanction', + 'sanctity', + 'sanctuary', + 'sandal', + 'sandbag', + 'sandbank', + 'sandbar', + 'sandblast', + 'sandbox', + 'sanded', + 'sandfish', + 'sanding', + 'sandlot', + 'sandpaper', + 'sandpit', + 'sandstone', + 'sandstorm', + 'sandworm', + 'sandy', + 'sanitary', + 'sanitizer', + 'sank', + 'santa', + 'sapling', + 'sappiness', + 'sappy', + 'sarcasm', + 'sarcastic', + 'sardine', + 'sash', + 'sasquatch', + 'sassy', + 'satchel', + 'satiable', + 'satin', + 'satirical', + 'satisfied', + 'satisfy', + 'saturate', + 'saturday', + 'sauciness', + 'saucy', + 'sauna', + 'savage', + 'savanna', + 'saved', + 'savings', + 'savior', + 'savor', + 'saxophone', + 'say', + 'scabbed', + 'scabby', + 'scalded', + 'scalding', + 'scale', + 'scaling', + 'scallion', + 'scallop', + 'scalping', + 'scam', + 'scandal', + 'scanner', + 'scanning', + 'scant', + 'scapegoat', + 'scarce', + 'scarcity', + 'scarecrow', + 'scared', + 'scarf', + 'scarily', + 'scariness', + 'scarring', + 'scary', + 'scavenger', + 'scenic', + 'schedule', + 'schematic', + 'scheme', + 'scheming', + 'schilling', + 'schnapps', + 'scholar', + 'science', + 'scientist', + 'scion', + 'scoff', + 'scolding', + 'scone', + 'scoop', + 'scooter', + 'scope', + 'scorch', + 'scorebook', + 'scorecard', + 'scored', + 'scoreless', + 'scorer', + 'scoring', + 'scorn', + 'scorpion', + 'scotch', + 'scoundrel', + 'scoured', + 'scouring', + 'scouting', + 'scouts', + 'scowling', + 'scrabble', + 'scraggly', + 'scrambled', + 'scrambler', + 'scrap', + 'scratch', + 'scrawny', + 'screen', + 'scribble', + 'scribe', + 'scribing', + 'scrimmage', + 'script', + 'scroll', + 'scrooge', + 'scrounger', + 'scrubbed', + 'scrubber', + 'scruffy', + 'scrunch', + 'scrutiny', + 'scuba', + 'scuff', + 'sculptor', + 'sculpture', + 'scurvy', + 'scuttle', + 'secluded', + 'secluding', + 'seclusion', + 'second', + 'secrecy', + 'secret', + 'sectional', + 'sector', + 'secular', + 'securely', + 'security', + 'sedan', + 'sedate', + 'sedation', + 'sedative', + 'sediment', + 'seduce', + 'seducing', + 'segment', + 'seismic', + 'seizing', + 'seldom', + 'selected', + 'selection', + 'selective', + 'selector', + 'self', + 'seltzer', + 'semantic', + 'semester', + 'semicolon', + 'semifinal', + 'seminar', + 'semisoft', + 'semisweet', + 'senate', + 'senator', + 'send', + 'senior', + 'senorita', + 'sensation', + 'sensitive', + 'sensitize', + 'sensually', + 'sensuous', + 'sepia', + 'september', + 'septic', + 'septum', + 'sequel', + 'sequence', + 'sequester', + 'series', + 'sermon', + 'serotonin', + 'serpent', + 'serrated', + 'serve', + 'service', + 'serving', + 'sesame', + 'sessions', + 'setback', + 'setting', + 'settle', + 'settling', + 'setup', + 'sevenfold', + 'seventeen', + 'seventh', + 'seventy', + 'severity', + 'shabby', + 'shack', + 'shaded', + 'shadily', + 'shadiness', + 'shading', + 'shadow', + 'shady', + 'shaft', + 'shakable', + 'shakily', + 'shakiness', + 'shaking', + 'shaky', + 'shale', + 'shallot', + 'shallow', + 'shame', + 'shampoo', + 'shamrock', + 'shank', + 'shanty', + 'shape', + 'shaping', + 'share', + 'sharpener', + 'sharper', + 'sharpie', + 'sharply', + 'sharpness', + 'shawl', + 'sheath', + 'shed', + 'sheep', + 'sheet', + 'shelf', + 'shell', + 'shelter', + 'shelve', + 'shelving', + 'sherry', + 'shield', + 'shifter', + 'shifting', + 'shiftless', + 'shifty', + 'shimmer', + 'shimmy', + 'shindig', + 'shine', + 'shingle', + 'shininess', + 'shining', + 'shiny', + 'ship', + 'shirt', + 'shivering', + 'shock', + 'shone', + 'shoplift', + 'shopper', + 'shopping', + 'shoptalk', + 'shore', + 'shortage', + 'shortcake', + 'shortcut', + 'shorten', + 'shorter', + 'shorthand', + 'shortlist', + 'shortly', + 'shortness', + 'shorts', + 'shortwave', + 'shorty', + 'shout', + 'shove', + 'showbiz', + 'showcase', + 'showdown', + 'shower', + 'showgirl', + 'showing', + 'showman', + 'shown', + 'showoff', + 'showpiece', + 'showplace', + 'showroom', + 'showy', + 'shrank', + 'shrapnel', + 'shredder', + 'shredding', + 'shrewdly', + 'shriek', + 'shrill', + 'shrimp', + 'shrine', + 'shrink', + 'shrivel', + 'shrouded', + 'shrubbery', + 'shrubs', + 'shrug', + 'shrunk', + 'shucking', + 'shudder', + 'shuffle', + 'shuffling', + 'shun', + 'shush', + 'shut', + 'shy', + 'siamese', + 'siberian', + 'sibling', + 'siding', + 'sierra', + 'siesta', + 'sift', + 'sighing', + 'silenced', + 'silencer', + 'silent', + 'silica', + 'silicon', + 'silk', + 'silliness', + 'silly', + 'silo', + 'silt', + 'silver', + 'similarly', + 'simile', + 'simmering', + 'simple', + 'simplify', + 'simply', + 'sincere', + 'sincerity', + 'singer', + 'singing', + 'single', + 'singular', + 'sinister', + 'sinless', + 'sinner', + 'sinuous', + 'sip', + 'siren', + 'sister', + 'sitcom', + 'sitter', + 'sitting', + 'situated', + 'situation', + 'sixfold', + 'sixteen', + 'sixth', + 'sixties', + 'sixtieth', + 'sixtyfold', + 'sizable', + 'sizably', + 'size', + 'sizing', + 'sizzle', + 'sizzling', + 'skater', + 'skating', + 'skedaddle', + 'skeletal', + 'skeleton', + 'skeptic', + 'sketch', + 'skewed', + 'skewer', + 'skid', + 'skied', + 'skier', + 'skies', + 'skiing', + 'skilled', + 'skillet', + 'skillful', + 'skimmed', + 'skimmer', + 'skimming', + 'skimpily', + 'skincare', + 'skinhead', + 'skinless', + 'skinning', + 'skinny', + 'skintight', + 'skipper', + 'skipping', + 'skirmish', + 'skirt', + 'skittle', + 'skydiver', + 'skylight', + 'skyline', + 'skype', + 'skyrocket', + 'skyward', + 'slab', + 'slacked', + 'slacker', + 'slacking', + 'slackness', + 'slacks', + 'slain', + 'slam', + 'slander', + 'slang', + 'slapping', + 'slapstick', + 'slashed', + 'slashing', + 'slate', + 'slather', + 'slaw', + 'sled', + 'sleek', + 'sleep', + 'sleet', + 'sleeve', + 'slept', + 'sliceable', + 'sliced', + 'slicer', + 'slicing', + 'slick', + 'slider', + 'slideshow', + 'sliding', + 'slighted', + 'slighting', + 'slightly', + 'slimness', + 'slimy', + 'slinging', + 'slingshot', + 'slinky', + 'slip', + 'slit', + 'sliver', + 'slobbery', + 'slogan', + 'sloped', + 'sloping', + 'sloppily', + 'sloppy', + 'slot', + 'slouching', + 'slouchy', + 'sludge', + 'slug', + 'slum', + 'slurp', + 'slush', + 'sly', + 'small', + 'smartly', + 'smartness', + 'smasher', + 'smashing', + 'smashup', + 'smell', + 'smelting', + 'smile', + 'smilingly', + 'smirk', + 'smite', + 'smith', + 'smitten', + 'smock', + 'smog', + 'smoked', + 'smokeless', + 'smokiness', + 'smoking', + 'smoky', + 'smolder', + 'smooth', + 'smother', + 'smudge', + 'smudgy', + 'smuggler', + 'smuggling', + 'smugly', + 'smugness', + 'snack', + 'snagged', + 'snaking', + 'snap', + 'snare', + 'snarl', + 'snazzy', + 'sneak', + 'sneer', + 'sneeze', + 'sneezing', + 'snide', + 'sniff', + 'snippet', + 'snipping', + 'snitch', + 'snooper', + 'snooze', + 'snore', + 'snoring', + 'snorkel', + 'snort', + 'snout', + 'snowbird', + 'snowboard', + 'snowbound', + 'snowcap', + 'snowdrift', + 'snowdrop', + 'snowfall', + 'snowfield', + 'snowflake', + 'snowiness', + 'snowless', + 'snowman', + 'snowplow', + 'snowshoe', + 'snowstorm', + 'snowsuit', + 'snowy', + 'snub', + 'snuff', + 'snuggle', + 'snugly', + 'snugness', + 'speak', + 'spearfish', + 'spearhead', + 'spearman', + 'spearmint', + 'species', + 'specimen', + 'specked', + 'speckled', + 'specks', + 'spectacle', + 'spectator', + 'spectrum', + 'speculate', + 'speech', + 'speed', + 'spellbind', + 'speller', + 'spelling', + 'spendable', + 'spender', + 'spending', + 'spent', + 'spew', + 'sphere', + 'spherical', + 'sphinx', + 'spider', + 'spied', + 'spiffy', + 'spill', + 'spilt', + 'spinach', + 'spinal', + 'spindle', + 'spinner', + 'spinning', + 'spinout', + 'spinster', + 'spiny', + 'spiral', + 'spirited', + 'spiritism', + 'spirits', + 'spiritual', + 'splashed', + 'splashing', + 'splashy', + 'splatter', + 'spleen', + 'splendid', + 'splendor', + 'splice', + 'splicing', + 'splinter', + 'splotchy', + 'splurge', + 'spoilage', + 'spoiled', + 'spoiler', + 'spoiling', + 'spoils', + 'spoken', + 'spokesman', + 'sponge', + 'spongy', + 'sponsor', + 'spoof', + 'spookily', + 'spooky', + 'spool', + 'spoon', + 'spore', + 'sporting', + 'sports', + 'sporty', + 'spotless', + 'spotlight', + 'spotted', + 'spotter', + 'spotting', + 'spotty', + 'spousal', + 'spouse', + 'spout', + 'sprain', + 'sprang', + 'sprawl', + 'spray', + 'spree', + 'sprig', + 'spring', + 'sprinkled', + 'sprinkler', + 'sprint', + 'sprite', + 'sprout', + 'spruce', + 'sprung', + 'spry', + 'spud', + 'spur', + 'sputter', + 'spyglass', + 'squabble', + 'squad', + 'squall', + 'squander', + 'squash', + 'squatted', + 'squatter', + 'squatting', + 'squeak', + 'squealer', + 'squealing', + 'squeamish', + 'squeegee', + 'squeeze', + 'squeezing', + 'squid', + 'squiggle', + 'squiggly', + 'squint', + 'squire', + 'squirt', + 'squishier', + 'squishy', + 'stability', + 'stabilize', + 'stable', + 'stack', + 'stadium', + 'staff', + 'stage', + 'staging', + 'stagnant', + 'stagnate', + 'stainable', + 'stained', + 'staining', + 'stainless', + 'stalemate', + 'staleness', + 'stalling', + 'stallion', + 'stamina', + 'stammer', + 'stamp', + 'stand', + 'stank', + 'staple', + 'stapling', + 'starboard', + 'starch', + 'stardom', + 'stardust', + 'starfish', + 'stargazer', + 'staring', + 'stark', + 'starless', + 'starlet', + 'starlight', + 'starlit', + 'starring', + 'starry', + 'starship', + 'starter', + 'starting', + 'startle', + 'startling', + 'startup', + 'starved', + 'starving', + 'stash', + 'state', + 'static', + 'statistic', + 'statue', + 'stature', + 'status', + 'statute', + 'statutory', + 'staunch', + 'stays', + 'steadfast', + 'steadier', + 'steadily', + 'steadying', + 'steam', + 'steed', + 'steep', + 'steerable', + 'steering', + 'steersman', + 'stegosaur', + 'stellar', + 'stem', + 'stench', + 'stencil', + 'step', + 'stereo', + 'sterile', + 'sterility', + 'sterilize', + 'sterling', + 'sternness', + 'sternum', + 'stew', + 'stick', + 'stiffen', + 'stiffly', + 'stiffness', + 'stifle', + 'stifling', + 'stillness', + 'stilt', + 'stimulant', + 'stimulate', + 'stimuli', + 'stimulus', + 'stinger', + 'stingily', + 'stinging', + 'stingray', + 'stingy', + 'stinking', + 'stinky', + 'stipend', + 'stipulate', + 'stir', + 'stitch', + 'stock', + 'stoic', + 'stoke', + 'stole', + 'stomp', + 'stonewall', + 'stoneware', + 'stonework', + 'stoning', + 'stony', + 'stood', + 'stooge', + 'stool', + 'stoop', + 'stoplight', + 'stoppable', + 'stoppage', + 'stopped', + 'stopper', + 'stopping', + 'stopwatch', + 'storable', + 'storage', + 'storeroom', + 'storewide', + 'storm', + 'stout', + 'stove', + 'stowaway', + 'stowing', + 'straddle', + 'straggler', + 'strained', + 'strainer', + 'straining', + 'strangely', + 'stranger', + 'strangle', + 'strategic', + 'strategy', + 'stratus', + 'straw', + 'stray', + 'streak', + 'stream', + 'street', + 'strength', + 'strenuous', + 'strep', + 'stress', + 'stretch', + 'strewn', + 'stricken', + 'strict', + 'stride', + 'strife', + 'strike', + 'striking', + 'strive', + 'striving', + 'strobe', + 'strode', + 'stroller', + 'strongbox', + 'strongly', + 'strongman', + 'struck', + 'structure', + 'strudel', + 'struggle', + 'strum', + 'strung', + 'strut', + 'stubbed', + 'stubble', + 'stubbly', + 'stubborn', + 'stucco', + 'stuck', + 'student', + 'studied', + 'studio', + 'study', + 'stuffed', + 'stuffing', + 'stuffy', + 'stumble', + 'stumbling', + 'stump', + 'stung', + 'stunned', + 'stunner', + 'stunning', + 'stunt', + 'stupor', + 'sturdily', + 'sturdy', + 'styling', + 'stylishly', + 'stylist', + 'stylized', + 'stylus', + 'suave', + 'subarctic', + 'subatomic', + 'subdivide', + 'subdued', + 'subduing', + 'subfloor', + 'subgroup', + 'subheader', + 'subject', + 'sublease', + 'sublet', + 'sublevel', + 'sublime', + 'submarine', + 'submerge', + 'submersed', + 'submitter', + 'subpanel', + 'subpar', + 'subplot', + 'subprime', + 'subscribe', + 'subscript', + 'subsector', + 'subside', + 'subsiding', + 'subsidize', + 'subsidy', + 'subsoil', + 'subsonic', + 'substance', + 'subsystem', + 'subtext', + 'subtitle', + 'subtly', + 'subtotal', + 'subtract', + 'subtype', + 'suburb', + 'subway', + 'subwoofer', + 'subzero', + 'succulent', + 'such', + 'suction', + 'sudden', + 'sudoku', + 'suds', + 'sufferer', + 'suffering', + 'suffice', + 'suffix', + 'suffocate', + 'suffrage', + 'sugar', + 'suggest', + 'suing', + 'suitable', + 'suitably', + 'suitcase', + 'suitor', + 'sulfate', + 'sulfide', + 'sulfite', + 'sulfur', + 'sulk', + 'sullen', + 'sulphate', + 'sulphuric', + 'sultry', + 'superbowl', + 'superglue', + 'superhero', + 'superior', + 'superjet', + 'superman', + 'supermom', + 'supernova', + 'supervise', + 'supper', + 'supplier', + 'supply', + 'support', + 'supremacy', + 'supreme', + 'surcharge', + 'surely', + 'sureness', + 'surface', + 'surfacing', + 'surfboard', + 'surfer', + 'surgery', + 'surgical', + 'surging', + 'surname', + 'surpass', + 'surplus', + 'surprise', + 'surreal', + 'surrender', + 'surrogate', + 'surround', + 'survey', + 'survival', + 'survive', + 'surviving', + 'survivor', + 'sushi', + 'suspect', + 'suspend', + 'suspense', + 'sustained', + 'sustainer', + 'swab', + 'swaddling', + 'swagger', + 'swampland', + 'swan', + 'swapping', + 'swarm', + 'sway', + 'swear', + 'sweat', + 'sweep', + 'swell', + 'swept', + 'swerve', + 'swifter', + 'swiftly', + 'swiftness', + 'swimmable', + 'swimmer', + 'swimming', + 'swimsuit', + 'swimwear', + 'swinger', + 'swinging', + 'swipe', + 'swirl', + 'switch', + 'swivel', + 'swizzle', + 'swooned', + 'swoop', + 'swoosh', + 'swore', + 'sworn', + 'swung', + 'sycamore', + 'sympathy', + 'symphonic', + 'symphony', + 'symptom', + 'synapse', + 'syndrome', + 'synergy', + 'synopses', + 'synopsis', + 'synthesis', + 'synthetic', + 'syrup', + 'system', + 't-shirt', + 'tabasco', + 'tabby', + 'tableful', + 'tables', + 'tablet', + 'tableware', + 'tabloid', + 'tackiness', + 'tacking', + 'tackle', + 'tackling', + 'tacky', + 'taco', + 'tactful', + 'tactical', + 'tactics', + 'tactile', + 'tactless', + 'tadpole', + 'taekwondo', + 'tag', + 'tainted', + 'take', + 'taking', + 'talcum', + 'talisman', + 'tall', + 'talon', + 'tamale', + 'tameness', + 'tamer', + 'tamper', + 'tank', + 'tanned', + 'tannery', + 'tanning', + 'tantrum', + 'tapeless', + 'tapered', + 'tapering', + 'tapestry', + 'tapioca', + 'tapping', + 'taps', + 'tarantula', + 'target', + 'tarmac', + 'tarnish', + 'tarot', + 'tartar', + 'tartly', + 'tartness', + 'task', + 'tassel', + 'taste', + 'tastiness', + 'tasting', + 'tasty', + 'tattered', + 'tattle', + 'tattling', + 'tattoo', + 'taunt', + 'tavern', + 'thank', + 'that', + 'thaw', + 'theater', + 'theatrics', + 'thee', + 'theft', + 'theme', + 'theology', + 'theorize', + 'thermal', + 'thermos', + 'thesaurus', + 'these', + 'thesis', + 'thespian', + 'thicken', + 'thicket', + 'thickness', + 'thieving', + 'thievish', + 'thigh', + 'thimble', + 'thing', + 'think', + 'thinly', + 'thinner', + 'thinness', + 'thinning', + 'thirstily', + 'thirsting', + 'thirsty', + 'thirteen', + 'thirty', + 'thong', + 'thorn', + 'those', + 'thousand', + 'thrash', + 'thread', + 'threaten', + 'threefold', + 'thrift', + 'thrill', + 'thrive', + 'thriving', + 'throat', + 'throbbing', + 'throng', + 'throttle', + 'throwaway', + 'throwback', + 'thrower', + 'throwing', + 'thud', + 'thumb', + 'thumping', + 'thursday', + 'thus', + 'thwarting', + 'thyself', + 'tiara', + 'tibia', + 'tidal', + 'tidbit', + 'tidiness', + 'tidings', + 'tidy', + 'tiger', + 'tighten', + 'tightly', + 'tightness', + 'tightrope', + 'tightwad', + 'tigress', + 'tile', + 'tiling', + 'till', + 'tilt', + 'timid', + 'timing', + 'timothy', + 'tinderbox', + 'tinfoil', + 'tingle', + 'tingling', + 'tingly', + 'tinker', + 'tinkling', + 'tinsel', + 'tinsmith', + 'tint', + 'tinwork', + 'tiny', + 'tipoff', + 'tipped', + 'tipper', + 'tipping', + 'tiptoeing', + 'tiptop', + 'tiring', + 'tissue', + 'trace', + 'tracing', + 'track', + 'traction', + 'tractor', + 'trade', + 'trading', + 'tradition', + 'traffic', + 'tragedy', + 'trailing', + 'trailside', + 'train', + 'traitor', + 'trance', + 'tranquil', + 'transfer', + 'transform', + 'translate', + 'transpire', + 'transport', + 'transpose', + 'trapdoor', + 'trapeze', + 'trapezoid', + 'trapped', + 'trapper', + 'trapping', + 'traps', + 'trash', + 'travel', + 'traverse', + 'travesty', + 'tray', + 'treachery', + 'treading', + 'treadmill', + 'treason', + 'treat', + 'treble', + 'tree', + 'trekker', + 'tremble', + 'trembling', + 'tremor', + 'trench', + 'trend', + 'trespass', + 'triage', + 'trial', + 'triangle', + 'tribesman', + 'tribunal', + 'tribune', + 'tributary', + 'tribute', + 'triceps', + 'trickery', + 'trickily', + 'tricking', + 'trickle', + 'trickster', + 'tricky', + 'tricolor', + 'tricycle', + 'trident', + 'tried', + 'trifle', + 'trifocals', + 'trillion', + 'trilogy', + 'trimester', + 'trimmer', + 'trimming', + 'trimness', + 'trinity', + 'trio', + 'tripod', + 'tripping', + 'triumph', + 'trivial', + 'trodden', + 'trolling', + 'trombone', + 'trophy', + 'tropical', + 'tropics', + 'trouble', + 'troubling', + 'trough', + 'trousers', + 'trout', + 'trowel', + 'truce', + 'truck', + 'truffle', + 'trump', + 'trunks', + 'trustable', + 'trustee', + 'trustful', + 'trusting', + 'trustless', + 'truth', + 'try', + 'tubby', + 'tubeless', + 'tubular', + 'tucking', + 'tuesday', + 'tug', + 'tuition', + 'tulip', + 'tumble', + 'tumbling', + 'tummy', + 'turban', + 'turbine', + 'turbofan', + 'turbojet', + 'turbulent', + 'turf', + 'turkey', + 'turmoil', + 'turret', + 'turtle', + 'tusk', + 'tutor', + 'tutu', + 'tux', + 'tweak', + 'tweed', + 'tweet', + 'tweezers', + 'twelve', + 'twentieth', + 'twenty', + 'twerp', + 'twice', + 'twiddle', + 'twiddling', + 'twig', + 'twilight', + 'twine', + 'twins', + 'twirl', + 'twistable', + 'twisted', + 'twister', + 'twisting', + 'twisty', + 'twitch', + 'twitter', + 'tycoon', + 'tying', + 'tyke', + 'udder', + 'ultimate', + 'ultimatum', + 'ultra', + 'umbilical', + 'umbrella', + 'umpire', + 'unabashed', + 'unable', + 'unadorned', + 'unadvised', + 'unafraid', + 'unaired', + 'unaligned', + 'unaltered', + 'unarmored', + 'unashamed', + 'unaudited', + 'unawake', + 'unaware', + 'unbaked', + 'unbalance', + 'unbeaten', + 'unbend', + 'unbent', + 'unbiased', + 'unbitten', + 'unblended', + 'unblessed', + 'unblock', + 'unbolted', + 'unbounded', + 'unboxed', + 'unbraided', + 'unbridle', + 'unbroken', + 'unbuckled', + 'unbundle', + 'unburned', + 'unbutton', + 'uncanny', + 'uncapped', + 'uncaring', + 'uncertain', + 'unchain', + 'unchanged', + 'uncharted', + 'uncheck', + 'uncivil', + 'unclad', + 'unclaimed', + 'unclamped', + 'unclasp', + 'uncle', + 'unclip', + 'uncloak', + 'unclog', + 'unclothed', + 'uncoated', + 'uncoiled', + 'uncolored', + 'uncombed', + 'uncommon', + 'uncooked', + 'uncork', + 'uncorrupt', + 'uncounted', + 'uncouple', + 'uncouth', + 'uncover', + 'uncross', + 'uncrown', + 'uncrushed', + 'uncured', + 'uncurious', + 'uncurled', + 'uncut', + 'undamaged', + 'undated', + 'undaunted', + 'undead', + 'undecided', + 'undefined', + 'underage', + 'underarm', + 'undercoat', + 'undercook', + 'undercut', + 'underdog', + 'underdone', + 'underfed', + 'underfeed', + 'underfoot', + 'undergo', + 'undergrad', + 'underhand', + 'underline', + 'underling', + 'undermine', + 'undermost', + 'underpaid', + 'underpass', + 'underpay', + 'underrate', + 'undertake', + 'undertone', + 'undertook', + 'undertow', + 'underuse', + 'underwear', + 'underwent', + 'underwire', + 'undesired', + 'undiluted', + 'undivided', + 'undocked', + 'undoing', + 'undone', + 'undrafted', + 'undress', + 'undrilled', + 'undusted', + 'undying', + 'unearned', + 'unearth', + 'unease', + 'uneasily', + 'uneasy', + 'uneatable', + 'uneaten', + 'unedited', + 'unelected', + 'unending', + 'unengaged', + 'unenvied', + 'unequal', + 'unethical', + 'uneven', + 'unexpired', + 'unexposed', + 'unfailing', + 'unfair', + 'unfasten', + 'unfazed', + 'unfeeling', + 'unfiled', + 'unfilled', + 'unfitted', + 'unfitting', + 'unfixable', + 'unfixed', + 'unflawed', + 'unfocused', + 'unfold', + 'unfounded', + 'unframed', + 'unfreeze', + 'unfrosted', + 'unfrozen', + 'unfunded', + 'unglazed', + 'ungloved', + 'unglue', + 'ungodly', + 'ungraded', + 'ungreased', + 'unguarded', + 'unguided', + 'unhappily', + 'unhappy', + 'unharmed', + 'unhealthy', + 'unheard', + 'unhearing', + 'unheated', + 'unhelpful', + 'unhidden', + 'unhinge', + 'unhitched', + 'unholy', + 'unhook', + 'unicorn', + 'unicycle', + 'unified', + 'unifier', + 'uniformed', + 'uniformly', + 'unify', + 'unimpeded', + 'uninjured', + 'uninstall', + 'uninsured', + 'uninvited', + 'union', + 'uniquely', + 'unisexual', + 'unison', + 'unissued', + 'unit', + 'universal', + 'universe', + 'unjustly', + 'unkempt', + 'unkind', + 'unknotted', + 'unknowing', + 'unknown', + 'unlaced', + 'unlatch', + 'unlawful', + 'unleaded', + 'unlearned', + 'unleash', + 'unless', + 'unleveled', + 'unlighted', + 'unlikable', + 'unlimited', + 'unlined', + 'unlinked', + 'unlisted', + 'unlit', + 'unlivable', + 'unloaded', + 'unloader', + 'unlocked', + 'unlocking', + 'unlovable', + 'unloved', + 'unlovely', + 'unloving', + 'unluckily', + 'unlucky', + 'unmade', + 'unmanaged', + 'unmanned', + 'unmapped', + 'unmarked', + 'unmasked', + 'unmasking', + 'unmatched', + 'unmindful', + 'unmixable', + 'unmixed', + 'unmolded', + 'unmoral', + 'unmovable', + 'unmoved', + 'unmoving', + 'unnamable', + 'unnamed', + 'unnatural', + 'unneeded', + 'unnerve', + 'unnerving', + 'unnoticed', + 'unopened', + 'unopposed', + 'unpack', + 'unpadded', + 'unpaid', + 'unpainted', + 'unpaired', + 'unpaved', + 'unpeeled', + 'unpicked', + 'unpiloted', + 'unpinned', + 'unplanned', + 'unplanted', + 'unpleased', + 'unpledged', + 'unplowed', + 'unplug', + 'unpopular', + 'unproven', + 'unquote', + 'unranked', + 'unrated', + 'unraveled', + 'unreached', + 'unread', + 'unreal', + 'unreeling', + 'unrefined', + 'unrelated', + 'unrented', + 'unrest', + 'unretired', + 'unrevised', + 'unrigged', + 'unripe', + 'unrivaled', + 'unroasted', + 'unrobed', + 'unroll', + 'unruffled', + 'unruly', + 'unrushed', + 'unsaddle', + 'unsafe', + 'unsaid', + 'unsalted', + 'unsaved', + 'unsavory', + 'unscathed', + 'unscented', + 'unscrew', + 'unsealed', + 'unseated', + 'unsecured', + 'unseeing', + 'unseemly', + 'unseen', + 'unselect', + 'unselfish', + 'unsent', + 'unsettled', + 'unshackle', + 'unshaken', + 'unshaved', + 'unshaven', + 'unsheathe', + 'unshipped', + 'unsightly', + 'unsigned', + 'unskilled', + 'unsliced', + 'unsmooth', + 'unsnap', + 'unsocial', + 'unsoiled', + 'unsold', + 'unsolved', + 'unsorted', + 'unspoiled', + 'unspoken', + 'unstable', + 'unstaffed', + 'unstamped', + 'unsteady', + 'unsterile', + 'unstirred', + 'unstitch', + 'unstopped', + 'unstuck', + 'unstuffed', + 'unstylish', + 'unsubtle', + 'unsubtly', + 'unsuited', + 'unsure', + 'unsworn', + 'untagged', + 'untainted', + 'untaken', + 'untamed', + 'untangled', + 'untapped', + 'untaxed', + 'unthawed', + 'unthread', + 'untidy', + 'untie', + 'until', + 'untimed', + 'untimely', + 'untitled', + 'untoasted', + 'untold', + 'untouched', + 'untracked', + 'untrained', + 'untreated', + 'untried', + 'untrimmed', + 'untrue', + 'untruth', + 'unturned', + 'untwist', + 'untying', + 'unusable', + 'unused', + 'unusual', + 'unvalued', + 'unvaried', + 'unvarying', + 'unveiled', + 'unveiling', + 'unvented', + 'unviable', + 'unvisited', + 'unvocal', + 'unwanted', + 'unwarlike', + 'unwary', + 'unwashed', + 'unwatched', + 'unweave', + 'unwed', + 'unwelcome', + 'unwell', + 'unwieldy', + 'unwilling', + 'unwind', + 'unwired', + 'unwitting', + 'unwomanly', + 'unworldly', + 'unworn', + 'unworried', + 'unworthy', + 'unwound', + 'unwoven', + 'unwrapped', + 'unwritten', + 'unzip', + 'upbeat', + 'upchuck', + 'upcoming', + 'upcountry', + 'update', + 'upfront', + 'upgrade', + 'upheaval', + 'upheld', + 'uphill', + 'uphold', + 'uplifted', + 'uplifting', + 'upload', + 'upon', + 'upper', + 'upright', + 'uprising', + 'upriver', + 'uproar', + 'uproot', + 'upscale', + 'upside', + 'upstage', + 'upstairs', + 'upstart', + 'upstate', + 'upstream', + 'upstroke', + 'upswing', + 'uptake', + 'uptight', + 'uptown', + 'upturned', + 'upward', + 'upwind', + 'uranium', + 'urban', + 'urchin', + 'urethane', + 'urgency', + 'urgent', + 'urging', + 'urologist', + 'urology', + 'usable', + 'usage', + 'useable', + 'used', + 'uselessly', + 'user', + 'usher', + 'usual', + 'utensil', + 'utility', + 'utilize', + 'utmost', + 'utopia', + 'utter', + 'vacancy', + 'vacant', + 'vacate', + 'vacation', + 'vagabond', + 'vagrancy', + 'vagrantly', + 'vaguely', + 'vagueness', + 'valiant', + 'valid', + 'valium', + 'valley', + 'valuables', + 'value', + 'vanilla', + 'vanish', + 'vanity', + 'vanquish', + 'vantage', + 'vaporizer', + 'variable', + 'variably', + 'varied', + 'variety', + 'various', + 'varmint', + 'varnish', + 'varsity', + 'varying', + 'vascular', + 'vaseline', + 'vastly', + 'vastness', + 'veal', + 'vegan', + 'veggie', + 'vehicular', + 'velcro', + 'velocity', + 'velvet', + 'vendetta', + 'vending', + 'vendor', + 'veneering', + 'vengeful', + 'venomous', + 'ventricle', + 'venture', + 'venue', + 'venus', + 'verbalize', + 'verbally', + 'verbose', + 'verdict', + 'verify', + 'verse', + 'version', + 'versus', + 'vertebrae', + 'vertical', + 'vertigo', + 'very', + 'vessel', + 'vest', + 'veteran', + 'veto', + 'vexingly', + 'viability', + 'viable', + 'vibes', + 'vice', + 'vicinity', + 'victory', + 'video', + 'viewable', + 'viewer', + 'viewing', + 'viewless', + 'viewpoint', + 'vigorous', + 'village', + 'villain', + 'vindicate', + 'vineyard', + 'vintage', + 'violate', + 'violation', + 'violator', + 'violet', + 'violin', + 'viper', + 'viral', + 'virtual', + 'virtuous', + 'virus', + 'visa', + 'viscosity', + 'viscous', + 'viselike', + 'visible', + 'visibly', + 'vision', + 'visiting', + 'visitor', + 'visor', + 'vista', + 'vitality', + 'vitalize', + 'vitally', + 'vitamins', + 'vivacious', + 'vividly', + 'vividness', + 'vixen', + 'vocalist', + 'vocalize', + 'vocally', + 'vocation', + 'voice', + 'voicing', + 'void', + 'volatile', + 'volley', + 'voltage', + 'volumes', + 'voter', + 'voting', + 'voucher', + 'vowed', + 'vowel', + 'voyage', + 'wackiness', + 'wad', + 'wafer', + 'waffle', + 'waged', + 'wager', + 'wages', + 'waggle', + 'wagon', + 'wake', + 'waking', + 'walk', + 'walmart', + 'walnut', + 'walrus', + 'waltz', + 'wand', + 'wannabe', + 'wanted', + 'wanting', + 'wasabi', + 'washable', + 'washbasin', + 'washboard', + 'washbowl', + 'washcloth', + 'washday', + 'washed', + 'washer', + 'washhouse', + 'washing', + 'washout', + 'washroom', + 'washstand', + 'washtub', + 'wasp', + 'wasting', + 'watch', + 'water', + 'waviness', + 'waving', + 'wavy', + 'whacking', + 'whacky', + 'wham', + 'wharf', + 'wheat', + 'whenever', + 'whiff', + 'whimsical', + 'whinny', + 'whiny', + 'whisking', + 'whoever', + 'whole', + 'whomever', + 'whoopee', + 'whooping', + 'whoops', + 'why', + 'wick', + 'widely', + 'widen', + 'widget', + 'widow', + 'width', + 'wieldable', + 'wielder', + 'wife', + 'wifi', + 'wikipedia', + 'wildcard', + 'wildcat', + 'wilder', + 'wildfire', + 'wildfowl', + 'wildland', + 'wildlife', + 'wildly', + 'wildness', + 'willed', + 'willfully', + 'willing', + 'willow', + 'willpower', + 'wilt', + 'wimp', + 'wince', + 'wincing', + 'wind', + 'wing', + 'winking', + 'winner', + 'winnings', + 'winter', + 'wipe', + 'wired', + 'wireless', + 'wiring', + 'wiry', + 'wisdom', + 'wise', + 'wish', + 'wisplike', + 'wispy', + 'wistful', + 'wizard', + 'wobble', + 'wobbling', + 'wobbly', + 'wok', + 'wolf', + 'wolverine', + 'womanhood', + 'womankind', + 'womanless', + 'womanlike', + 'womanly', + 'womb', + 'woof', + 'wooing', + 'wool', + 'woozy', + 'word', + 'work', + 'worried', + 'worrier', + 'worrisome', + 'worry', + 'worsening', + 'worshiper', + 'worst', + 'wound', + 'woven', + 'wow', + 'wrangle', + 'wrath', + 'wreath', + 'wreckage', + 'wrecker', + 'wrecking', + 'wrench', + 'wriggle', + 'wriggly', + 'wrinkle', + 'wrinkly', + 'wrist', + 'writing', + 'written', + 'wrongdoer', + 'wronged', + 'wrongful', + 'wrongly', + 'wrongness', + 'wrought', + 'xbox', + 'xerox', + 'yahoo', + 'yam', + 'yanking', + 'yapping', + 'yard', + 'yarn', + 'yeah', + 'yearbook', + 'yearling', + 'yearly', + 'yearning', + 'yeast', + 'yelling', + 'yelp', + 'yen', + 'yesterday', + 'yiddish', + 'yield', + 'yin', + 'yippee', + 'yo-yo', + 'yodel', + 'yoga', + 'yogurt', + 'yonder', + 'yoyo', + 'yummy', + 'zap', + 'zealous', + 'zebra', + 'zen', + 'zeppelin', + 'zero', + 'zestfully', + 'zesty', + 'zigzagged', + 'zipfile', + 'zipping', + 'zippy', + 'zips', + 'zit', + 'zodiac', + 'zombie', + 'zone', + 'zoning', + 'zookeeper', + 'zoologist', + 'zoology', + 'zoom', +]; diff --git a/src/services/import.service.ts b/src/services/import.service.ts index d5dbed16f9..c35f916259 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -40,8 +40,8 @@ import { OnePassword1PifImporter } from '../importers/onepassword1PifImporter'; import { OnePasswordWinCsvImporter } from '../importers/onepasswordWinCsvImporter'; import { PadlockCsvImporter } from '../importers/padlockCsvImporter'; import { PassKeepCsvImporter } from '../importers/passkeepCsvImporter'; -import { PasspackCsvImporter } from '../importers/passpackCsvImporter'; import { PassmanJsonImporter } from '../importers/passmanJsonImporter'; +import { PasspackCsvImporter } from '../importers/passpackCsvImporter'; import { PasswordAgentCsvImporter } from '../importers/passwordAgentCsvImporter'; import { PasswordBossJsonImporter } from '../importers/passwordBossJsonImporter'; import { PasswordDragonXmlImporter } from '../importers/passwordDragonXmlImporter'; diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 9227f40446..120fde2de7 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -7,7 +7,7 @@ import { } from '../abstractions/passwordGeneration.service'; import { StorageService } from '../abstractions/storage.service'; -import { WordList } from '../misc/wordlist'; +import { EEFLongWordList } from '../misc/wordlist'; const DefaultOptions = { length: 14, @@ -20,6 +20,9 @@ const DefaultOptions = { minLowercase: 0, special: false, minSpecial: 1, + type: 0, + numWords: 3, + wordSeparator: ' ', }; const Keys = { @@ -39,7 +42,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr // overload defaults with given options const o = Object.assign({}, DefaultOptions, options); - if (o.generatePassphrase) { + if (o.type === 1) { // TODO: enum? return this.generatePassphrase(options); } @@ -159,13 +162,20 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr async generatePassphrase(options: any): Promise { const o = Object.assign({}, DefaultOptions, options); - const listLength = WordList.length - 1; + if (o.numWords == null || o.numWords <= 2) { + o.numWords = DefaultOptions.numWords; + } + if (o.wordSeparator == null || o.wordSeparator.length === 0 || o.wordSeparator.length > 1) { + o.wordSeparator = DefaultOptions.wordSeparator; + } + + const listLength = EEFLongWordList.length - 1; const wordList = new Array(o.numWords); for (let i = 0; i < o.numWords; i++) { - const wordindex = await this.cryptoService.randomNumber(0, listLength); - wordList[i] = WordList[wordindex]; + const wordIndex = await this.cryptoService.randomNumber(0, listLength); + wordList[i] = EEFLongWordList[wordIndex]; } - return wordList.join(' '); + return wordList.join(o.wordSeparator); } async getOptions() { From a867c14b2a8bb5f75cd495018e6c86eff22651b9 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 8 Oct 2018 22:06:06 -0400 Subject: [PATCH 0576/1626] pass gen fixes. word sep option --- .../components/password-generator.component.ts | 16 ++++++++++------ src/services/passwordGeneration.service.ts | 8 ++++---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/angular/components/password-generator.component.ts b/src/angular/components/password-generator.component.ts index 424640a434..97f4bc9695 100644 --- a/src/angular/components/password-generator.component.ts +++ b/src/angular/components/password-generator.component.ts @@ -17,7 +17,6 @@ export class PasswordGeneratorComponent implements OnInit { password: string = '-'; showOptions = false; avoidAmbiguous = false; - type = '0'; constructor(protected passwordGenerationService: PasswordGenerationService, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, @@ -26,7 +25,7 @@ export class PasswordGeneratorComponent implements OnInit { async ngOnInit() { this.options = await this.passwordGenerationService.getOptions(); this.avoidAmbiguous = !this.options.ambiguous; - this.type = this.options.type === 1 ? '1' : '0'; + this.options.type = this.options.type === 'passphrase' ? 'passphrase' : 'password'; this.password = await this.passwordGenerationService.generatePassword(this.options); this.platformUtilsService.eventTrack('Generated Password'); await this.passwordGenerationService.addHistory(this.password); @@ -79,13 +78,14 @@ export class PasswordGeneratorComponent implements OnInit { this.options.minLowercase = 0; this.options.minUppercase = 0; this.options.ambiguous = !this.avoidAmbiguous; - this.options.type = this.type == null || this.type === '0' ? 0 : 1; if (!this.options.uppercase && !this.options.lowercase && !this.options.number && !this.options.special) { this.options.lowercase = true; - const lowercase = document.querySelector('#lowercase') as HTMLInputElement; - if (lowercase) { - lowercase.checked = true; + if (this.win != null) { + const lowercase = this.win.document.querySelector('#lowercase') as HTMLInputElement; + if (lowercase) { + lowercase.checked = true; + } } } @@ -120,5 +120,9 @@ export class PasswordGeneratorComponent implements OnInit { } else if (this.options.numWords > 20) { this.options.numWords = 20; } + + if (this.options.wordSeparator != null && this.options.wordSeparator.length > 1) { + this.options.wordSeparator = this.options.wordSeparator[0]; + } } } diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 120fde2de7..907d766424 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -20,9 +20,9 @@ const DefaultOptions = { minLowercase: 0, special: false, minSpecial: 1, - type: 0, + type: 'password', numWords: 3, - wordSeparator: ' ', + wordSeparator: '-', }; const Keys = { @@ -42,7 +42,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr // overload defaults with given options const o = Object.assign({}, DefaultOptions, options); - if (o.type === 1) { // TODO: enum? + if (o.type === 'passphrase') { return this.generatePassphrase(options); } @@ -166,7 +166,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr o.numWords = DefaultOptions.numWords; } if (o.wordSeparator == null || o.wordSeparator.length === 0 || o.wordSeparator.length > 1) { - o.wordSeparator = DefaultOptions.wordSeparator; + o.wordSeparator = ' '; } const listLength = EEFLongWordList.length - 1; From 2870b7472bbb91893561c5c6603294e32bccd57d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Oct 2018 15:18:25 -0400 Subject: [PATCH 0577/1626] re-write data file if malformed json --- src/services/lowdbStorage.service.ts | 38 +++++++++++++++++----------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/services/lowdbStorage.service.ts b/src/services/lowdbStorage.service.ts index 581c48291a..50c01e66b3 100644 --- a/src/services/lowdbStorage.service.ts +++ b/src/services/lowdbStorage.service.ts @@ -11,6 +11,7 @@ import { Utils } from '../misc/utils'; export class LowdbStorageService implements StorageService { private db: lowdb.LowdbSync; private defaults: any; + private dataFilePath: string; constructor(defaults?: any, dir?: string, private allowCache = false) { this.defaults = defaults; @@ -20,25 +21,30 @@ export class LowdbStorageService implements StorageService { if (!fs.existsSync(dir)) { NodeUtils.mkdirpSync(dir, '700'); } - const p = path.join(dir, 'data.json'); - adapter = new FileSync(p); + this.dataFilePath = path.join(dir, 'data.json'); + adapter = new FileSync(this.dataFilePath); + } + try { + this.db = lowdb(adapter); + } catch (e) { + if (e instanceof SyntaxError) { + fs.writeFileSync(this.dataFilePath, ''); + this.db = lowdb(adapter); + } else { + throw e; + } } - this.db = lowdb(adapter); } init() { if (this.defaults != null) { - if (!this.allowCache) { - this.db.read(); - } + this.readForNoCache(); this.db.defaults(this.defaults).write(); } } get(key: string): Promise { - if (!this.allowCache) { - this.db.read(); - } + this.readForNoCache(); const val = this.db.get(key).value(); if (val == null) { return Promise.resolve(null); @@ -47,18 +53,20 @@ export class LowdbStorageService implements StorageService { } save(key: string, obj: any): Promise { - if (!this.allowCache) { - this.db.read(); - } + this.readForNoCache(); this.db.set(key, obj).write(); return Promise.resolve(); } remove(key: string): Promise { - if (!this.allowCache) { - this.db.read(); - } + this.readForNoCache(); this.db.unset(key).write(); return Promise.resolve(); } + + private readForNoCache() { + if (!this.allowCache) { + this.db.read(); + } + } } From dcef50bd8ff897e664477016252fc5b1b7ec1977 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Oct 2018 15:31:52 -0400 Subject: [PATCH 0578/1626] just use adapter --- src/services/lowdbStorage.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/lowdbStorage.service.ts b/src/services/lowdbStorage.service.ts index 50c01e66b3..e9621a0fcf 100644 --- a/src/services/lowdbStorage.service.ts +++ b/src/services/lowdbStorage.service.ts @@ -28,7 +28,7 @@ export class LowdbStorageService implements StorageService { this.db = lowdb(adapter); } catch (e) { if (e instanceof SyntaxError) { - fs.writeFileSync(this.dataFilePath, ''); + adapter.write({}); this.db = lowdb(adapter); } else { throw e; From 6f3806845d2800a9ecb0413a174c73d9949c8754 Mon Sep 17 00:00:00 2001 From: ServiusHack Date: Tue, 9 Oct 2018 21:40:06 +0200 Subject: [PATCH 0579/1626] Remove artificial limit on name length (#14) --- src/importers/passmanJsonImporter.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/importers/passmanJsonImporter.ts b/src/importers/passmanJsonImporter.ts index 08c39cef92..8e2346f0b8 100644 --- a/src/importers/passmanJsonImporter.ts +++ b/src/importers/passmanJsonImporter.ts @@ -20,9 +20,6 @@ export class PassmanJsonImporter extends BaseImporter implements Importer { const cipher = this.initLoginCipher(); cipher.name = credential.label; - if (cipher.name != null && cipher.name.length > 30) { - cipher.name = cipher.name.substring(0, 30); - } cipher.login.username = this.getValueOrDefault(credential.username); if (this.isNullOrWhitespace(cipher.login.username)) { From 3ca1544eb38c85f3b08c194c275d7db447a82673 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Oct 2018 16:00:50 -0400 Subject: [PATCH 0580/1626] renamed event to updated2fa --- src/enums/eventType.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/enums/eventType.ts b/src/enums/eventType.ts index 103a6b6873..11b4c26aa9 100644 --- a/src/enums/eventType.ts +++ b/src/enums/eventType.ts @@ -1,7 +1,7 @@ export enum EventType { User_LoggedIn = 1000, User_ChangedPassword = 1001, - User_Enabled2fa = 1002, + User_Updated2fa = 1002, User_Disabled2fa = 1003, User_Recovered2fa = 1004, User_FailedLogIn = 1005, From 16343c7a2fea850f8fd6f33d2a3143d4fe41a8bd Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Oct 2018 17:52:50 -0400 Subject: [PATCH 0581/1626] update electron --- package-lock.json | 22 +++++++++++----------- package.json | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index b02d5a7110..5983f12e8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1968,9 +1968,9 @@ "dev": true }, "electron": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/electron/-/electron-2.0.7.tgz", - "integrity": "sha512-MRrDE6mrp+ZrIBpZM27pxbO2yEDKYfkmc6Ll79BtedMNEZsY4+oblupeDJL6RM6meUIp82KMo63W7fP65Tb89Q==", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/electron/-/electron-2.0.11.tgz", + "integrity": "sha512-bFTMDQN3epfiymqTPdgffyTxuy/7A52sIkW7Hos+hY5XLPArOXLXAKx1JtB3dM7CcPfZa+5qp/J3cPCidh5WXg==", "dev": true, "requires": { "@types/node": "^8.0.24", @@ -1979,9 +1979,9 @@ }, "dependencies": { "@types/node": { - "version": "8.10.26", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.26.tgz", - "integrity": "sha512-opk6bLLErLSwyVVJeSH5Ek7ZWOBSsN0JrvXTNVGLXLAXKB9xlTYajrplR44xVyMrmbut94H6uJ9jqzM/12jxkA==", + "version": "8.10.36", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.36.tgz", + "integrity": "sha512-SL6KhfM7PTqiFmbCW3eVNwVBZ+88Mrzbuvn9olPsfv43mbiWaFY+nRcz/TGGku0/lc2FepdMbImdMY1JrQ+zbw==", "dev": true } } @@ -2027,7 +2027,7 @@ }, "jsonfile": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "resolved": "http://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", "dev": true, "requires": { @@ -2162,9 +2162,9 @@ } }, "es6-promise": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", - "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.5.tgz", + "integrity": "sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg==", "dev": true }, "escape-html": { @@ -7045,7 +7045,7 @@ }, "readable-stream": { "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "dev": true, "requires": { diff --git a/package.json b/package.json index 90406232d4..f3757d2b1c 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@types/papaparse": "^4.5.3", "@types/webcrypto": "0.0.28", "concurrently": "3.5.1", - "electron": "2.0.7", + "electron": "2.0.11", "jasmine": "^3.2.0", "jasmine-core": "^3.2.1", "jasmine-spec-reporter": "^4.2.1", From 154c211fb1ff4e43d62d172a5b2e70fc9734c5e1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Oct 2018 17:59:56 -0400 Subject: [PATCH 0582/1626] update signalr --- package-lock.json | 30 +++++++++++++++--------------- package.json | 4 ++-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5983f12e8c..7dc3baed80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -85,14 +85,14 @@ } }, "@aspnet/signalr": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@aspnet/signalr/-/signalr-1.0.3.tgz", - "integrity": "sha512-8nPSarp4k+oP2M6P7tw2FZMXOMR86wH9GPb/4wiqA18c4Ds88SUmE0pSpnNQPDOoWGMj6y9F2Xz5JyoynCPXWQ==" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@aspnet/signalr/-/signalr-1.0.4.tgz", + "integrity": "sha512-q7HMlTZPkZCa/0UclsXvEyqNirpjRfRuwhjEeADD1i6pqe0Yx5OwuCO7+Xsc6MNKR8vE1C9MyxnSj0SecvUbTA==" }, "@aspnet/signalr-protocol-msgpack": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@aspnet/signalr-protocol-msgpack/-/signalr-protocol-msgpack-1.0.3.tgz", - "integrity": "sha512-R5CRpxXGICi0Tgbd86uA8bC75Rm0y7Yp6oD87INfKxsav/m/xbz33rkGOI1OZIfsc/J6/WU5z0Bqpc2QgYfaJQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@aspnet/signalr-protocol-msgpack/-/signalr-protocol-msgpack-1.0.4.tgz", + "integrity": "sha512-nwGwkroojOLmjvV4nybfBqw3GS488Mx9hYoY5pMDUKNSeNOPCejNeCmdFPf1c/xAnV24eC4O1XKolA7IGI7Fzw==", "requires": { "msgpack5": "^4.0.2" } @@ -2027,7 +2027,7 @@ }, "jsonfile": { "version": "2.4.0", - "resolved": "http://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", "dev": true, "requires": { @@ -4812,20 +4812,20 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "msgpack5": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/msgpack5/-/msgpack5-4.2.0.tgz", - "integrity": "sha512-tQkRlwO4f3/E8Kq5qm6PcVw+J+K4+U/XNqeD9Ebo1qVsrjkcKb2FfmdtuuIslw42CGT+K3ZVKAvKfSPp3QRplQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/msgpack5/-/msgpack5-4.2.1.tgz", + "integrity": "sha512-Xo7nE9ZfBVonQi1rSopNAqPdts/QHyuSEUwIEzAkB+V2FtmkkLUbP6MyVqVVQxsZYI65FpvW3Bb8Z9ZWEjbgHQ==", "requires": { - "bl": "^2.0.0", + "bl": "^2.0.1", "inherits": "^2.0.3", "readable-stream": "^2.3.6", "safe-buffer": "^5.1.2" }, "dependencies": { "bl": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/bl/-/bl-2.0.1.tgz", - "integrity": "sha512-FrMgLukB9jujvJ92p5TA0hcKIHtInVXXhxD7qgAuV7k0cbPt9USZmOYnhDXH6IsnGeIUglX42TSBV7Gn4q5sbQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.1.1.tgz", + "integrity": "sha512-YTmzlmPyCuKGFSTLL3P7nlZHk+CDC3ddehCT+/ZwcI35jUjnpfSXlrAAr3hIEoD/+4TvKy27vMp7yc/q8aa6tA==", "requires": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" @@ -7045,7 +7045,7 @@ }, "readable-stream": { "version": "1.1.14", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "dev": true, "requires": { diff --git a/package.json b/package.json index f3757d2b1c..2647556ef8 100644 --- a/package.json +++ b/package.json @@ -67,8 +67,8 @@ "@angular/platform-browser-dynamic": "6.1.7", "@angular/router": "6.1.7", "@angular/upgrade": "6.1.7", - "@aspnet/signalr": "1.0.3", - "@aspnet/signalr-protocol-msgpack": "1.0.3", + "@aspnet/signalr": "1.0.4", + "@aspnet/signalr-protocol-msgpack": "1.0.4", "core-js": "2.5.7", "electron-log": "2.2.14", "electron-updater": "3.0.3", From 7c3e0cba34be4c3ccc32ff74804c0c71d9700905 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 10 Oct 2018 09:59:09 -0400 Subject: [PATCH 0583/1626] overload defaults on options get --- src/services/passwordGeneration.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 907d766424..f5b9b083aa 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -184,7 +184,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr if (options == null) { this.optionsCache = DefaultOptions; } else { - this.optionsCache = options; + this.optionsCache = Object.assign({}, DefaultOptions, options); } } From 9cd0bd5f7cfec02c2c61c7e20eaf4f0d5e7a16df Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 10 Oct 2018 16:46:57 -0400 Subject: [PATCH 0584/1626] allow multiple u2f challenges during 2fa login --- src/angular/components/two-factor.component.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 57daee1d64..3f60203b9e 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -98,14 +98,16 @@ export class TwoFactorComponent implements OnInit, OnDestroy { } const challenges = JSON.parse(params.Challenges); - if (challenges.length > 0) { + if (challenges != null && challenges.length > 0) { this.u2f.init({ appId: challenges[0].appId, challenge: challenges[0].challenge, - keys: [{ - version: challenges[0].version, - keyHandle: challenges[0].keyHandle, - }], + keys: challenges.map((c: any) => { + return { + version: c.version, + keyHandle: c.keyHandle, + }; + }), }); } break; From 90f723316aea59287ed2a11ebee86d474fe67e42 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 10 Oct 2018 17:52:08 -0400 Subject: [PATCH 0585/1626] support for new Challenge token for U2F --- .../components/two-factor.component.ts | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 3f60203b9e..99342b8e9a 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -97,18 +97,23 @@ export class TwoFactorComponent implements OnInit, OnDestroy { break; } - const challenges = JSON.parse(params.Challenges); - if (challenges != null && challenges.length > 0) { - this.u2f.init({ - appId: challenges[0].appId, - challenge: challenges[0].challenge, - keys: challenges.map((c: any) => { - return { - version: c.version, - keyHandle: c.keyHandle, - }; - }), - }); + if (params.Challenge != null) { + this.u2f.init(JSON.parse(params.Challenge)); + } else { + // TODO: Deprecated. Remove in future version. + const challenges = JSON.parse(params.Challenges); + if (challenges != null && challenges.length > 0) { + this.u2f.init({ + appId: challenges[0].appId, + challenge: challenges[0].challenge, + keys: challenges.map((c: any) => { + return { + version: c.version, + keyHandle: c.keyHandle, + }; + }), + }); + } } break; case TwoFactorProviderType.Duo: From 9fa99f3a6ecf7c4a14e5d7271562b3908cf64728 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 11 Oct 2018 16:49:11 -0400 Subject: [PATCH 0586/1626] only show warning if there is a row --- src/importers/baseImporter.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index ca642666cf..b70a566a59 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -80,8 +80,10 @@ export abstract class BaseImporter { }); if (result.errors != null && result.errors.length > 0) { result.errors.forEach((e) => { - // tslint:disable-next-line - console.warn('Error parsing row ' + e.row + ': ' + e.message); + if (e.row != null) { + // tslint:disable-next-line + console.warn('Error parsing row ' + e.row + ': ' + e.message); + } }); } return result.data && result.data.length > 0 ? result.data : null; From c35576deb8d3fa06959157eebff4603ce3d21a9b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 13 Oct 2018 00:11:06 -0400 Subject: [PATCH 0587/1626] ns updates and overloads --- src/angular/components/login.component.ts | 7 ++++++- src/misc/utils.ts | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/angular/components/login.component.ts b/src/angular/components/login.component.ts index d5a19526b1..86e0e66a7c 100644 --- a/src/angular/components/login.component.ts +++ b/src/angular/components/login.component.ts @@ -27,6 +27,7 @@ export class LoginComponent implements OnInit { formPromise: Promise; onSuccessfulLogin: () => Promise; onSuccessfulLoginNavigate: () => Promise; + onSuccessfulLoginTwoFactorNavigate: () => Promise; protected twoFactorRoute = '2fa'; protected successRoute = 'vault'; @@ -79,7 +80,11 @@ export class LoginComponent implements OnInit { } if (response.twoFactor) { this.platformUtilsService.eventTrack('Logged In To Two-step'); - this.router.navigate([this.twoFactorRoute]); + if (this.onSuccessfulLoginTwoFactorNavigate != null) { + this.onSuccessfulLoginTwoFactorNavigate(); + } else { + this.router.navigate([this.twoFactorRoute]); + } } else { if (this.onSuccessfulLogin != null) { this.onSuccessfulLogin(); diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 6ff46f15cd..0b5c86dc25 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -22,7 +22,7 @@ export class Utils { Utils.isBrowser = typeof window !== 'undefined'; Utils.isNativeScript = !Utils.isNode && !Utils.isBrowser; Utils.isMobileBrowser = Utils.isBrowser && this.isMobile(window); - Utils.global = Utils.isNativeScript ? new Object() : (Utils.isNode && !Utils.isBrowser ? global : window); + Utils.global = Utils.isNativeScript ? global : (Utils.isNode && !Utils.isBrowser ? global : window); } static fromB64ToArray(str: string): Uint8Array { From ad97afc5904b47bee64e952b911e2bbd39839168 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 13 Oct 2018 22:21:54 -0400 Subject: [PATCH 0588/1626] move getDomain to jslib Utils --- package-lock.json | 17 +++++++- package.json | 2 + spec/common/misc/utils.spec.ts | 29 +++++++++++++ src/abstractions/platformUtils.service.ts | 1 - .../services/electronPlatformUtils.service.ts | 5 --- src/misc/utils.ts | 42 +++++++++++++++++++ src/models/view/loginUriView.ts | 14 ++----- src/services/cipher.service.ts | 5 +-- src/services/container.service.ts | 8 +--- 9 files changed, 94 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7dc3baed80..c28a756d59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -166,6 +166,12 @@ "@types/node": "*" } }, + "@types/tldjs": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@types/tldjs/-/tldjs-2.3.0.tgz", + "integrity": "sha512-+gqspH/N6YjpApp96/XzM2AZK4R0Bk2qb4e5o16indSvgblfFaAIxNV8BdJmbqfSAYUyZubLzvrmpvdVEmBq3A==", + "dev": true + }, "@types/webcrypto": { "version": "0.0.28", "resolved": "https://registry.npmjs.org/@types/webcrypto/-/webcrypto-0.0.28.tgz", @@ -5936,8 +5942,7 @@ "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, "qjobs": { "version": "1.2.0", @@ -7087,6 +7092,14 @@ "setimmediate": "^1.0.4" } }, + "tldjs": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tldjs/-/tldjs-2.3.1.tgz", + "integrity": "sha512-W/YVH/QczLUxVjnQhFC61Iq232NWu3TqDdO0S/MtXVz4xybejBov4ud+CIwN9aYqjOecEqIy0PscGkwpG9ZyTw==", + "requires": { + "punycode": "^1.4.1" + } + }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", diff --git a/package.json b/package.json index 2647556ef8..305763fe70 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@types/node-fetch": "^2.1.2", "@types/node-forge": "^0.7.5", "@types/papaparse": "^4.5.3", + "@types/tldjs": "^2.3.0", "@types/webcrypto": "0.0.28", "concurrently": "3.5.1", "electron": "2.0.11", @@ -80,6 +81,7 @@ "node-forge": "0.7.6", "papaparse": "4.6.0", "rxjs": "6.3.2", + "tldjs": "2.3.1", "zone.js": "0.8.26" } } diff --git a/spec/common/misc/utils.spec.ts b/spec/common/misc/utils.spec.ts index adbe753718..3638b5964c 100644 --- a/spec/common/misc/utils.spec.ts +++ b/spec/common/misc/utils.spec.ts @@ -1,6 +1,35 @@ import { Utils } from '../../../src/misc/utils'; describe('Utils Service', () => { + describe('getDomain', () => { + it('should fail for invalid urls', () => { + expect(Utils.getDomain(null)).toBeNull(); + expect(Utils.getDomain(undefined)).toBeNull(); + expect(Utils.getDomain(' ')).toBeNull(); + expect(Utils.getDomain('https://bit!:"_&ward.com')).toBeNull(); + expect(Utils.getDomain('bitwarden')).toBeNull(); + }); + + it('should handle urls without protocol', () => { + expect(Utils.getDomain('bitwarden.com')).toBe('bitwarden.com'); + expect(Utils.getDomain('wrong://bitwarden.com')).toBe('bitwarden.com'); + }); + + it('should handle valid urls', () => { + expect(Utils.getDomain('https://bitwarden')).toBe('bitwarden'); + expect(Utils.getDomain('https://bitwarden.com')).toBe('bitwarden.com'); + expect(Utils.getDomain('http://bitwarden.com')).toBe('bitwarden.com'); + expect(Utils.getDomain('http://vault.bitwarden.com')).toBe('bitwarden.com'); + expect(Utils.getDomain('https://user:password@bitwarden.com:8080/password/sites?and&query#hash')).toBe('bitwarden.com'); + expect(Utils.getDomain('https://bitwarden.unknown')).toBe('bitwarden.unknown'); + }); + + it('should support localhost and IP', () => { + expect(Utils.getDomain('https://localhost')).toBe('localhost'); + expect(Utils.getDomain('https://192.168.1.1')).toBe('192.168.1.1'); + }); + }); + describe('getHostname', () => { it('should fail for invalid urls', () => { expect(Utils.getHostname(null)).toBeNull(); diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index e33b97db54..14131a13c4 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -13,7 +13,6 @@ export abstract class PlatformUtilsService { isIE: () => boolean; isMacAppStore: () => boolean; analyticsId: () => string; - getDomain: (uriString: string) => string; isViewOpen: () => boolean; lockTimeout: () => number; launchUri: (uri: string, options?: any) => void; diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index c243558c92..536de73617 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -17,7 +17,6 @@ import { MessagingService } from '../../abstractions/messaging.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { AnalyticsIds } from '../../misc/analytics'; -import { Utils } from '../../misc/utils'; export class ElectronPlatformUtilsService implements PlatformUtilsService { identityClientId: string; @@ -99,10 +98,6 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { return this.analyticsIdCache; } - getDomain(uriString: string): string { - return Utils.getHostname(uriString); - } - isViewOpen(): boolean { return false; } diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 0b5c86dc25..2760d2af6c 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -1,3 +1,5 @@ +import * as tldjs from 'tldjs'; + import { I18nService } from '../abstractions/i18n.service'; // tslint:disable-next-line @@ -163,6 +165,36 @@ export class Utils { } } + static getDomain(uriString: string): string { + if (uriString == null) { + return null; + } + + uriString = uriString.trim(); + if (uriString === '') { + return null; + } + + if (uriString.startsWith('http://') || uriString.startsWith('https://')) { + try { + const url = Utils.getUrlObject(uriString); + if (url.hostname === 'localhost' || Utils.validIpAddress(url.hostname)) { + return url.hostname; + } + + const urlDomain = tldjs != null && tldjs.getDomain != null ? tldjs.getDomain(url.hostname) : null; + return urlDomain != null ? urlDomain : url.hostname; + } catch (e) { } + } + + const domain = tldjs != null && tldjs.getDomain != null ? tldjs.getDomain(uriString) : null; + if (domain != null) { + return domain; + } + + return null; + } + static getQueryParams(uriString: string): Map { const url = Utils.getUrl(uriString); if (url == null || url.search == null || url.search === '') { @@ -197,6 +229,12 @@ export class Utils { }; } + private static validIpAddress(ipString: string): boolean { + // tslint:disable-next-line + const ipRegex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + return ipRegex.test(ipString); + } + private static isMobile(win: Window) { let mobile = false; ((a) => { @@ -225,6 +263,10 @@ export class Utils { return null; } + return Utils.getUrlObject(uriString); + } + + private static getUrlObject(uriString: string): URL { try { if (nodeURL != null) { return nodeURL.URL ? new nodeURL.URL(uriString) : nodeURL.parse(uriString); diff --git a/src/models/view/loginUriView.ts b/src/models/view/loginUriView.ts index 476067959a..c402c6fb3b 100644 --- a/src/models/view/loginUriView.ts +++ b/src/models/view/loginUriView.ts @@ -4,8 +4,6 @@ import { View } from './view'; import { LoginUri } from '../domain/loginUri'; -import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; - import { Utils } from '../../misc/utils'; export class LoginUriView implements View { @@ -35,15 +33,9 @@ export class LoginUriView implements View { get domain(): string { if (this._domain == null && this.uri != null) { - const containerService = (Utils.global as any).bitwardenContainerService; - if (containerService) { - const platformUtilsService: PlatformUtilsService = containerService.getPlatformUtilsService(); - this._domain = platformUtilsService.getDomain(this.uri); - if (this._domain === '') { - this._domain = null; - } - } else { - throw new Error('global bitwardenContainerService not initialized.'); + this._domain = Utils.getDomain(this.uri); + if (this._domain === '') { + this._domain = null; } } diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index b201c171bd..2ceb801dd2 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -37,7 +37,6 @@ import { ApiService } from '../abstractions/api.service'; import { CipherService as CipherServiceAbstraction } from '../abstractions/cipher.service'; import { CryptoService } from '../abstractions/crypto.service'; import { I18nService } from '../abstractions/i18n.service'; -import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { SearchService } from '../abstractions/search.service'; import { SettingsService } from '../abstractions/settings.service'; import { StorageService } from '../abstractions/storage.service'; @@ -59,7 +58,7 @@ export class CipherService implements CipherServiceAbstraction { constructor(private cryptoService: CryptoService, private userService: UserService, private settingsService: SettingsService, private apiService: ApiService, private storageService: StorageService, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, private searchService: () => SearchService) { + private searchService: () => SearchService) { } get decryptedCipherCache() { @@ -311,7 +310,7 @@ export class CipherService implements CipherServiceAbstraction { return Promise.resolve([]); } - const domain = this.platformUtilsService.getDomain(url); + const domain = Utils.getDomain(url); const eqDomainsPromise = domain == null ? Promise.resolve([]) : this.settingsService.getEquivalentDomains().then((eqDomains: any[][]) => { let matches: any[] = []; diff --git a/src/services/container.service.ts b/src/services/container.service.ts index 10e3f6390e..08f428ad0f 100644 --- a/src/services/container.service.ts +++ b/src/services/container.service.ts @@ -1,9 +1,7 @@ import { CryptoService } from '../abstractions/crypto.service'; -import { PlatformUtilsService } from '../abstractions/platformUtils.service'; export class ContainerService { - constructor(private cryptoService: CryptoService, - private platformUtilsService: PlatformUtilsService) { + constructor(private cryptoService: CryptoService) { } // deprecated, use attachToGlobal instead @@ -20,8 +18,4 @@ export class ContainerService { getCryptoService(): CryptoService { return this.cryptoService; } - - getPlatformUtilsService(): PlatformUtilsService { - return this.platformUtilsService; - } } From 0d30a1a1c9bd2c323189069fe4ec233ab85c35fe Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 13 Oct 2018 22:43:54 -0400 Subject: [PATCH 0589/1626] tldjs noop --- src/misc/tldjs.noop.ts | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/misc/tldjs.noop.ts diff --git a/src/misc/tldjs.noop.ts b/src/misc/tldjs.noop.ts new file mode 100644 index 0000000000..74a02918fb --- /dev/null +++ b/src/misc/tldjs.noop.ts @@ -0,0 +1,3 @@ +export function getDomain(host: string): string | null { + return null; +} From 2f6426deb470b71838b51c52587929ac64d428bf Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 15 Oct 2018 13:02:31 -0400 Subject: [PATCH 0590/1626] success callbacks --- src/angular/components/hint.component.ts | 7 ++++++- src/angular/components/lock.component.ts | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/angular/components/hint.component.ts b/src/angular/components/hint.component.ts index e1052a58f4..914ebfb95b 100644 --- a/src/angular/components/hint.component.ts +++ b/src/angular/components/hint.component.ts @@ -11,6 +11,7 @@ export class HintComponent { formPromise: Promise; protected successRoute = 'login'; + protected onSuccessfulSubmit: () => void; constructor(protected router: Router, protected i18nService: I18nService, protected apiService: ApiService, protected platformUtilsService: PlatformUtilsService) { } @@ -32,7 +33,11 @@ export class HintComponent { await this.formPromise; this.platformUtilsService.eventTrack('Requested Hint'); this.platformUtilsService.showToast('success', null, this.i18nService.t('masterPassSent')); - this.router.navigate([this.successRoute]); + if (this.onSuccessfulSubmit != null) { + this.onSuccessfulSubmit(); + } else if (this.router != null) { + this.router.navigate([this.successRoute]); + } } catch { } } } diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 1d2ba6f147..6e8caea78a 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -11,6 +11,7 @@ export class LockComponent { showPassword: boolean = false; protected successRoute: string = 'vault'; + protected onSuccessfulSubmit: () => void; constructor(protected router: Router, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected messagingService: MessagingService, @@ -33,7 +34,11 @@ export class LockComponent { if (storedKeyHash != null && keyHash != null && storedKeyHash === keyHash) { await this.cryptoService.setKey(key); this.messagingService.send('unlocked'); - this.router.navigate([this.successRoute]); + if (this.onSuccessfulSubmit != null) { + this.onSuccessfulSubmit(); + } else if (this.router != null) { + this.router.navigate([this.successRoute]); + } } else { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('invalidMasterPassword')); From 00efae261684eb97e7fcd8fd77aa04bca5e87a72 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 17 Oct 2018 10:51:09 -0400 Subject: [PATCH 0591/1626] add manager org user type --- src/enums/organizationUserType.ts | 1 + src/models/domain/organization.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/enums/organizationUserType.ts b/src/enums/organizationUserType.ts index 217c0b450e..de794fe690 100644 --- a/src/enums/organizationUserType.ts +++ b/src/enums/organizationUserType.ts @@ -2,4 +2,5 @@ export enum OrganizationUserType { Owner = 0, Admin = 1, User = 2, + Manager = 3, } diff --git a/src/models/domain/organization.ts b/src/models/domain/organization.ts index 122dc27fe0..961f8cc023 100644 --- a/src/models/domain/organization.ts +++ b/src/models/domain/organization.ts @@ -49,6 +49,11 @@ export class Organization { return this.enabled && this.status === OrganizationUserStatusType.Confirmed; } + get isManager() { + return this.type === OrganizationUserType.Manager || this.type === OrganizationUserType.Owner || + this.type === OrganizationUserType.Admin; + } + get isAdmin() { return this.type === OrganizationUserType.Owner || this.type === OrganizationUserType.Admin; } From 0d8e09b3f1da8e1377bad5cfea2bc0dfcc7701c0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 17 Oct 2018 11:18:12 -0400 Subject: [PATCH 0592/1626] add API for user collections --- src/abstractions/api.service.ts | 1 + src/services/api.service.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 0410fab49b..e72140982f 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -155,6 +155,7 @@ export abstract class ApiService { organizationId: string) => Promise; getCollectionDetails: (organizationId: string, id: string) => Promise; + getUserCollections: () => Promise>; getCollections: (organizationId: string) => Promise>; getCollectionUsers: (organizationId: string, id: string) => Promise>; postCollection: (organizationId: string, request: CollectionRequest) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 18fdd9935c..a0b9c816e7 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -434,6 +434,11 @@ export class ApiService implements ApiServiceAbstraction { return new CollectionGroupDetailsResponse(r); } + async getUserCollections(): Promise> { + const r = await this.send('GET', '/collections', null, true, true); + return new ListResponse(r, CollectionResponse); + } + async getCollections(organizationId: string): Promise> { const r = await this.send('GET', '/organizations/' + organizationId + '/collections', null, true, true); return new ListResponse(r, CollectionResponse); From d49182597e3f29d9790a4b918ba0ab6583922ab6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 17 Oct 2018 17:31:13 -0400 Subject: [PATCH 0593/1626] new collection user api endpoint --- src/abstractions/api.service.ts | 4 ++-- src/models/response/collectionUserResponse.ts | 22 ------------------- src/services/api.service.ts | 6 ++--- 3 files changed, 5 insertions(+), 27 deletions(-) delete mode 100644 src/models/response/collectionUserResponse.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index e72140982f..046e6226b8 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -55,7 +55,6 @@ import { CollectionGroupDetailsResponse, CollectionResponse, } from '../models/response/collectionResponse'; -import { CollectionUserResponse } from '../models/response/collectionUserResponse'; import { DomainsResponse } from '../models/response/domainsResponse'; import { EventResponse } from '../models/response/eventResponse'; import { FolderResponse } from '../models/response/folderResponse'; @@ -75,6 +74,7 @@ import { } from '../models/response/organizationUserResponse'; import { PreloginResponse } from '../models/response/preloginResponse'; import { ProfileResponse } from '../models/response/profileResponse'; +import { SelectionReadOnlyResponse } from '../models/response/selectionReadOnlyResponse'; import { SyncResponse } from '../models/response/syncResponse'; import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; import { TwoFactorDuoResponse } from '../models/response/twoFactorDuoResponse'; @@ -157,7 +157,7 @@ export abstract class ApiService { getCollectionDetails: (organizationId: string, id: string) => Promise; getUserCollections: () => Promise>; getCollections: (organizationId: string) => Promise>; - getCollectionUsers: (organizationId: string, id: string) => Promise>; + getCollectionUsers: (organizationId: string, id: string) => Promise>; postCollection: (organizationId: string, request: CollectionRequest) => Promise; putCollection: (organizationId: string, id: string, request: CollectionRequest) => Promise; deleteCollection: (organizationId: string, id: string) => Promise; diff --git a/src/models/response/collectionUserResponse.ts b/src/models/response/collectionUserResponse.ts deleted file mode 100644 index ea36e473e6..0000000000 --- a/src/models/response/collectionUserResponse.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { OrganizationUserStatusType } from '../../enums/organizationUserStatusType'; -import { OrganizationUserType } from '../../enums/organizationUserType'; - -export class CollectionUserResponse { - organizationUserId: string; - accessAll: boolean; - name: string; - email: string; - type: OrganizationUserType; - status: OrganizationUserStatusType; - readOnly: boolean; - - constructor(response: any) { - this.organizationUserId = response.OrganizationUserId; - this.accessAll = response.AccessAll; - this.name = response.Name; - this.email = response.Email; - this.type = response.Type; - this.status = response.Status; - this.readOnly = response.ReadOnly; - } -} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index a0b9c816e7..b3ad4b03ba 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -61,7 +61,6 @@ import { CollectionGroupDetailsResponse, CollectionResponse, } from '../models/response/collectionResponse'; -import { CollectionUserResponse } from '../models/response/collectionUserResponse'; import { DomainsResponse } from '../models/response/domainsResponse'; import { ErrorResponse } from '../models/response/errorResponse'; import { EventResponse } from '../models/response/eventResponse'; @@ -82,6 +81,7 @@ import { } from '../models/response/organizationUserResponse'; import { PreloginResponse } from '../models/response/preloginResponse'; import { ProfileResponse } from '../models/response/profileResponse'; +import { SelectionReadOnlyResponse } from '../models/response/selectionReadOnlyResponse'; import { SyncResponse } from '../models/response/syncResponse'; import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; import { TwoFactorDuoResponse } from '../models/response/twoFactorDuoResponse'; @@ -444,10 +444,10 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, CollectionResponse); } - async getCollectionUsers(organizationId: string, id: string): Promise> { + async getCollectionUsers(organizationId: string, id: string): Promise> { const r = await this.send('GET', '/organizations/' + organizationId + '/collections/' + id + '/users', null, true, true); - return new ListResponse(r, CollectionUserResponse); + return new ListResponse(r, SelectionReadOnlyResponse); } async postCollection(organizationId: string, request: CollectionRequest): Promise { From 2b8ffea494a82aef30a7b0ee741f5f986bd027cc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 17 Oct 2018 22:18:28 -0400 Subject: [PATCH 0594/1626] put collection users apis --- src/abstractions/api.service.ts | 2 ++ src/services/api.service.ts | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 046e6226b8..a2e5f48542 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -31,6 +31,7 @@ import { PaymentRequest } from '../models/request/paymentRequest'; import { PreloginRequest } from '../models/request/preloginRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { SeatRequest } from '../models/request/seatRequest'; +import { SelectionReadOnlyRequest } from '../models/request/selectionReadOnlyRequest'; import { StorageRequest } from '../models/request/storageRequest'; import { TokenRequest } from '../models/request/tokenRequest'; import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; @@ -159,6 +160,7 @@ export abstract class ApiService { getCollections: (organizationId: string) => Promise>; getCollectionUsers: (organizationId: string, id: string) => Promise>; postCollection: (organizationId: string, request: CollectionRequest) => Promise; + putCollectionUsers: (organizationId: string, id: string, request: SelectionReadOnlyRequest[]) => Promise; putCollection: (organizationId: string, id: string, request: CollectionRequest) => Promise; deleteCollection: (organizationId: string, id: string) => Promise; deleteCollectionUser: (organizationId: string, id: string, organizationUserId: string) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index b3ad4b03ba..a7b6f530c8 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -37,6 +37,7 @@ import { PaymentRequest } from '../models/request/paymentRequest'; import { PreloginRequest } from '../models/request/preloginRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { SeatRequest } from '../models/request/seatRequest'; +import { SelectionReadOnlyRequest } from '../models/request/selectionReadOnlyRequest'; import { StorageRequest } from '../models/request/storageRequest'; import { TokenRequest } from '../models/request/tokenRequest'; import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; @@ -461,6 +462,11 @@ export class ApiService implements ApiServiceAbstraction { return new CollectionResponse(r); } + async putCollectionUsers(organizationId: string, id: string, request: SelectionReadOnlyRequest[]): Promise { + await this.send('PUT', '/organizations/' + organizationId + '/collections/' + id + '/users', + request, true, false); + } + deleteCollection(organizationId: string, id: string): Promise { return this.send('DELETE', '/organizations/' + organizationId + '/collections/' + id, null, true, false); } From d1f7a97011f043a6225f603e71f0565acbe7dcdd Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 17 Oct 2018 22:56:28 -0400 Subject: [PATCH 0595/1626] group user apis --- src/abstractions/api.service.ts | 6 +++--- src/models/response/groupUserResponse.ts | 20 -------------------- src/services/api.service.ts | 13 ++++++++----- 3 files changed, 11 insertions(+), 28 deletions(-) delete mode 100644 src/models/response/groupUserResponse.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index a2e5f48542..264da1bfa9 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -63,7 +63,6 @@ import { GroupDetailsResponse, GroupResponse, } from '../models/response/groupResponse'; -import { GroupUserResponse } from '../models/response/groupUserResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; import { ListResponse } from '../models/response/listResponse'; @@ -158,7 +157,7 @@ export abstract class ApiService { getCollectionDetails: (organizationId: string, id: string) => Promise; getUserCollections: () => Promise>; getCollections: (organizationId: string) => Promise>; - getCollectionUsers: (organizationId: string, id: string) => Promise>; + getCollectionUsers: (organizationId: string, id: string) => Promise; postCollection: (organizationId: string, request: CollectionRequest) => Promise; putCollectionUsers: (organizationId: string, id: string, request: SelectionReadOnlyRequest[]) => Promise; putCollection: (organizationId: string, id: string, request: CollectionRequest) => Promise; @@ -167,9 +166,10 @@ export abstract class ApiService { getGroupDetails: (organizationId: string, id: string) => Promise; getGroups: (organizationId: string) => Promise>; - getGroupUsers: (organizationId: string, id: string) => Promise>; + getGroupUsers: (organizationId: string, id: string) => Promise; postGroup: (organizationId: string, request: GroupRequest) => Promise; putGroup: (organizationId: string, id: string, request: GroupRequest) => Promise; + putGroupUsers: (organizationId: string, id: string, request: string[]) => Promise; deleteGroup: (organizationId: string, id: string) => Promise; deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise; diff --git a/src/models/response/groupUserResponse.ts b/src/models/response/groupUserResponse.ts deleted file mode 100644 index 0d8d25c30d..0000000000 --- a/src/models/response/groupUserResponse.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { OrganizationUserStatusType } from '../../enums/organizationUserStatusType'; -import { OrganizationUserType } from '../../enums/organizationUserType'; - -export class GroupUserResponse { - organizationUserId: string; - accessAll: boolean; - name: string; - email: string; - type: OrganizationUserType; - status: OrganizationUserStatusType; - - constructor(response: any) { - this.organizationUserId = response.OrganizationUserId; - this.accessAll = response.AccessAll; - this.name = response.Name; - this.email = response.Email; - this.type = response.Type; - this.status = response.Status; - } -} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index a7b6f530c8..0206e3d643 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -70,7 +70,6 @@ import { GroupDetailsResponse, GroupResponse, } from '../models/response/groupResponse'; -import { GroupUserResponse } from '../models/response/groupUserResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; import { ListResponse } from '../models/response/listResponse'; @@ -445,10 +444,10 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, CollectionResponse); } - async getCollectionUsers(organizationId: string, id: string): Promise> { + async getCollectionUsers(organizationId: string, id: string): Promise { const r = await this.send('GET', '/organizations/' + organizationId + '/collections/' + id + '/users', null, true, true); - return new ListResponse(r, SelectionReadOnlyResponse); + return r.map((dr: any) => new SelectionReadOnlyResponse(dr)); } async postCollection(organizationId: string, request: CollectionRequest): Promise { @@ -490,10 +489,10 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, GroupResponse); } - async getGroupUsers(organizationId: string, id: string): Promise> { + async getGroupUsers(organizationId: string, id: string): Promise { const r = await this.send('GET', '/organizations/' + organizationId + '/groups/' + id + '/users', null, true, true); - return new ListResponse(r, GroupUserResponse); + return r; } async postGroup(organizationId: string, request: GroupRequest): Promise { @@ -506,6 +505,10 @@ export class ApiService implements ApiServiceAbstraction { return new GroupResponse(r); } + async putGroupUsers(organizationId: string, id: string, request: string[]): Promise { + await this.send('PUT', '/organizations/' + organizationId + '/groups/' + id + '/users', request, true, false); + } + deleteGroup(organizationId: string, id: string): Promise { return this.send('DELETE', '/organizations/' + organizationId + '/groups/' + id, null, true, false); } From bf48b450104a7eb103d788dfcf819b9725ff0431 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 19 Oct 2018 09:14:11 -0400 Subject: [PATCH 0596/1626] apis for new create with collections --- src/abstractions/api.service.ts | 4 +++- src/abstractions/cipher.service.ts | 2 +- src/models/request/cipherCreateRequest.ts | 13 +++++++++++++ src/services/api.service.ts | 8 +++++++- src/services/cipher.service.ts | 14 ++++++++++---- 5 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 src/models/request/cipherCreateRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 264da1bfa9..abb2f93095 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -4,6 +4,7 @@ import { CipherBulkDeleteRequest } from '../models/request/cipherBulkDeleteReque import { CipherBulkMoveRequest } from '../models/request/cipherBulkMoveRequest'; import { CipherBulkShareRequest } from '../models/request/cipherBulkShareRequest'; import { CipherCollectionsRequest } from '../models/request/cipherCollectionsRequest'; +import { CipherCreateRequest } from '../models/request/cipherCreateRequest'; import { CipherRequest } from '../models/request/cipherRequest'; import { CipherShareRequest } from '../models/request/cipherShareRequest'; import { CollectionRequest } from '../models/request/collectionRequest'; @@ -132,7 +133,8 @@ export abstract class ApiService { getCipherAdmin: (id: string) => Promise; getCiphersOrganization: (organizationId: string) => Promise>; postCipher: (request: CipherRequest) => Promise; - postCipherAdmin: (request: CipherRequest) => Promise; + postCipherCreate: (request: CipherCreateRequest) => Promise; + postCipherAdmin: (request: CipherCreateRequest) => Promise; putCipher: (id: string, request: CipherRequest) => Promise; putCipherAdmin: (id: string, request: CipherRequest) => Promise; deleteCipher: (id: string) => Promise; diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index 71fa641228..1aae907f34 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -25,7 +25,7 @@ export abstract class CipherService { getLastUsedForUrl: (url: string) => Promise; updateLastUsedDate: (id: string) => Promise; saveNeverDomain: (domain: string) => Promise; - saveWithServer: (cipher: Cipher) => Promise; + saveWithServer: (cipher: Cipher, collectionIds?: string[]) => Promise; shareWithServer: (cipher: CipherView, organizationId: string, collectionIds: string[]) => Promise; shareManyWithServer: (ciphers: CipherView[], organizationId: string, collectionIds: string[]) => Promise; shareAttachmentWithServer: (attachmentView: AttachmentView, cipherId: string, diff --git a/src/models/request/cipherCreateRequest.ts b/src/models/request/cipherCreateRequest.ts new file mode 100644 index 0000000000..f9f7a8af5b --- /dev/null +++ b/src/models/request/cipherCreateRequest.ts @@ -0,0 +1,13 @@ +import { CipherRequest } from './cipherRequest'; + +import { Cipher } from '../domain/cipher'; + +export class CipherCreateRequest { + cipher: CipherRequest; + collectionIds: string[]; + + constructor(cipher: Cipher, collectionIds: string[]) { + this.cipher = new CipherRequest(cipher); + this.collectionIds = collectionIds; + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 0206e3d643..8e502c91e0 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -10,6 +10,7 @@ import { CipherBulkDeleteRequest } from '../models/request/cipherBulkDeleteReque import { CipherBulkMoveRequest } from '../models/request/cipherBulkMoveRequest'; import { CipherBulkShareRequest } from '../models/request/cipherBulkShareRequest'; import { CipherCollectionsRequest } from '../models/request/cipherCollectionsRequest'; +import { CipherCreateRequest } from '../models/request/cipherCreateRequest'; import { CipherRequest } from '../models/request/cipherRequest'; import { CipherShareRequest } from '../models/request/cipherShareRequest'; import { CollectionRequest } from '../models/request/collectionRequest'; @@ -337,7 +338,12 @@ export class ApiService implements ApiServiceAbstraction { return new CipherResponse(r); } - async postCipherAdmin(request: CipherRequest): Promise { + async postCipherCreate(request: CipherCreateRequest): Promise { + const r = await this.send('POST', '/ciphers/create', request, true, true); + return new CipherResponse(r); + } + + async postCipherAdmin(request: CipherCreateRequest): Promise { const r = await this.send('POST', '/ciphers/admin', request, true, true); return new CipherResponse(r); } diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 2ceb801dd2..ee696faa22 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -21,6 +21,7 @@ import { CipherBulkDeleteRequest } from '../models/request/cipherBulkDeleteReque import { CipherBulkMoveRequest } from '../models/request/cipherBulkMoveRequest'; import { CipherBulkShareRequest } from '../models/request/cipherBulkShareRequest'; import { CipherCollectionsRequest } from '../models/request/cipherCollectionsRequest'; +import { CipherCreateRequest } from '../models/request/cipherCreateRequest'; import { CipherRequest } from '../models/request/cipherRequest'; import { CipherShareRequest } from '../models/request/cipherShareRequest'; @@ -438,14 +439,19 @@ export class CipherService implements CipherServiceAbstraction { await this.storageService.save(Keys.neverDomains, domains); } - async saveWithServer(cipher: Cipher): Promise { - const request = new CipherRequest(cipher); - + async saveWithServer(cipher: Cipher, collectionIds: string[] = null): Promise { let response: CipherResponse; if (cipher.id == null) { - response = await this.apiService.postCipher(request); + if (collectionIds != null) { + const request = new CipherCreateRequest(cipher, collectionIds); + response = await this.apiService.postCipherCreate(request); + } else { + const request = new CipherRequest(cipher); + response = await this.apiService.postCipher(request); + } cipher.id = response.id; } else { + const request = new CipherRequest(cipher); response = await this.apiService.putCipher(cipher.id, request); } From b1ead78e34abcf5e1701df27f9b7653a00f2e989 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 19 Oct 2018 09:15:42 -0400 Subject: [PATCH 0597/1626] throw error if trying to edit cipher with collection ids --- src/services/cipher.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index ee696faa22..d53c5a0a66 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -451,6 +451,9 @@ export class CipherService implements CipherServiceAbstraction { } cipher.id = response.id; } else { + if (collectionIds != null) { + throw new Error('You cannot edit with collection ids.'); + } const request = new CipherRequest(cipher); response = await this.apiService.putCipher(cipher.id, request); } From 194374ea7302246ec50be13e4baeb7024382f101 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 19 Oct 2018 11:20:04 -0400 Subject: [PATCH 0598/1626] support org/collection selection on cipher add --- src/abstractions/cipher.service.ts | 2 +- src/angular/components/add-edit.component.ts | 46 +++++++++++++++++++- src/models/request/cipherCreateRequest.ts | 4 +- src/services/cipher.service.ts | 9 ++-- 4 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index 1aae907f34..71fa641228 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -25,7 +25,7 @@ export abstract class CipherService { getLastUsedForUrl: (url: string) => Promise; updateLastUsedDate: (id: string) => Promise; saveNeverDomain: (domain: string) => Promise; - saveWithServer: (cipher: Cipher, collectionIds?: string[]) => Promise; + saveWithServer: (cipher: Cipher) => Promise; shareWithServer: (cipher: CipherView, organizationId: string, collectionIds: string[]) => Promise; shareManyWithServer: (ciphers: CipherView[], organizationId: string, collectionIds: string[]) => Promise; shareAttachmentWithServer: (attachmentView: AttachmentView, cipherId: string, diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 345a22740a..c7e6c7cf86 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -1,25 +1,30 @@ import { EventEmitter, Input, + OnInit, Output, } from '@angular/core'; import { CipherType } from '../../enums/cipherType'; import { FieldType } from '../../enums/fieldType'; +import { OrganizationUserStatusType } from '../../enums/organizationUserStatusType'; import { SecureNoteType } from '../../enums/secureNoteType'; import { UriMatchType } from '../../enums/uriMatchType'; import { AuditService } from '../../abstractions/audit.service'; import { CipherService } from '../../abstractions/cipher.service'; +import { CollectionService } from '../../abstractions/collection.service'; import { FolderService } from '../../abstractions/folder.service'; import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { StateService } from '../../abstractions/state.service'; +import { UserService } from '../../abstractions/user.service'; import { Cipher } from '../../models/domain/cipher'; import { CardView } from '../../models/view/cardView'; import { CipherView } from '../../models/view/cipherView'; +import { CollectionView } from '../../models/view/collectionView'; import { FieldView } from '../../models/view/fieldView'; import { FolderView } from '../../models/view/folderView'; import { IdentityView } from '../../models/view/identityView'; @@ -27,7 +32,9 @@ import { LoginUriView } from '../../models/view/loginUriView'; import { LoginView } from '../../models/view/loginView'; import { SecureNoteView } from '../../models/view/secureNoteView'; -export class AddEditComponent { +import { Utils } from '../../misc/utils'; + +export class AddEditComponent implements OnInit { @Input() folderId: string = null; @Input() cipherId: string; @Input() type: CipherType; @@ -40,6 +47,7 @@ export class AddEditComponent { editMode: boolean = false; cipher: CipherView; folders: FolderView[]; + collections: CollectionView[] = []; title: string; formPromise: Promise; deletePromise: Promise; @@ -55,10 +63,14 @@ export class AddEditComponent { identityTitleOptions: any[]; addFieldTypeOptions: any[]; uriMatchOptions: any[]; + ownershipOptions: any[] = []; + + private writeableCollections: CollectionView[]; constructor(protected cipherService: CipherService, protected folderService: FolderService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, - protected auditService: AuditService, protected stateService: StateService) { + protected auditService: AuditService, protected stateService: StateService, + protected userService: UserService, protected collectionService: CollectionService) { this.typeOptions = [ { name: i18nService.t('typeLogin'), value: CipherType.Login }, { name: i18nService.t('typeCard'), value: CipherType.Card }, @@ -115,6 +127,19 @@ export class AddEditComponent { ]; } + async ngOnInit() { + const myEmail = await this.userService.getEmail(); + this.ownershipOptions.push({ name: myEmail, value: null }); + const orgs = await this.userService.getAllOrganizations(); + orgs.sort(Utils.getSortFunction(this.i18nService, 'name')).forEach((o) => { + if (o.enabled && o.status === OrganizationUserStatusType.Confirmed) { + this.ownershipOptions.push({ name: o.name, value: o.id }); + } + }); + const allCollections = await this.collectionService.getAllDecrypted(); + this.writeableCollections = allCollections.filter((c) => !c.readOnly); + } + async load() { this.editMode = this.cipherId != null; if (this.editMode) { @@ -132,6 +157,7 @@ export class AddEditComponent { this.cipher = await cipher.decrypt(); } else { this.cipher = new CipherView(); + this.cipher.organizationId = null; this.cipher.folderId = this.folderId; this.cipher.type = this.type == null ? CipherType.Login : this.type; this.cipher.login = new LoginView(); @@ -159,6 +185,11 @@ export class AddEditComponent { this.cipher.login.uris = null; } + if (!this.editMode && this.cipher.organizationId != null) { + this.cipher.collectionIds = this.collections == null ? [] : + this.collections.filter((c) => (c as any).checked).map((c) => c.id); + } + const cipher = await this.encryptCipher(); try { this.formPromise = this.saveCipher(cipher); @@ -282,6 +313,17 @@ export class AddEditComponent { u.showOptions = u.showOptions == null ? true : u.showOptions; } + organizationChanged() { + if (this.writeableCollections != null) { + this.writeableCollections.forEach((c) => (c as any).checked = false); + } + if (this.cipher.organizationId != null) { + this.collections = this.writeableCollections.filter((c) => c.organizationId === this.cipher.organizationId); + } else { + this.collections = []; + } + } + async checkPassword() { if (this.checkPasswordPromise != null) { return; diff --git a/src/models/request/cipherCreateRequest.ts b/src/models/request/cipherCreateRequest.ts index f9f7a8af5b..683d0a52ef 100644 --- a/src/models/request/cipherCreateRequest.ts +++ b/src/models/request/cipherCreateRequest.ts @@ -6,8 +6,8 @@ export class CipherCreateRequest { cipher: CipherRequest; collectionIds: string[]; - constructor(cipher: Cipher, collectionIds: string[]) { + constructor(cipher: Cipher) { this.cipher = new CipherRequest(cipher); - this.collectionIds = collectionIds; + this.collectionIds = cipher.collectionIds; } } diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index d53c5a0a66..3428851195 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -439,11 +439,11 @@ export class CipherService implements CipherServiceAbstraction { await this.storageService.save(Keys.neverDomains, domains); } - async saveWithServer(cipher: Cipher, collectionIds: string[] = null): Promise { + async saveWithServer(cipher: Cipher): Promise { let response: CipherResponse; if (cipher.id == null) { - if (collectionIds != null) { - const request = new CipherCreateRequest(cipher, collectionIds); + if (cipher.collectionIds != null) { + const request = new CipherCreateRequest(cipher); response = await this.apiService.postCipherCreate(request); } else { const request = new CipherRequest(cipher); @@ -451,9 +451,6 @@ export class CipherService implements CipherServiceAbstraction { } cipher.id = response.id; } else { - if (collectionIds != null) { - throw new Error('You cannot edit with collection ids.'); - } const request = new CipherRequest(cipher); response = await this.apiService.putCipher(cipher.id, request); } From c946f01b5fbe5b84b928ae8dd4ad54a15dd1803d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 19 Oct 2018 12:18:53 -0400 Subject: [PATCH 0599/1626] set organizationUseTotp on organizationChanged --- src/angular/components/add-edit.component.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index c7e6c7cf86..3da4a5ec96 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -313,12 +313,16 @@ export class AddEditComponent implements OnInit { u.showOptions = u.showOptions == null ? true : u.showOptions; } - organizationChanged() { + async organizationChanged() { if (this.writeableCollections != null) { this.writeableCollections.forEach((c) => (c as any).checked = false); } if (this.cipher.organizationId != null) { this.collections = this.writeableCollections.filter((c) => c.organizationId === this.cipher.organizationId); + const org = await this.userService.getOrganization(this.cipher.organizationId); + if (org != null) { + this.cipher.organizationUseTotp = org.useTotp; + } } else { this.collections = []; } From 3021afc9ddae1579f2226010ee487fa3edcddb0b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 22 Oct 2018 14:47:49 -0400 Subject: [PATCH 0600/1626] protected loadCollections for add/edit --- src/angular/components/add-edit.component.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 3da4a5ec96..89f94d4864 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -136,8 +136,7 @@ export class AddEditComponent implements OnInit { this.ownershipOptions.push({ name: o.name, value: o.id }); } }); - const allCollections = await this.collectionService.getAllDecrypted(); - this.writeableCollections = allCollections.filter((c) => !c.readOnly); + this.writeableCollections = await this.loadCollections(); } async load() { @@ -350,6 +349,11 @@ export class AddEditComponent implements OnInit { } } + protected async loadCollections() { + const allCollections = await this.collectionService.getAllDecrypted(); + return allCollections.filter((c) => !c.readOnly); + } + protected loadCipher() { return this.cipherService.get(this.cipherId); } From 0a36a211c355ada433ce9b0560c5ab85a6cead23 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 22 Oct 2018 16:46:33 -0400 Subject: [PATCH 0601/1626] allow setting org id and collections from input --- src/angular/components/add-edit.component.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 89f94d4864..059e702592 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -38,6 +38,8 @@ export class AddEditComponent implements OnInit { @Input() folderId: string = null; @Input() cipherId: string; @Input() type: CipherType; + @Input() collectionIds: string[]; + @Input() organizationId: string = null; @Output() onSavedCipher = new EventEmitter(); @Output() onDeletedCipher = new EventEmitter(); @Output() onCancelled = new EventEmitter(); @@ -156,7 +158,7 @@ export class AddEditComponent implements OnInit { this.cipher = await cipher.decrypt(); } else { this.cipher = new CipherView(); - this.cipher.organizationId = null; + this.cipher.organizationId = this.organizationId == null ? null : this.organizationId; this.cipher.folderId = this.folderId; this.cipher.type = this.type == null ? CipherType.Login : this.type; this.cipher.login = new LoginView(); @@ -165,6 +167,15 @@ export class AddEditComponent implements OnInit { this.cipher.identity = new IdentityView(); this.cipher.secureNote = new SecureNoteView(); this.cipher.secureNote.type = SecureNoteType.Generic; + + await this.organizationChanged(); + if (this.collectionIds != null && this.collectionIds.length > 0 && this.collections.length > 0) { + this.collections.forEach((c) => { + if (this.collectionIds.indexOf(c.id) > -1) { + (c as any).checked = true; + } + }); + } } } From 89c23522d5697c722dcbe7d074cd12ebb6dc8783 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 23 Oct 2018 10:22:53 -0400 Subject: [PATCH 0602/1626] share component --- src/angular/components/add-edit.component.ts | 2 +- src/angular/components/share.component.ts | 99 ++++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 src/angular/components/share.component.ts diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 059e702592..25071de329 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -67,7 +67,7 @@ export class AddEditComponent implements OnInit { uriMatchOptions: any[]; ownershipOptions: any[] = []; - private writeableCollections: CollectionView[]; + protected writeableCollections: CollectionView[]; constructor(protected cipherService: CipherService, protected folderService: FolderService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, diff --git a/src/angular/components/share.component.ts b/src/angular/components/share.component.ts new file mode 100644 index 0000000000..0bd64784fb --- /dev/null +++ b/src/angular/components/share.component.ts @@ -0,0 +1,99 @@ +import { + EventEmitter, + Input, + OnInit, + Output, +} from '@angular/core'; + +import { CipherService } from '../../abstractions/cipher.service'; +import { CollectionService } from '../../abstractions/collection.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { UserService } from '../../abstractions/user.service'; + +import { Organization } from '../../models/domain/organization'; +import { CipherView } from '../../models/view/cipherView'; +import { CollectionView } from '../../models/view/collectionView'; + +import { Utils } from '../../misc/utils'; + +export class ShareComponent implements OnInit { + @Input() cipherId: string; + @Input() organizationId: string; + @Output() onSharedCipher = new EventEmitter(); + + formPromise: Promise; + cipher: CipherView; + collections: CollectionView[] = []; + organizations: Organization[] = []; + + protected writeableCollections: CollectionView[] = []; + + constructor(protected collectionService: CollectionService, protected platformUtilsService: PlatformUtilsService, + protected i18nService: I18nService, protected userService: UserService, + protected cipherService: CipherService) { } + + async ngOnInit() { + await this.load(); + } + + async load() { + const allCollections = await this.collectionService.getAllDecrypted(); + this.writeableCollections = allCollections.map((c) => c).filter((c) => !c.readOnly) + .sort(Utils.getSortFunction(this.i18nService, 'name')); + const orgs = await this.userService.getAllOrganizations(); + this.organizations = orgs.sort(Utils.getSortFunction(this.i18nService, 'name')); + + const cipherDomain = await this.cipherService.get(this.cipherId); + this.cipher = await cipherDomain.decrypt(); + if (this.organizationId == null && this.organizations.length > 0) { + this.organizationId = this.organizations[0].id; + } + this.filterCollections(); + } + + filterCollections() { + this.writeableCollections.forEach((c) => (c as any).checked = false); + if (this.organizationId == null || this.writeableCollections.length === 0) { + this.collections = []; + } else { + this.collections = this.writeableCollections.filter((c) => c.organizationId === this.organizationId); + } + } + + async submit() { + const cipherDomain = await this.cipherService.get(this.cipherId); + const cipherView = await cipherDomain.decrypt(); + + const attachmentPromises: Array> = []; + if (cipherView.attachments != null) { + for (const attachment of cipherView.attachments) { + const promise = this.cipherService.shareAttachmentWithServer(attachment, cipherView.id, + this.organizationId); + attachmentPromises.push(promise); + } + } + + const checkedCollectionIds = this.collections.filter((c) => (c as any).checked).map((c) => c.id); + try { + this.formPromise = Promise.all(attachmentPromises).then(async () => { + await this.cipherService.shareWithServer(cipherView, this.organizationId, checkedCollectionIds); + this.onSharedCipher.emit(); + this.platformUtilsService.eventTrack('Shared Cipher'); + this.platformUtilsService.showToast('success', null, this.i18nService.t('sharedItem')); + }); + await this.formPromise; + } catch { } + } + + get canSave() { + if (this.collections != null) { + for (let i = 0; i < this.collections.length; i++) { + if ((this.collections[i] as any).checked) { + return true; + } + } + } + return false; + } +} From 8e377050e9bfddae46fa0a167771b670593eca16 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 23 Oct 2018 12:04:21 -0400 Subject: [PATCH 0603/1626] collections component to jslib --- .../components/collections.component.ts | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/angular/components/collections.component.ts diff --git a/src/angular/components/collections.component.ts b/src/angular/components/collections.component.ts new file mode 100644 index 0000000000..379f5b5747 --- /dev/null +++ b/src/angular/components/collections.component.ts @@ -0,0 +1,79 @@ +import { + EventEmitter, + Input, + OnInit, + Output, +} from '@angular/core'; + +import { CipherService } from '../../abstractions/cipher.service'; +import { CollectionService } from '../../abstractions/collection.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +import { CipherView } from '../../models/view/cipherView'; +import { CollectionView } from '../../models/view/collectionView'; + +import { Cipher } from '../../models/domain/cipher'; + +export class CollectionsComponent implements OnInit { + @Input() cipherId: string; + @Output() onSavedCollections = new EventEmitter(); + + formPromise: Promise; + cipher: CipherView; + collectionIds: string[]; + collections: CollectionView[] = []; + + protected cipherDomain: Cipher; + + constructor(protected collectionService: CollectionService, protected platformUtilsService: PlatformUtilsService, + protected i18nService: I18nService, protected cipherService: CipherService) { } + + async ngOnInit() { + await this.load(); + } + + async load() { + this.cipherDomain = await this.loadCipher(); + this.collectionIds = this.loadCipherCollections(); + this.cipher = await this.cipherDomain.decrypt(); + this.collections = await this.loadCollections(); + + this.collections.forEach((c) => (c as any).checked = false); + if (this.collectionIds != null) { + this.collections.forEach((c) => { + (c as any).checked = this.collectionIds != null && this.collectionIds.indexOf(c.id) > -1; + }); + } + } + + async submit() { + this.cipherDomain.collectionIds = this.collections + .filter((c) => !!(c as any).checked) + .map((c) => c.id); + try { + this.formPromise = this.saveCollections(); + await this.formPromise; + this.onSavedCollections.emit(); + this.platformUtilsService.eventTrack('Edited Cipher Collections'); + this.platformUtilsService.showToast('success', null, this.i18nService.t('editedItem')); + } catch { } + } + + protected loadCipher() { + return this.cipherService.get(this.cipherId); + } + + protected loadCipherCollections() { + return this.cipherDomain.collectionIds; + } + + protected async loadCollections() { + const allCollections = await this.collectionService.getAllDecrypted(); + return allCollections.filter((c) => !c.readOnly && c.organizationId === this.cipher.organizationId); + } + + protected saveCollections() { + return this.cipherService.saveCollectionsWithServer(this.cipherDomain); + } +} From 2f510a798853ef3adfed7e8285c9d3f54eba493c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 23 Oct 2018 15:42:20 -0400 Subject: [PATCH 0604/1626] emit events for share and collections --- src/angular/components/add-edit.component.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 25071de329..3a2f42ed07 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -44,6 +44,8 @@ export class AddEditComponent implements OnInit { @Output() onDeletedCipher = new EventEmitter(); @Output() onCancelled = new EventEmitter(); @Output() onEditAttachments = new EventEmitter(); + @Output() onShareCipher = new EventEmitter(); + @Output() onEditCollections = new EventEmitter(); @Output() onGeneratePassword = new EventEmitter(); editMode: boolean = false; @@ -263,6 +265,14 @@ export class AddEditComponent implements OnInit { this.onEditAttachments.emit(this.cipher); } + share() { + this.onShareCipher.emit(this.cipher); + } + + editCollections() { + this.onEditCollections.emit(this.cipher); + } + async delete(): Promise { const confirmed = await this.platformUtilsService.showDialog( this.i18nService.t('deleteItemConfirmation'), this.i18nService.t('deleteItem'), From 43c0cbce452daff9bcc4c70866a56c8cbd548b4a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 23 Oct 2018 16:16:59 -0400 Subject: [PATCH 0605/1626] save share response --- src/abstractions/api.service.ts | 2 +- src/services/api.service.ts | 5 +++-- src/services/cipher.service.ts | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index abb2f93095..ee27049121 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -141,7 +141,7 @@ export abstract class ApiService { deleteCipherAdmin: (id: string) => Promise; deleteManyCiphers: (request: CipherBulkDeleteRequest) => Promise; putMoveCiphers: (request: CipherBulkMoveRequest) => Promise; - putShareCipher: (id: string, request: CipherShareRequest) => Promise; + putShareCipher: (id: string, request: CipherShareRequest) => Promise; putShareCiphers: (request: CipherBulkShareRequest) => Promise; putCipherCollections: (id: string, request: CipherCollectionsRequest) => Promise; putCipherCollectionsAdmin: (id: string, request: CipherCollectionsRequest) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 8e502c91e0..40c7c6c78c 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -374,8 +374,9 @@ export class ApiService implements ApiServiceAbstraction { return this.send('PUT', '/ciphers/move', request, true, false); } - putShareCipher(id: string, request: CipherShareRequest): Promise { - return this.send('PUT', '/ciphers/' + id + '/share', request, true, false); + async putShareCipher(id: string, request: CipherShareRequest): Promise { + const r = await this.send('PUT', '/ciphers/' + id + '/share', request, true, true); + return new CipherResponse(r); } putShareCiphers(request: CipherBulkShareRequest): Promise { diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 3428851195..6cff911e34 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -465,9 +465,10 @@ export class CipherService implements CipherServiceAbstraction { cipher.collectionIds = collectionIds; const encCipher = await this.encrypt(cipher); const request = new CipherShareRequest(encCipher); - await this.apiService.putShareCipher(cipher.id, request); + const response = await this.apiService.putShareCipher(cipher.id, request); const userId = await this.userService.getUserId(); - await this.upsert(encCipher.toCipherData(userId)); + const data = new CipherData(response, userId, collectionIds); + await this.upsert(data); } async shareManyWithServer(ciphers: CipherView[], organizationId: string, collectionIds: string[]): Promise { From 4165a78277048d7b37319e63bd7e6473cbba5156 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 23 Oct 2018 22:10:08 -0400 Subject: [PATCH 0606/1626] move attachment sharing logic to service --- src/abstractions/cipher.service.ts | 2 - src/angular/components/share.component.ts | 21 ++---- src/services/cipher.service.ts | 80 ++++++++++++----------- 3 files changed, 49 insertions(+), 54 deletions(-) diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index 71fa641228..7a0fab508f 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -28,8 +28,6 @@ export abstract class CipherService { saveWithServer: (cipher: Cipher) => Promise; shareWithServer: (cipher: CipherView, organizationId: string, collectionIds: string[]) => Promise; shareManyWithServer: (ciphers: CipherView[], organizationId: string, collectionIds: string[]) => Promise; - shareAttachmentWithServer: (attachmentView: AttachmentView, cipherId: string, - organizationId: string) => Promise; saveAttachmentWithServer: (cipher: Cipher, unencryptedFile: any, admin?: boolean) => Promise; saveAttachmentRawWithServer: (cipher: Cipher, filename: string, data: ArrayBuffer, admin?: boolean) => Promise; diff --git a/src/angular/components/share.component.ts b/src/angular/components/share.component.ts index 0bd64784fb..d0c7dea4fa 100644 --- a/src/angular/components/share.component.ts +++ b/src/angular/components/share.component.ts @@ -65,23 +65,14 @@ export class ShareComponent implements OnInit { const cipherDomain = await this.cipherService.get(this.cipherId); const cipherView = await cipherDomain.decrypt(); - const attachmentPromises: Array> = []; - if (cipherView.attachments != null) { - for (const attachment of cipherView.attachments) { - const promise = this.cipherService.shareAttachmentWithServer(attachment, cipherView.id, - this.organizationId); - attachmentPromises.push(promise); - } - } - const checkedCollectionIds = this.collections.filter((c) => (c as any).checked).map((c) => c.id); try { - this.formPromise = Promise.all(attachmentPromises).then(async () => { - await this.cipherService.shareWithServer(cipherView, this.organizationId, checkedCollectionIds); - this.onSharedCipher.emit(); - this.platformUtilsService.eventTrack('Shared Cipher'); - this.platformUtilsService.showToast('success', null, this.i18nService.t('sharedItem')); - }); + this.formPromise = this.cipherService.shareWithServer(cipherView, this.organizationId, + checkedCollectionIds).then(async () => { + this.onSharedCipher.emit(); + this.platformUtilsService.eventTrack('Shared Cipher'); + this.platformUtilsService.showToast('success', null, this.i18nService.t('sharedItem')); + }); await this.formPromise; } catch { } } diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 6cff911e34..257d59b4a6 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -461,6 +461,14 @@ export class CipherService implements CipherServiceAbstraction { } async shareWithServer(cipher: CipherView, organizationId: string, collectionIds: string[]): Promise { + const attachmentPromises: Array> = []; + if (cipher.attachments != null) { + cipher.attachments.forEach((attachment) => { + attachmentPromises.push(this.shareAttachmentWithServer(attachment, cipher.id, organizationId)); + }); + } + await Promise.all(attachmentPromises); + cipher.organizationId = organizationId; cipher.collectionIds = collectionIds; const encCipher = await this.encrypt(cipher); @@ -488,43 +496,6 @@ export class CipherService implements CipherServiceAbstraction { await this.upsert(encCiphers.map((c) => c.toCipherData(userId))); } - async shareAttachmentWithServer(attachmentView: AttachmentView, cipherId: string, - organizationId: string): Promise { - const attachmentResponse = await fetch(new Request(attachmentView.url, { cache: 'no-cache' })); - if (attachmentResponse.status !== 200) { - throw Error('Failed to download attachment: ' + attachmentResponse.status.toString()); - } - - const buf = await attachmentResponse.arrayBuffer(); - const decBuf = await this.cryptoService.decryptFromBytes(buf, null); - const key = await this.cryptoService.getOrgKey(organizationId); - const encData = await this.cryptoService.encryptToBytes(decBuf, key); - const encFileName = await this.cryptoService.encrypt(attachmentView.fileName, key); - - const fd = new FormData(); - try { - const blob = new Blob([encData], { type: 'application/octet-stream' }); - fd.append('data', blob, encFileName.encryptedString); - } catch (e) { - if (Utils.isNode && !Utils.isBrowser) { - fd.append('data', Buffer.from(encData) as any, { - filepath: encFileName.encryptedString, - contentType: 'application/octet-stream', - } as any); - } else { - throw e; - } - } - - let response: CipherResponse; - try { - response = await this.apiService.postShareCipherAttachment(cipherId, attachmentView.id, fd, - organizationId); - } catch (e) { - throw new Error((e as ErrorResponse).getSingleMessage()); - } - } - saveAttachmentWithServer(cipher: Cipher, unencryptedFile: any, admin = false): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); @@ -772,6 +743,41 @@ export class CipherService implements CipherServiceAbstraction { // Helpers + private async shareAttachmentWithServer(attachmentView: AttachmentView, cipherId: string, + organizationId: string): Promise { + const attachmentResponse = await fetch(new Request(attachmentView.url, { cache: 'no-cache' })); + if (attachmentResponse.status !== 200) { + throw Error('Failed to download attachment: ' + attachmentResponse.status.toString()); + } + + const buf = await attachmentResponse.arrayBuffer(); + const decBuf = await this.cryptoService.decryptFromBytes(buf, null); + const key = await this.cryptoService.getOrgKey(organizationId); + const encData = await this.cryptoService.encryptToBytes(decBuf, key); + const encFileName = await this.cryptoService.encrypt(attachmentView.fileName, key); + + const fd = new FormData(); + try { + const blob = new Blob([encData], { type: 'application/octet-stream' }); + fd.append('data', blob, encFileName.encryptedString); + } catch (e) { + if (Utils.isNode && !Utils.isBrowser) { + fd.append('data', Buffer.from(encData) as any, { + filepath: encFileName.encryptedString, + contentType: 'application/octet-stream', + } as any); + } else { + throw e; + } + } + + try { + await this.apiService.postShareCipherAttachment(cipherId, attachmentView.id, fd, organizationId); + } catch (e) { + throw new Error((e as ErrorResponse).getSingleMessage()); + } + } + private async encryptObjProperty(model: V, obj: D, map: any, key: SymmetricCryptoKey): Promise { const promises = []; From 06f129e2c1cac2bfeb566e3ec59db0fd51dd730d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 25 Oct 2018 08:20:31 -0400 Subject: [PATCH 0607/1626] default type is login --- src/importers/bitwardenCsvImporter.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/importers/bitwardenCsvImporter.ts b/src/importers/bitwardenCsvImporter.ts index c8414f5ac2..3da6c338b6 100644 --- a/src/importers/bitwardenCsvImporter.ts +++ b/src/importers/bitwardenCsvImporter.ts @@ -85,8 +85,12 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer { const valueType = value.type != null ? value.type.toLowerCase() : null; switch (valueType) { - case 'login': - case null: + case 'note': + cipher.type = CipherType.SecureNote; + cipher.secureNote = new SecureNoteView(); + cipher.secureNote.type = SecureNoteType.Generic; + break; + default: cipher.type = CipherType.Login; cipher.login = new LoginView(); cipher.login.totp = this.getValueOrDefault(value.login_totp || value.totp); @@ -95,13 +99,6 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer { const uris = this.parseSingleRowCsv(value.login_uri || value.uri); cipher.login.uris = this.makeUriArray(uris); break; - case 'note': - cipher.type = CipherType.SecureNote; - cipher.secureNote = new SecureNoteView(); - cipher.secureNote.type = SecureNoteType.Generic; - break; - default: - break; } result.ciphers.push(cipher); From 6aba4550a4b1e643528392d6305df00977046232 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 25 Oct 2018 09:38:37 -0400 Subject: [PATCH 0608/1626] Nested folders --- src/abstractions/folder.service.ts | 2 + src/angular/components/groupings.component.ts | 7 +++ src/models/domain/treeNode.ts | 8 ++++ src/services/folder.service.ts | 47 +++++++++++++++++++ 4 files changed, 64 insertions(+) create mode 100644 src/models/domain/treeNode.ts diff --git a/src/abstractions/folder.service.ts b/src/abstractions/folder.service.ts index 1fec203798..fa1c6809de 100644 --- a/src/abstractions/folder.service.ts +++ b/src/abstractions/folder.service.ts @@ -2,6 +2,7 @@ import { FolderData } from '../models/data/folderData'; import { Folder } from '../models/domain/folder'; import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; +import { TreeNode } from '../models/domain/treeNode'; import { FolderView } from '../models/view/folderView'; @@ -13,6 +14,7 @@ export abstract class FolderService { get: (id: string) => Promise; getAll: () => Promise; getAllDecrypted: () => Promise; + getAllNested: () => Promise>>; saveWithServer: (folder: Folder) => Promise; upsert: (folder: FolderData | FolderData[]) => Promise; replace: (folders: { [id: string]: FolderData; }) => Promise; diff --git a/src/angular/components/groupings.component.ts b/src/angular/components/groupings.component.ts index 88ea88d88e..4643f3490b 100644 --- a/src/angular/components/groupings.component.ts +++ b/src/angular/components/groupings.component.ts @@ -9,11 +9,14 @@ import { CipherType } from '../../enums/cipherType'; import { CollectionView } from '../../models/view/collectionView'; import { FolderView } from '../../models/view/folderView'; +import { TreeNode } from '../../models/domain/treeNode'; + import { CollectionService } from '../../abstractions/collection.service'; import { FolderService } from '../../abstractions/folder.service'; export class GroupingsComponent { @Input() showFolders = true; + @Input() loadNestedFolder = false; @Input() showCollections = true; @Input() showFavorites = true; @@ -26,6 +29,7 @@ export class GroupingsComponent { @Output() onCollectionClicked = new EventEmitter(); folders: FolderView[]; + nestedFolders: Array>; collections: CollectionView[]; loaded: boolean = false; cipherType = CipherType; @@ -64,6 +68,9 @@ export class GroupingsComponent { return; } this.folders = await this.folderService.getAllDecrypted(); + if (this.loadNestedFolder) { + this.nestedFolders = await this.folderService.getAllNested(); + } } selectAll() { diff --git a/src/models/domain/treeNode.ts b/src/models/domain/treeNode.ts new file mode 100644 index 0000000000..4463c50fb4 --- /dev/null +++ b/src/models/domain/treeNode.ts @@ -0,0 +1,8 @@ +export class TreeNode { + node: T; + children: Array> = []; + + constructor(node: T) { + this.node = node; + } +} diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index 34cecf775c..07dc79024a 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -19,11 +19,13 @@ import { UserService } from '../abstractions/user.service'; import { CipherData } from '../models/data/cipherData'; import { Utils } from '../misc/utils'; +import { TreeNode } from '../models/domain/treeNode'; const Keys = { foldersPrefix: 'folders_', ciphersPrefix: 'ciphers_', }; +const NestingDelimiter = '/'; export class FolderService implements FolderServiceAbstraction { decryptedFolderCache: FolderView[]; @@ -95,6 +97,18 @@ export class FolderService implements FolderServiceAbstraction { return this.decryptedFolderCache; } + async getAllNested(): Promise>> { + const folders = await this.getAllDecrypted(); + const nodes: Array> = []; + folders.forEach((f) => { + const folderCopy = new FolderView(); + folderCopy.id = f.id; + folderCopy.revisionDate = f.revisionDate; + this.nestedTraverse(nodes, 0, f.name.split(NestingDelimiter), folderCopy); + }); + return nodes; + } + async saveWithServer(folder: Folder): Promise { const request = new FolderRequest(folder); @@ -185,4 +199,37 @@ export class FolderService implements FolderServiceAbstraction { await this.apiService.deleteFolder(id); await this.delete(id); } + + private nestedTraverse(nodeTree: Array>, partIndex: number, + parts: string[], folder: FolderView) { + if (parts.length <= partIndex) { + return; + } + + const end = partIndex === parts.length - 1; + const partName = parts[partIndex]; + + for (let i = 0; i < nodeTree.length; i++) { + if (nodeTree[i].node.name === parts[partIndex]) { + if (end && nodeTree[i].node.id !== folder.id) { + // Another node with the same name. + folder.name = partName; + nodeTree.push(new TreeNode(folder)); + return; + } + this.nestedTraverse(nodeTree[i].children, partIndex + 1, parts, folder); + return; + } + } + + if (nodeTree.filter((n) => n.node.name === partName).length === 0) { + if (end) { + folder.name = partName; + nodeTree.push(new TreeNode(folder)); + return; + } + const newPartName = parts[partIndex] + NestingDelimiter + parts[partIndex + 1]; + this.nestedTraverse(nodeTree, 0, [newPartName, ...parts.slice(partIndex + 2)], folder); + } + } } From 59f0549072ab4ace78a7ad5c0a0f97a08b1620e6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 25 Oct 2018 12:09:22 -0400 Subject: [PATCH 0609/1626] getAllNested for collections too. added treenodeobject interface --- src/abstractions/collection.service.ts | 2 ++ src/misc/serviceUtils.ts | 37 +++++++++++++++++++++++++ src/models/domain/treeNode.ts | 10 +++++-- src/models/view/collectionView.ts | 3 +- src/models/view/folderView.ts | 3 +- src/services/collection.service.ts | 16 +++++++++++ src/services/folder.service.ts | 38 ++------------------------ 7 files changed, 70 insertions(+), 39 deletions(-) create mode 100644 src/misc/serviceUtils.ts diff --git a/src/abstractions/collection.service.ts b/src/abstractions/collection.service.ts index a6decc2c09..4847a816f1 100644 --- a/src/abstractions/collection.service.ts +++ b/src/abstractions/collection.service.ts @@ -1,6 +1,7 @@ import { CollectionData } from '../models/data/collectionData'; import { Collection } from '../models/domain/collection'; +import { TreeNode } from '../models/domain/treeNode'; import { CollectionView } from '../models/view/collectionView'; @@ -13,6 +14,7 @@ export abstract class CollectionService { get: (id: string) => Promise; getAll: () => Promise; getAllDecrypted: () => Promise; + getAllNested: (collections?: CollectionView[]) => Promise>>; upsert: (collection: CollectionData | CollectionData[]) => Promise; replace: (collections: { [id: string]: CollectionData; }) => Promise; clear: (userId: string) => Promise; diff --git a/src/misc/serviceUtils.ts b/src/misc/serviceUtils.ts new file mode 100644 index 0000000000..550a402925 --- /dev/null +++ b/src/misc/serviceUtils.ts @@ -0,0 +1,37 @@ +import { + ITreeNodeObject, + TreeNode, +} from '../models/domain/treeNode'; + +export class ServiceUtils { + static nestedTraverse(nodeTree: Array>, partIndex: number, parts: string[], + obj: ITreeNodeObject, delimiter: string) { + if (parts.length <= partIndex) { + return; + } + + const end = partIndex === parts.length - 1; + const partName = parts[partIndex]; + + for (let i = 0; i < nodeTree.length; i++) { + if (nodeTree[i].node.name === parts[partIndex]) { + if (end && nodeTree[i].node.id !== obj.id) { + // Another node with the same name. + nodeTree.push(new TreeNode(obj, partName)); + return; + } + this.nestedTraverse(nodeTree[i].children, partIndex + 1, parts, obj, delimiter); + return; + } + } + + if (nodeTree.filter((n) => n.node.name === partName).length === 0) { + if (end) { + nodeTree.push(new TreeNode(obj, partName)); + return; + } + const newPartName = parts[partIndex] + delimiter + parts[partIndex + 1]; + this.nestedTraverse(nodeTree, 0, [newPartName, ...parts.slice(partIndex + 2)], obj, delimiter); + } + } +} diff --git a/src/models/domain/treeNode.ts b/src/models/domain/treeNode.ts index 4463c50fb4..ee020c2980 100644 --- a/src/models/domain/treeNode.ts +++ b/src/models/domain/treeNode.ts @@ -1,8 +1,14 @@ -export class TreeNode { +export class TreeNode { node: T; children: Array> = []; - constructor(node: T) { + constructor(node: T, name: string) { this.node = node; + this.node.name = name; } } + +export interface ITreeNodeObject { + id: string; + name: string; +} diff --git a/src/models/view/collectionView.ts b/src/models/view/collectionView.ts index 1c625182d4..f667f742db 100644 --- a/src/models/view/collectionView.ts +++ b/src/models/view/collectionView.ts @@ -1,8 +1,9 @@ import { View } from './view'; import { Collection } from '../domain/collection'; +import { ITreeNodeObject } from '../domain/treeNode'; -export class CollectionView implements View { +export class CollectionView implements View, ITreeNodeObject { id: string; organizationId: string; name: string; diff --git a/src/models/view/folderView.ts b/src/models/view/folderView.ts index a0fa99b48d..a61091b2fd 100644 --- a/src/models/view/folderView.ts +++ b/src/models/view/folderView.ts @@ -1,8 +1,9 @@ import { View } from './view'; import { Folder } from '../domain/folder'; +import { ITreeNodeObject } from '../domain/treeNode'; -export class FolderView implements View { +export class FolderView implements View, ITreeNodeObject { id: string = null; name: string; revisionDate: Date; diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index 152d91713b..42a0f48361 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -1,6 +1,7 @@ import { CollectionData } from '../models/data/collectionData'; import { Collection } from '../models/domain/collection'; +import { TreeNode } from '../models/domain/treeNode'; import { CollectionView } from '../models/view/collectionView'; @@ -10,11 +11,13 @@ import { I18nService } from '../abstractions/i18n.service'; import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; +import { ServiceUtils } from '../misc/serviceUtils'; import { Utils } from '../misc/utils'; const Keys = { collectionsPrefix: 'collections_', }; +const NestingDelimiter = '/'; export class CollectionService implements CollectionServiceAbstraction { decryptedCollectionCache: CollectionView[]; @@ -95,6 +98,19 @@ export class CollectionService implements CollectionServiceAbstraction { return this.decryptedCollectionCache; } + async getAllNested(collections: CollectionView[] = null): Promise>> { + if (collections == null) { + collections = await this.getAllDecrypted(); + } + const nodes: Array> = []; + collections.forEach((f) => { + const collectionCopy = new CollectionView(); + collectionCopy.id = f.id; + ServiceUtils.nestedTraverse(nodes, 0, f.name.split(NestingDelimiter), collectionCopy, NestingDelimiter); + }); + return nodes; + } + async upsert(collection: CollectionData | CollectionData[]): Promise { const userId = await this.userService.getUserId(); let collections = await this.storageService.get<{ [id: string]: CollectionData; }>( diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index 07dc79024a..6ca6875de0 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -2,6 +2,7 @@ import { FolderData } from '../models/data/folderData'; import { Folder } from '../models/domain/folder'; import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; +import { TreeNode } from '../models/domain/treeNode'; import { FolderRequest } from '../models/request/folderRequest'; @@ -18,8 +19,8 @@ import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; import { CipherData } from '../models/data/cipherData'; +import { ServiceUtils } from '../misc/serviceUtils'; import { Utils } from '../misc/utils'; -import { TreeNode } from '../models/domain/treeNode'; const Keys = { foldersPrefix: 'folders_', @@ -104,7 +105,7 @@ export class FolderService implements FolderServiceAbstraction { const folderCopy = new FolderView(); folderCopy.id = f.id; folderCopy.revisionDate = f.revisionDate; - this.nestedTraverse(nodes, 0, f.name.split(NestingDelimiter), folderCopy); + ServiceUtils.nestedTraverse(nodes, 0, f.name.split(NestingDelimiter), folderCopy, NestingDelimiter); }); return nodes; } @@ -199,37 +200,4 @@ export class FolderService implements FolderServiceAbstraction { await this.apiService.deleteFolder(id); await this.delete(id); } - - private nestedTraverse(nodeTree: Array>, partIndex: number, - parts: string[], folder: FolderView) { - if (parts.length <= partIndex) { - return; - } - - const end = partIndex === parts.length - 1; - const partName = parts[partIndex]; - - for (let i = 0; i < nodeTree.length; i++) { - if (nodeTree[i].node.name === parts[partIndex]) { - if (end && nodeTree[i].node.id !== folder.id) { - // Another node with the same name. - folder.name = partName; - nodeTree.push(new TreeNode(folder)); - return; - } - this.nestedTraverse(nodeTree[i].children, partIndex + 1, parts, folder); - return; - } - } - - if (nodeTree.filter((n) => n.node.name === partName).length === 0) { - if (end) { - folder.name = partName; - nodeTree.push(new TreeNode(folder)); - return; - } - const newPartName = parts[partIndex] + NestingDelimiter + parts[partIndex + 1]; - this.nestedTraverse(nodeTree, 0, [newPartName, ...parts.slice(partIndex + 2)], folder); - } - } } From b0eea9d7cefdc5453750f49985689fefa1fa53a5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 25 Oct 2018 12:18:36 -0400 Subject: [PATCH 0610/1626] load nested collections --- src/angular/components/groupings.component.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/angular/components/groupings.component.ts b/src/angular/components/groupings.component.ts index 4643f3490b..f5b7e7f494 100644 --- a/src/angular/components/groupings.component.ts +++ b/src/angular/components/groupings.component.ts @@ -16,8 +16,9 @@ import { FolderService } from '../../abstractions/folder.service'; export class GroupingsComponent { @Input() showFolders = true; - @Input() loadNestedFolder = false; + @Input() loadNestedFolders = false; @Input() showCollections = true; + @Input() loadNestedCollections = false; @Input() showFavorites = true; @Output() onAllClicked = new EventEmitter(); @@ -31,6 +32,7 @@ export class GroupingsComponent { folders: FolderView[]; nestedFolders: Array>; collections: CollectionView[]; + nestedCollections: Array>; loaded: boolean = false; cipherType = CipherType; selectedAll: boolean = false; @@ -61,6 +63,9 @@ export class GroupingsComponent { } else { this.collections = collections; } + if (this.loadNestedCollections) { + this.nestedCollections = await this.collectionService.getAllNested(this.collections); + } } async loadFolders() { @@ -68,7 +73,7 @@ export class GroupingsComponent { return; } this.folders = await this.folderService.getAllDecrypted(); - if (this.loadNestedFolder) { + if (this.loadNestedFolders) { this.nestedFolders = await this.folderService.getAllNested(); } } From d4b3a16fd1196abd3134c23a9fa0b6c002790458 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 26 Oct 2018 10:48:06 -0400 Subject: [PATCH 0611/1626] getNested tree object by node id --- src/abstractions/collection.service.ts | 1 + src/abstractions/folder.service.ts | 1 + src/angular/components/groupings.component.ts | 10 ++-------- src/misc/serviceUtils.ts | 18 ++++++++++++++++-- src/services/collection.service.ts | 5 +++++ src/services/folder.service.ts | 5 +++++ 6 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/abstractions/collection.service.ts b/src/abstractions/collection.service.ts index 4847a816f1..a20c313b4c 100644 --- a/src/abstractions/collection.service.ts +++ b/src/abstractions/collection.service.ts @@ -15,6 +15,7 @@ export abstract class CollectionService { getAll: () => Promise; getAllDecrypted: () => Promise; getAllNested: (collections?: CollectionView[]) => Promise>>; + getNested: (id: string) => Promise>; upsert: (collection: CollectionData | CollectionData[]) => Promise; replace: (collections: { [id: string]: CollectionData; }) => Promise; clear: (userId: string) => Promise; diff --git a/src/abstractions/folder.service.ts b/src/abstractions/folder.service.ts index fa1c6809de..9f7099988d 100644 --- a/src/abstractions/folder.service.ts +++ b/src/abstractions/folder.service.ts @@ -15,6 +15,7 @@ export abstract class FolderService { getAll: () => Promise; getAllDecrypted: () => Promise; getAllNested: () => Promise>>; + getNested: (id: string) => Promise>; saveWithServer: (folder: Folder) => Promise; upsert: (folder: FolderData | FolderData[]) => Promise; replace: (folders: { [id: string]: FolderData; }) => Promise; diff --git a/src/angular/components/groupings.component.ts b/src/angular/components/groupings.component.ts index f5b7e7f494..23381634e1 100644 --- a/src/angular/components/groupings.component.ts +++ b/src/angular/components/groupings.component.ts @@ -16,9 +16,7 @@ import { FolderService } from '../../abstractions/folder.service'; export class GroupingsComponent { @Input() showFolders = true; - @Input() loadNestedFolders = false; @Input() showCollections = true; - @Input() loadNestedCollections = false; @Input() showFavorites = true; @Output() onAllClicked = new EventEmitter(); @@ -63,9 +61,7 @@ export class GroupingsComponent { } else { this.collections = collections; } - if (this.loadNestedCollections) { - this.nestedCollections = await this.collectionService.getAllNested(this.collections); - } + this.nestedCollections = await this.collectionService.getAllNested(this.collections); } async loadFolders() { @@ -73,9 +69,7 @@ export class GroupingsComponent { return; } this.folders = await this.folderService.getAllDecrypted(); - if (this.loadNestedFolders) { - this.nestedFolders = await this.folderService.getAllNested(); - } + this.nestedFolders = await this.folderService.getAllNested(); } selectAll() { diff --git a/src/misc/serviceUtils.ts b/src/misc/serviceUtils.ts index 550a402925..d12f7b61f9 100644 --- a/src/misc/serviceUtils.ts +++ b/src/misc/serviceUtils.ts @@ -20,7 +20,7 @@ export class ServiceUtils { nodeTree.push(new TreeNode(obj, partName)); return; } - this.nestedTraverse(nodeTree[i].children, partIndex + 1, parts, obj, delimiter); + ServiceUtils.nestedTraverse(nodeTree[i].children, partIndex + 1, parts, obj, delimiter); return; } } @@ -31,7 +31,21 @@ export class ServiceUtils { return; } const newPartName = parts[partIndex] + delimiter + parts[partIndex + 1]; - this.nestedTraverse(nodeTree, 0, [newPartName, ...parts.slice(partIndex + 2)], obj, delimiter); + ServiceUtils.nestedTraverse(nodeTree, 0, [newPartName, ...parts.slice(partIndex + 2)], obj, delimiter); } } + + static getTreeNodeObject(nodeTree: Array>, id: string): TreeNode { + for (let i = 0; i < nodeTree.length; i++) { + if (nodeTree[i].node.id === id) { + return nodeTree[i]; + } else if (nodeTree[i].children != null) { + const node = ServiceUtils.getTreeNodeObject(nodeTree[i].children, id); + if (node !== null) { + return node; + } + } + } + return null; + } } diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index 42a0f48361..358e5408f1 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -111,6 +111,11 @@ export class CollectionService implements CollectionServiceAbstraction { return nodes; } + async getNested(id: string): Promise> { + const collections = await this.getAllNested(); + return ServiceUtils.getTreeNodeObject(collections, id) as TreeNode; + } + async upsert(collection: CollectionData | CollectionData[]): Promise { const userId = await this.userService.getUserId(); let collections = await this.storageService.get<{ [id: string]: CollectionData; }>( diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index 6ca6875de0..dc68a55d20 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -110,6 +110,11 @@ export class FolderService implements FolderServiceAbstraction { return nodes; } + async getNested(id: string): Promise> { + const folders = await this.getAllNested(); + return ServiceUtils.getTreeNodeObject(folders, id) as TreeNode; + } + async saveWithServer(folder: Folder): Promise { const request = new FolderRequest(folder); From aa0b274f8fd80db620abd4a85c3086f511e19c88 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 26 Oct 2018 16:08:48 -0400 Subject: [PATCH 0612/1626] store parent node --- src/misc/serviceUtils.ts | 12 +++++++----- src/models/domain/treeNode.ts | 4 +++- src/services/collection.service.ts | 3 ++- src/services/folder.service.ts | 3 ++- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/misc/serviceUtils.ts b/src/misc/serviceUtils.ts index d12f7b61f9..a466c83fbd 100644 --- a/src/misc/serviceUtils.ts +++ b/src/misc/serviceUtils.ts @@ -5,7 +5,7 @@ import { export class ServiceUtils { static nestedTraverse(nodeTree: Array>, partIndex: number, parts: string[], - obj: ITreeNodeObject, delimiter: string) { + obj: ITreeNodeObject, parent: ITreeNodeObject, delimiter: string) { if (parts.length <= partIndex) { return; } @@ -17,21 +17,23 @@ export class ServiceUtils { if (nodeTree[i].node.name === parts[partIndex]) { if (end && nodeTree[i].node.id !== obj.id) { // Another node with the same name. - nodeTree.push(new TreeNode(obj, partName)); + nodeTree.push(new TreeNode(obj, partName, parent)); return; } - ServiceUtils.nestedTraverse(nodeTree[i].children, partIndex + 1, parts, obj, delimiter); + ServiceUtils.nestedTraverse(nodeTree[i].children, partIndex + 1, parts, + obj, nodeTree[i].node, delimiter); return; } } if (nodeTree.filter((n) => n.node.name === partName).length === 0) { if (end) { - nodeTree.push(new TreeNode(obj, partName)); + nodeTree.push(new TreeNode(obj, partName, parent)); return; } const newPartName = parts[partIndex] + delimiter + parts[partIndex + 1]; - ServiceUtils.nestedTraverse(nodeTree, 0, [newPartName, ...parts.slice(partIndex + 2)], obj, delimiter); + ServiceUtils.nestedTraverse(nodeTree, 0, [newPartName, ...parts.slice(partIndex + 2)], + obj, parent, delimiter); } } diff --git a/src/models/domain/treeNode.ts b/src/models/domain/treeNode.ts index ee020c2980..beda19f158 100644 --- a/src/models/domain/treeNode.ts +++ b/src/models/domain/treeNode.ts @@ -1,8 +1,10 @@ export class TreeNode { + parent: T; node: T; children: Array> = []; - constructor(node: T, name: string) { + constructor(node: T, name: string, parent: T) { + this.parent = parent; this.node = node; this.node.name = name; } diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index 358e5408f1..c33818a3d9 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -106,7 +106,8 @@ export class CollectionService implements CollectionServiceAbstraction { collections.forEach((f) => { const collectionCopy = new CollectionView(); collectionCopy.id = f.id; - ServiceUtils.nestedTraverse(nodes, 0, f.name.split(NestingDelimiter), collectionCopy, NestingDelimiter); + ServiceUtils.nestedTraverse(nodes, 0, f.name.split(NestingDelimiter), collectionCopy, + null, NestingDelimiter); }); return nodes; } diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index dc68a55d20..70fc7f1e71 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -105,7 +105,8 @@ export class FolderService implements FolderServiceAbstraction { const folderCopy = new FolderView(); folderCopy.id = f.id; folderCopy.revisionDate = f.revisionDate; - ServiceUtils.nestedTraverse(nodes, 0, f.name.split(NestingDelimiter), folderCopy, NestingDelimiter); + ServiceUtils.nestedTraverse(nodes, 0, f.name.split(NestingDelimiter), folderCopy, + null, NestingDelimiter); }); return nodes; } From a98a8bda9bea5b7bb4459cea9413e0f03b2aa068 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 29 Oct 2018 22:41:19 -0400 Subject: [PATCH 0613/1626] init function --- src/angular/components/add-edit.component.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 3a2f42ed07..607c95b691 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -132,6 +132,10 @@ export class AddEditComponent implements OnInit { } async ngOnInit() { + await this.init(); + } + + async init() { const myEmail = await this.userService.getEmail(); this.ownershipOptions.push({ name: myEmail, value: null }); const orgs = await this.userService.getAllOrganizations(); From 2c4005d4c7fcb2b8e61e1b8c38b8cfc23b2f7a2c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 30 Oct 2018 11:03:41 -0400 Subject: [PATCH 0614/1626] index organizationid for search --- src/services/search.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index ff9fa07789..5c5c5aa51f 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -62,6 +62,7 @@ export class SearchService implements SearchServiceAbstraction { (builder as any).field('attachments', { extractor: (c: CipherView) => this.attachmentExtractor(c, false) }); (builder as any).field('attachments_joined', { extractor: (c: CipherView) => this.attachmentExtractor(c, true) }); + (builder as any).field('organizationid', { extractor: (c: CipherView) => c.organizationId }); const ciphers = await this.cipherService.getAllDecrypted(); ciphers.forEach((c) => builder.add(c)); this.index = builder.build(); From 3fd65dd82fda78855108776787bb8b35c60b053e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 1 Nov 2018 09:26:03 -0400 Subject: [PATCH 0615/1626] convert 1password to note if needed --- src/importers/onepassword1PifImporter.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/importers/onepassword1PifImporter.ts b/src/importers/onepassword1PifImporter.ts index 2978a8c8ce..798c2cb640 100644 --- a/src/importers/onepassword1PifImporter.ts +++ b/src/importers/onepassword1PifImporter.ts @@ -50,6 +50,7 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { } } + this.convertToNoteIfNeeded(cipher); this.cleanupCipher(cipher); this.result.ciphers.push(cipher); }); From ff981532fd8439b74dcaf5da57693937df9990c4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 2 Nov 2018 08:39:09 -0400 Subject: [PATCH 0616/1626] whitelist of protocols for can launch --- src/models/view/loginUriView.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/models/view/loginUriView.ts b/src/models/view/loginUriView.ts index c402c6fb3b..ae9f509053 100644 --- a/src/models/view/loginUriView.ts +++ b/src/models/view/loginUriView.ts @@ -6,6 +6,16 @@ import { LoginUri } from '../domain/loginUri'; import { Utils } from '../../misc/utils'; +const CanLaunchWhitelist = [ + 'https://', + 'http://', + 'ssh://', + 'ftp://', + 'sftp://', + 'irc://', + 'chrome://', +]; + export class LoginUriView implements View { match: UriMatchType = null; @@ -62,6 +72,14 @@ export class LoginUriView implements View { } get canLaunch(): boolean { - return this.uri != null && this.uri.indexOf('://') > -1; + if (this.uri == null) { + return false; + } + for (let i = 0; i < CanLaunchWhitelist.length; i++) { + if (this.uri.indexOf(CanLaunchWhitelist[i]) === 0) { + return true; + } + } + return false; } } From 4a13e62477b6d271616644001dedacfe8d07f0bb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 2 Nov 2018 13:03:00 -0400 Subject: [PATCH 0617/1626] cache canLaunch result --- src/models/view/loginUriView.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/models/view/loginUriView.ts b/src/models/view/loginUriView.ts index ae9f509053..2ac71bd8f2 100644 --- a/src/models/view/loginUriView.ts +++ b/src/models/view/loginUriView.ts @@ -23,6 +23,7 @@ export class LoginUriView implements View { private _uri: string; private _domain: string; private _hostname: string; + private _canLaunch: boolean; // tslint:enable constructor(u?: LoginUri) { @@ -39,6 +40,7 @@ export class LoginUriView implements View { set uri(value: string) { this._uri = value; this._domain = null; + this._canLaunch = null; } get domain(): string { @@ -72,14 +74,18 @@ export class LoginUriView implements View { } get canLaunch(): boolean { - if (this.uri == null) { - return false; + if (this._canLaunch != null) { + return this._canLaunch; } - for (let i = 0; i < CanLaunchWhitelist.length; i++) { - if (this.uri.indexOf(CanLaunchWhitelist[i]) === 0) { - return true; + if (this.uri != null) { + for (let i = 0; i < CanLaunchWhitelist.length; i++) { + if (this.uri.indexOf(CanLaunchWhitelist[i]) === 0) { + this._canLaunch = true; + return this._canLaunch; + } } } - return false; + this._canLaunch = false; + return this._canLaunch; } } From 6fa3538cbc6a4de3e948dcc8d39491ba650eb230 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 5 Nov 2018 14:48:07 -0500 Subject: [PATCH 0618/1626] copy org id too --- src/services/collection.service.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index c33818a3d9..ef7d602c8e 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -103,10 +103,11 @@ export class CollectionService implements CollectionServiceAbstraction { collections = await this.getAllDecrypted(); } const nodes: Array> = []; - collections.forEach((f) => { + collections.forEach((c) => { const collectionCopy = new CollectionView(); - collectionCopy.id = f.id; - ServiceUtils.nestedTraverse(nodes, 0, f.name.split(NestingDelimiter), collectionCopy, + collectionCopy.id = c.id; + collectionCopy.organizationId = c.organizationId; + ServiceUtils.nestedTraverse(nodes, 0, c.name.split(NestingDelimiter), collectionCopy, null, NestingDelimiter); }); return nodes; From 52e087e4dda5213885f062952572306aeb99caf4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 6 Nov 2018 12:13:11 -0500 Subject: [PATCH 0619/1626] icon code --- src/angular/components/icon.component.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/angular/components/icon.component.ts b/src/angular/components/icon.component.ts index 4b127c07db..0e2dabfc25 100644 --- a/src/angular/components/icon.component.ts +++ b/src/angular/components/icon.component.ts @@ -6,6 +6,8 @@ import { import { CipherType } from '../../enums/cipherType'; +import { CipherView } from '../../models/view/cipherView'; + import { EnvironmentService } from '../../abstractions/environment.service'; import { StateService } from '../../abstractions/state.service'; @@ -13,12 +15,21 @@ import { ConstantsService } from '../../services/constants.service'; import { Utils } from '../../misc/utils'; +const IconMap = { + 'fa-globe': String.fromCharCode(0xf0ac), + 'fa-sticky-note-o': String.fromCharCode(0xf24a), + 'fa-id-card-o': String.fromCharCode(0xf2c3), + 'fa-credit-card': String.fromCharCode(0xf09d), + 'fa-android': String.fromCharCode(0xf17b), + 'fa-apple': String.fromCharCode(0xf179), +}; + @Component({ selector: 'app-vault-icon', templateUrl: 'icon.component.html', }) export class IconComponent implements OnChanges { - @Input() cipher: any; + @Input() cipher: CipherView; icon: string; image: string; fallbackImage: string; @@ -59,6 +70,10 @@ export class IconComponent implements OnChanges { } } + get iconCode(): string { + return IconMap[this.icon]; + } + private setLoginIcon() { if (this.cipher.login.uri) { let hostnameUri = this.cipher.login.uri; From 7dcb9b5f8bfd7c65f759b0cc34dfc6abd33fdcd6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 6 Nov 2018 15:53:45 -0500 Subject: [PATCH 0620/1626] IconMap is type any --- src/angular/components/icon.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/icon.component.ts b/src/angular/components/icon.component.ts index 0e2dabfc25..be525b926d 100644 --- a/src/angular/components/icon.component.ts +++ b/src/angular/components/icon.component.ts @@ -15,7 +15,7 @@ import { ConstantsService } from '../../services/constants.service'; import { Utils } from '../../misc/utils'; -const IconMap = { +const IconMap: any = { 'fa-globe': String.fromCharCode(0xf0ac), 'fa-sticky-note-o': String.fromCharCode(0xf24a), 'fa-id-card-o': String.fromCharCode(0xf2c3), From 40b69b7c3596e8e4df689d15674926beb57014f6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 7 Nov 2018 12:07:55 -0500 Subject: [PATCH 0621/1626] update electron libs --- package-lock.json | 116 ++++++++++++++++++++++++++++++---------------- package.json | 4 +- 2 files changed, 79 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index c28a756d59..195a41a8e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -776,7 +776,7 @@ }, "bl": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "requires": { "readable-stream": "^2.3.5", @@ -792,14 +792,22 @@ "bluebird": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", + "dev": true }, "bluebird-lst": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.5.tgz", - "integrity": "sha512-Ey0bDNys5qpYPhZ/oQ9vOEvD0TYQDTILMXWP2iGfvMg7rSDde+oV4aQQgqRH+CvBFNz2BSDQnPGMUl6LKBUUQA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.6.tgz", + "integrity": "sha512-CBWFoPuUPpcvMUxfyr8DKdI5d4kjxFl1h39+VbKxP3KJWJHEsLtuT4pPLkjpxCGU6Ask21tvbnftWXdqIxYldQ==", "requires": { - "bluebird": "^3.5.1" + "bluebird": "^3.5.2" + }, + "dependencies": { + "bluebird": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", + "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==" + } } }, "bn.js": { @@ -1100,14 +1108,29 @@ "dev": true }, "builder-util-runtime": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-4.4.1.tgz", - "integrity": "sha512-8L2pbL6D3VdI1f8OMknlZJpw0c7KK15BRz3cY77AOUElc4XlCv2UhVV01jJM7+6Lx7henaQh80ALULp64eFYAQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-5.0.0.tgz", + "integrity": "sha512-mTyLqmzdPzavKQNAfxcGu6kqaDiPCtFKJG+nNO9SYfL6lY7VgTUW+45iXhowc5ElmPj0eSTDaIGlScxVMwFUEA==", "requires": { "bluebird-lst": "^1.0.5", - "debug": "^3.1.0", + "debug": "^4.1.0", "fs-extra-p": "^4.6.1", "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", + "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } } }, "builtin-modules": { @@ -1262,9 +1285,9 @@ } }, "chownr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", - "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" }, "ci-info": { "version": "1.1.3", @@ -1734,6 +1757,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, "requires": { "ms": "2.0.0" } @@ -2053,19 +2077,26 @@ "integrity": "sha512-Rj+XyK4nShe/nv9v1Uks4KEfjtQ6N+eSnx5CLpAjG6rlyUdAflyFHoybcHSLoq9l9pGavclULWS5IXgk8umc2g==" }, "electron-updater": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-3.0.3.tgz", - "integrity": "sha512-7gJLZp34Db+lXiJsFzW8DunGnvxJgZclBZa1DNLbXOet3lRXkVKbFJ73mClbv+UTW6hW/EJ6MmSsofRiK1s6Dw==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-3.1.6.tgz", + "integrity": "sha512-ZzNrFqvJJmG2O0qEVhAhI25WqJx6Nq3lNlXvBvX3i0zqWtAuSmcAdCbq4VyDymun/glsZ4bD3ZzATcIk3sYygg==", "requires": { "bluebird-lst": "^1.0.5", - "builder-util-runtime": "~4.4.1", + "builder-util-runtime": "~5.0.0", "electron-is-dev": "^0.3.0", "fs-extra-p": "^4.6.1", "js-yaml": "^3.12.0", "lazy-val": "^1.0.3", "lodash.isequal": "^4.5.0", - "semver": "^5.5.0", - "source-map-support": "^0.5.6" + "semver": "^5.6.0", + "source-map-support": "^0.5.9" + }, + "dependencies": { + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" + } } }, "elliptic": { @@ -4401,12 +4432,12 @@ } }, "keytar": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-4.2.1.tgz", - "integrity": "sha1-igamV3/fY3PgqmsRInfmPex3/RI=", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-4.3.0.tgz", + "integrity": "sha512-pd++/v+fS0LQKmzWlW6R1lziTXFqhfGeS6sYLfuTIqEy2pDzAbjutbSW8f9tnJdEEMn/9XhAQlT34VAtl9h4MQ==", "requires": { "nan": "2.8.0", - "prebuild-install": "^2.4.1" + "prebuild-install": "^5.0.0" } }, "kind-of": { @@ -4815,7 +4846,8 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true }, "msgpack5": { "version": "4.2.1", @@ -4902,6 +4934,11 @@ } } }, + "napi-build-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.1.tgz", + "integrity": "sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA==" + }, "negotiator": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", @@ -4909,9 +4946,9 @@ "dev": true }, "node-abi": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.4.3.tgz", - "integrity": "sha512-b656V5C0628gOOA2kwcpNA/bxdlqYF9FvxJ+qqVX0ctdXNVZpS8J6xEUYir3WAKc7U0BH/NRlSpNbGsy+azjeg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.5.0.tgz", + "integrity": "sha512-9g2twBGSP6wIR5PW7tXvAWnEWKJDH/VskdXp168xsw9VVxpEGov8K4jsP4/VeoC7b2ZAyzckvMCuQuQlw44lXg==", "requires": { "semver": "^5.4.1" } @@ -5823,21 +5860,22 @@ "dev": true }, "prebuild-install": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-2.5.3.tgz", - "integrity": "sha512-/rI36cN2g7vDQnKWN8Uzupi++KjyqS9iS+/fpwG4Ea8d0Pip0PQ5bshUNzVwt+/D2MRfhVAplYMMvWLqWrCF/g==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.2.1.tgz", + "integrity": "sha512-9DAccsInWHB48TBQi2eJkLPE049JuAI6FjIH0oIrij4bpDVEbX6JvlWRAcAAlUqBHhjgq0jNqA3m3bBXWm9v6w==", "requires": { "detect-libc": "^1.0.3", "expand-template": "^1.0.2", "github-from-package": "0.0.0", "minimist": "^1.2.0", "mkdirp": "^0.5.1", + "napi-build-utils": "^1.0.1", "node-abi": "^2.2.0", "noop-logger": "^0.1.1", "npmlog": "^4.0.1", "os-homedir": "^1.0.1", "pump": "^2.0.1", - "rc": "^1.1.6", + "rc": "^1.2.7", "simple-get": "^2.7.0", "tar-fs": "^1.13.0", "tunnel-agent": "^0.6.0", @@ -6654,9 +6692,9 @@ } }, "source-map-support": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz", - "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==", + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz", + "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==", "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -6998,16 +7036,16 @@ } }, "tar-stream": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.1.tgz", - "integrity": "sha512-IFLM5wp3QrJODQFPm6/to3LJZrONdBY/otxcvDIQzu217zKye6yVR3hhi9lAjrC2Z+m/j5oDxMPb1qcd8cIvpA==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", "requires": { "bl": "^1.0.0", - "buffer-alloc": "^1.1.0", + "buffer-alloc": "^1.2.0", "end-of-stream": "^1.0.0", "fs-constants": "^1.0.0", "readable-stream": "^2.3.0", - "to-buffer": "^1.1.0", + "to-buffer": "^1.1.1", "xtend": "^4.0.0" } }, diff --git a/package.json b/package.json index 305763fe70..7e2cc7b440 100644 --- a/package.json +++ b/package.json @@ -72,9 +72,9 @@ "@aspnet/signalr-protocol-msgpack": "1.0.4", "core-js": "2.5.7", "electron-log": "2.2.14", - "electron-updater": "3.0.3", + "electron-updater": "3.1.6", "form-data": "2.3.2", - "keytar": "4.2.1", + "keytar": "4.3.0", "lowdb": "1.0.0", "lunr": "2.3.3", "node-fetch": "2.2.0", From 8ed7c5c5376ebde9e8e17e0a4403b49256d58ef4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 7 Nov 2018 12:09:26 -0500 Subject: [PATCH 0622/1626] update lock file --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 195a41a8e3..58c0664195 100644 --- a/package-lock.json +++ b/package-lock.json @@ -776,7 +776,7 @@ }, "bl": { "version": "1.2.2", - "resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "requires": { "readable-stream": "^2.3.5", From f355840f3cf4af0279c9a078e0c14729a701f37a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 7 Nov 2018 12:28:21 -0500 Subject: [PATCH 0623/1626] downgrade libs --- package-lock.json | 70 ++++++++++++++--------------------------------- package.json | 4 +-- 2 files changed, 22 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index 58c0664195..f10106e59d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -776,7 +776,7 @@ }, "bl": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "requires": { "readable-stream": "^2.3.5", @@ -1108,29 +1108,14 @@ "dev": true }, "builder-util-runtime": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-5.0.0.tgz", - "integrity": "sha512-mTyLqmzdPzavKQNAfxcGu6kqaDiPCtFKJG+nNO9SYfL6lY7VgTUW+45iXhowc5ElmPj0eSTDaIGlScxVMwFUEA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-4.4.1.tgz", + "integrity": "sha512-8L2pbL6D3VdI1f8OMknlZJpw0c7KK15BRz3cY77AOUElc4XlCv2UhVV01jJM7+6Lx7henaQh80ALULp64eFYAQ==", "requires": { "bluebird-lst": "^1.0.5", - "debug": "^4.1.0", + "debug": "^3.1.0", "fs-extra-p": "^4.6.1", "sax": "^1.2.4" - }, - "dependencies": { - "debug": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", - "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - } } }, "builtin-modules": { @@ -1757,7 +1742,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, "requires": { "ms": "2.0.0" } @@ -2077,26 +2061,19 @@ "integrity": "sha512-Rj+XyK4nShe/nv9v1Uks4KEfjtQ6N+eSnx5CLpAjG6rlyUdAflyFHoybcHSLoq9l9pGavclULWS5IXgk8umc2g==" }, "electron-updater": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-3.1.6.tgz", - "integrity": "sha512-ZzNrFqvJJmG2O0qEVhAhI25WqJx6Nq3lNlXvBvX3i0zqWtAuSmcAdCbq4VyDymun/glsZ4bD3ZzATcIk3sYygg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-3.0.3.tgz", + "integrity": "sha512-7gJLZp34Db+lXiJsFzW8DunGnvxJgZclBZa1DNLbXOet3lRXkVKbFJ73mClbv+UTW6hW/EJ6MmSsofRiK1s6Dw==", "requires": { "bluebird-lst": "^1.0.5", - "builder-util-runtime": "~5.0.0", + "builder-util-runtime": "~4.4.1", "electron-is-dev": "^0.3.0", "fs-extra-p": "^4.6.1", "js-yaml": "^3.12.0", "lazy-val": "^1.0.3", "lodash.isequal": "^4.5.0", - "semver": "^5.6.0", - "source-map-support": "^0.5.9" - }, - "dependencies": { - "semver": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" - } + "semver": "^5.5.0", + "source-map-support": "^0.5.6" } }, "elliptic": { @@ -4432,12 +4409,12 @@ } }, "keytar": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-4.3.0.tgz", - "integrity": "sha512-pd++/v+fS0LQKmzWlW6R1lziTXFqhfGeS6sYLfuTIqEy2pDzAbjutbSW8f9tnJdEEMn/9XhAQlT34VAtl9h4MQ==", + "version": "4.2.1", + "resolved": "http://registry.npmjs.org/keytar/-/keytar-4.2.1.tgz", + "integrity": "sha1-igamV3/fY3PgqmsRInfmPex3/RI=", "requires": { "nan": "2.8.0", - "prebuild-install": "^5.0.0" + "prebuild-install": "^2.4.1" } }, "kind-of": { @@ -4846,8 +4823,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "msgpack5": { "version": "4.2.1", @@ -4934,11 +4910,6 @@ } } }, - "napi-build-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.1.tgz", - "integrity": "sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA==" - }, "negotiator": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", @@ -5860,22 +5831,21 @@ "dev": true }, "prebuild-install": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.2.1.tgz", - "integrity": "sha512-9DAccsInWHB48TBQi2eJkLPE049JuAI6FjIH0oIrij4bpDVEbX6JvlWRAcAAlUqBHhjgq0jNqA3m3bBXWm9v6w==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-2.5.3.tgz", + "integrity": "sha512-/rI36cN2g7vDQnKWN8Uzupi++KjyqS9iS+/fpwG4Ea8d0Pip0PQ5bshUNzVwt+/D2MRfhVAplYMMvWLqWrCF/g==", "requires": { "detect-libc": "^1.0.3", "expand-template": "^1.0.2", "github-from-package": "0.0.0", "minimist": "^1.2.0", "mkdirp": "^0.5.1", - "napi-build-utils": "^1.0.1", "node-abi": "^2.2.0", "noop-logger": "^0.1.1", "npmlog": "^4.0.1", "os-homedir": "^1.0.1", "pump": "^2.0.1", - "rc": "^1.2.7", + "rc": "^1.1.6", "simple-get": "^2.7.0", "tar-fs": "^1.13.0", "tunnel-agent": "^0.6.0", diff --git a/package.json b/package.json index 7e2cc7b440..305763fe70 100644 --- a/package.json +++ b/package.json @@ -72,9 +72,9 @@ "@aspnet/signalr-protocol-msgpack": "1.0.4", "core-js": "2.5.7", "electron-log": "2.2.14", - "electron-updater": "3.1.6", + "electron-updater": "3.0.3", "form-data": "2.3.2", - "keytar": "4.3.0", + "keytar": "4.2.1", "lowdb": "1.0.0", "lunr": "2.3.3", "node-fetch": "2.2.0", From ed07c8d01fb293ab65fe1d33af5cd1369decbb44 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 7 Nov 2018 22:54:56 -0500 Subject: [PATCH 0624/1626] get fingerprint crypto method --- package-lock.json | 7 +++++- package.json | 1 + src/abstractions/crypto.service.ts | 1 + src/services/crypto.service.ts | 35 ++++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index f10106e59d..857b8d673b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -768,6 +768,11 @@ "callsite": "1.0.0" } }, + "big-integer": { + "version": "1.6.36", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.36.tgz", + "integrity": "sha512-t70bfa7HYEA1D9idDbmuv7YbsbVkQ+Hp+8KFSul4aE5e/i1bjCNIRYJZlA8Q8p0r9T8cF/RVvwUgRA//FydEyg==" + }, "binary-extensions": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", @@ -776,7 +781,7 @@ }, "bl": { "version": "1.2.2", - "resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "requires": { "readable-stream": "^2.3.5", diff --git a/package.json b/package.json index 305763fe70..f2f5666ed6 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@angular/upgrade": "6.1.7", "@aspnet/signalr": "1.0.4", "@aspnet/signalr-protocol-msgpack": "1.0.4", + "big-integer": "1.6.36", "core-js": "2.5.7", "electron-log": "2.2.14", "electron-updater": "3.0.3", diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 3dc13669fa..59ada585e1 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -16,6 +16,7 @@ export abstract class CryptoService { getEncKey: () => Promise; getPublicKey: () => Promise; getPrivateKey: () => Promise; + getFingerprint: () => Promise; getOrgKeys: () => Promise>; getOrgKey: (orgId: string) => Promise; hasKey: () => Promise; diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index b6a9097884..101b3bc271 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -1,3 +1,5 @@ +import * as bigInt from 'big-integer'; + import { EncryptionType } from '../enums/encryptionType'; import { KdfType } from '../enums/kdfType'; @@ -14,6 +16,7 @@ import { ConstantsService } from './constants.service'; import { sequentialize } from '../misc/sequentialize'; import { Utils } from '../misc/utils'; +import { EEFLongWordList } from '../misc/wordlist'; const Keys = { key: 'key', @@ -163,6 +166,16 @@ export class CryptoService implements CryptoServiceAbstraction { return this.privateKey; } + async getFingerprint(): Promise { + const publicKey = await this.getPublicKey(); + if (publicKey === null) { + throw new Error('No public key available.'); + } + const keyFingerprint = await this.cryptoFunctionService.hash(publicKey, 'sha256'); + const userFingerprint = await this.hkdfExpand(keyFingerprint, Utils.fromUtf8ToArray('USER-ID'), 32); + return this.hashPhrase(userFingerprint.buffer); + } + @sequentialize(() => 'getOrgKeys') async getOrgKeys(): Promise> { if (this.orgKeys != null && this.orgKeys.size > 0) { @@ -675,6 +688,28 @@ export class CryptoService implements CryptoServiceAbstraction { return okm; } + private async hashPhrase(data: ArrayBuffer, minimumEntropy: number = 64) { + const wordListLength = EEFLongWordList.length; + const entropyPerWord = Math.log(wordListLength) / Math.log(2); + let numWords = Math.ceil(minimumEntropy / entropyPerWord); + + const hashBuffer = await this.cryptoFunctionService.pbkdf2(data, '', 'sha256', 50000); + const hash = Array.from(new Uint8Array(hashBuffer)); + const entropyAvailable = hash.length * 4; + if (numWords * entropyPerWord > entropyAvailable) { + throw new Error('Output entropy of hash function is too small'); + } + + const phrase: string[] = []; + let hashNumber = bigInt.fromArray(hash, 256); + while (numWords--) { + const remainder = hashNumber.mod(wordListLength); + hashNumber = hashNumber.divide(wordListLength); + phrase.push(EEFLongWordList[remainder as any]); + } + return phrase; + } + private async buildEncKey(key: SymmetricCryptoKey, encKey: ArrayBuffer = null) : Promise<[SymmetricCryptoKey, CipherString]> { let encKeyEnc: CipherString = null; From 1e6b3b4aae84495d757d02583d0d69a8d18a029b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 7 Nov 2018 23:12:45 -0500 Subject: [PATCH 0625/1626] allow userId and pub key to be passed in --- src/abstractions/crypto.service.ts | 2 +- src/services/crypto.service.ts | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 59ada585e1..547c142d84 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -16,7 +16,7 @@ export abstract class CryptoService { getEncKey: () => Promise; getPublicKey: () => Promise; getPrivateKey: () => Promise; - getFingerprint: () => Promise; + getFingerprint: (userId: string, publicKey?: ArrayBuffer) => Promise; getOrgKeys: () => Promise>; getOrgKey: (orgId: string) => Promise; hasKey: () => Promise; diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 101b3bc271..6f5b201ab2 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -166,13 +166,15 @@ export class CryptoService implements CryptoServiceAbstraction { return this.privateKey; } - async getFingerprint(): Promise { - const publicKey = await this.getPublicKey(); + async getFingerprint(userId: string, publicKey?: ArrayBuffer): Promise { + if (publicKey == null) { + publicKey = await this.getPublicKey(); + } if (publicKey === null) { throw new Error('No public key available.'); } const keyFingerprint = await this.cryptoFunctionService.hash(publicKey, 'sha256'); - const userFingerprint = await this.hkdfExpand(keyFingerprint, Utils.fromUtf8ToArray('USER-ID'), 32); + const userFingerprint = await this.hkdfExpand(keyFingerprint, Utils.fromUtf8ToArray(userId), 32); return this.hashPhrase(userFingerprint.buffer); } @@ -688,12 +690,12 @@ export class CryptoService implements CryptoServiceAbstraction { return okm; } - private async hashPhrase(data: ArrayBuffer, minimumEntropy: number = 64) { - const wordListLength = EEFLongWordList.length; - const entropyPerWord = Math.log(wordListLength) / Math.log(2); + private async hashPhrase(data: ArrayBuffer, minimumEntropy: number = 64, hashIterations: number = 50000) { + const entropyPerWord = Math.log(EEFLongWordList.length) / Math.log(2); let numWords = Math.ceil(minimumEntropy / entropyPerWord); - const hashBuffer = await this.cryptoFunctionService.pbkdf2(data, '', 'sha256', 50000); + const hashBuffer = await this.cryptoFunctionService.pbkdf2(data, new Uint8Array([]).buffer, + 'sha256', hashIterations); const hash = Array.from(new Uint8Array(hashBuffer)); const entropyAvailable = hash.length * 4; if (numWords * entropyPerWord > entropyAvailable) { @@ -703,8 +705,8 @@ export class CryptoService implements CryptoServiceAbstraction { const phrase: string[] = []; let hashNumber = bigInt.fromArray(hash, 256); while (numWords--) { - const remainder = hashNumber.mod(wordListLength); - hashNumber = hashNumber.divide(wordListLength); + const remainder = hashNumber.mod(EEFLongWordList.length); + hashNumber = hashNumber.divide(EEFLongWordList.length); phrase.push(EEFLongWordList[remainder as any]); } return phrase; From b4fad203b94da53d33693f4283d7249e3a8f1afe Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 9 Nov 2018 08:20:54 -0500 Subject: [PATCH 0626/1626] pbkdf2 not needed for hash phrase --- src/services/crypto.service.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 6f5b201ab2..01b4ba4b62 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -690,20 +690,18 @@ export class CryptoService implements CryptoServiceAbstraction { return okm; } - private async hashPhrase(data: ArrayBuffer, minimumEntropy: number = 64, hashIterations: number = 50000) { + private async hashPhrase(hash: ArrayBuffer, minimumEntropy: number = 64) { const entropyPerWord = Math.log(EEFLongWordList.length) / Math.log(2); let numWords = Math.ceil(minimumEntropy / entropyPerWord); - const hashBuffer = await this.cryptoFunctionService.pbkdf2(data, new Uint8Array([]).buffer, - 'sha256', hashIterations); - const hash = Array.from(new Uint8Array(hashBuffer)); - const entropyAvailable = hash.length * 4; + const hashArr = Array.from(new Uint8Array(hash)); + const entropyAvailable = hashArr.length * 4; if (numWords * entropyPerWord > entropyAvailable) { throw new Error('Output entropy of hash function is too small'); } const phrase: string[] = []; - let hashNumber = bigInt.fromArray(hash, 256); + let hashNumber = bigInt.fromArray(hashArr, 256); while (numWords--) { const remainder = hashNumber.mod(EEFLongWordList.length); hashNumber = hashNumber.divide(EEFLongWordList.length); From 786fa02b90d64044aee23011a18f6e202856a362 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 9 Nov 2018 17:44:45 -0500 Subject: [PATCH 0627/1626] added collpase/expand functions to groupings --- src/angular/components/groupings.component.ts | 36 ++++++++++++++++++- src/services/constants.service.ts | 2 ++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/angular/components/groupings.component.ts b/src/angular/components/groupings.component.ts index 23381634e1..9983823dfa 100644 --- a/src/angular/components/groupings.component.ts +++ b/src/angular/components/groupings.component.ts @@ -13,6 +13,10 @@ import { TreeNode } from '../../models/domain/treeNode'; import { CollectionService } from '../../abstractions/collection.service'; import { FolderService } from '../../abstractions/folder.service'; +import { StorageService } from '../../abstractions/storage.service'; +import { UserService } from '../../abstractions/user.service'; + +import { ConstantsService } from '../../services/constants.service'; export class GroupingsComponent { @Input() showFolders = true; @@ -40,9 +44,22 @@ export class GroupingsComponent { selectedFolderId: string = null; selectedCollectionId: string = null; - constructor(protected collectionService: CollectionService, protected folderService: FolderService) { } + private collapsedGroupings: Set; + private collapsedGroupingsKey: string; + + constructor(protected collectionService: CollectionService, protected folderService: FolderService, + protected storageService: StorageService, protected userService: UserService) { } async load(setLoaded = true) { + const userId = await this.userService.getUserId(); + this.collapsedGroupingsKey = ConstantsService.collapsedGroupingsKey + '_' + userId; + const collapsedGroupings = await this.storageService.get(this.collapsedGroupingsKey); + if (collapsedGroupings == null) { + this.collapsedGroupings = new Set(); + } else { + this.collapsedGroupings = new Set(collapsedGroupings); + } + await this.loadFolders(); await this.loadCollections(); @@ -119,4 +136,21 @@ export class GroupingsComponent { this.selectedFolderId = null; this.selectedCollectionId = null; } + + collapse(grouping: FolderView | CollectionView, idPrefix = '') { + if (grouping.id == null) { + return; + } + const id = idPrefix + grouping.id; + if (this.isCollapsed(grouping, idPrefix)) { + this.collapsedGroupings.delete(id); + } else { + this.collapsedGroupings.add(id); + } + this.storageService.save(this.collapsedGroupingsKey, this.collapsedGroupings); + } + + isCollapsed(grouping: FolderView | CollectionView, idPrefix = '') { + return this.collapsedGroupings.has(idPrefix + grouping.id); + } } diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index 987f53ce48..cd0a260359 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -13,6 +13,7 @@ export class ConstantsService { static readonly installedVersionKey: string = 'installedVersion'; static readonly localeKey: string = 'locale'; static readonly themeKey: string = 'theme'; + static readonly collapsedGroupingsKey: string = 'collapsedGroupings'; readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; readonly disableGaKey: string = ConstantsService.disableGaKey; @@ -27,4 +28,5 @@ export class ConstantsService { readonly installedVersionKey: string = ConstantsService.installedVersionKey; readonly localeKey: string = ConstantsService.localeKey; readonly themeKey: string = ConstantsService.themeKey; + readonly collapsedGroupingsKey: string = ConstantsService.collapsedGroupingsKey; } From aa16fb2a9e4b6fb33ed80dea2a4c7bfa2234d45c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 12 Nov 2018 22:54:18 -0500 Subject: [PATCH 0628/1626] password strength function with zxcvbn --- package-lock.json | 11 ++++++++ package.json | 4 ++- .../passwordGeneration.service.ts | 1 + src/angular/components/register.component.ts | 26 ++++++++++++++++++- src/services/passwordGeneration.service.ts | 16 ++++++++++++ 5 files changed, 56 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 857b8d673b..2daa4b22d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -178,6 +178,12 @@ "integrity": "sha512-jzAoSUvqA+183nJO/Sc73CREQJsv+p77WJdn532GqA3YXQzlwRwHhClVa7U4O8iB2sJSR7G3v6f1mJFNkwA9YQ==", "dev": true }, + "@types/zxcvbn": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@types/zxcvbn/-/zxcvbn-4.4.0.tgz", + "integrity": "sha512-GQLOT+SN20a+AI51y3fAimhyTF4Y0RG+YP3gf91OibIZ7CJmPFgoZi+ZR5a+vRbS01LbQosITWum4ATmJ1Z6Pg==", + "dev": true + }, "abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", @@ -7920,6 +7926,11 @@ "version": "0.8.26", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.26.tgz", "integrity": "sha512-W9Nj+UmBJG251wkCacIkETgra4QgBo/vgoEkb4a2uoLzpQG7qF9nzwoLXWU5xj3Fg2mxGvEDh47mg24vXccYjA==" + }, + "zxcvbn": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz", + "integrity": "sha1-KOwXzwl0PtyrBW3dixsGJizHPDA=" } } } diff --git a/package.json b/package.json index f2f5666ed6..d1a822950c 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@types/papaparse": "^4.5.3", "@types/tldjs": "^2.3.0", "@types/webcrypto": "0.0.28", + "@types/zxcvbn": "^4.4.0", "concurrently": "3.5.1", "electron": "2.0.11", "jasmine": "^3.2.0", @@ -83,6 +84,7 @@ "papaparse": "4.6.0", "rxjs": "6.3.2", "tldjs": "2.3.1", - "zone.js": "0.8.26" + "zone.js": "0.8.26", + "zxcvbn": "4.4.2" } } diff --git a/src/abstractions/passwordGeneration.service.ts b/src/abstractions/passwordGeneration.service.ts index c2d904cc96..a94283d73a 100644 --- a/src/abstractions/passwordGeneration.service.ts +++ b/src/abstractions/passwordGeneration.service.ts @@ -8,4 +8,5 @@ export abstract class PasswordGenerationService { getHistory: () => Promise; addHistory: (password: string) => Promise; clear: () => Promise; + passwordStrength: (password: string, userInputs?: string[]) => zxcvbn.ZXCVBNResult; } diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index bad3b3ced9..0880385677 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -7,6 +7,7 @@ import { ApiService } from '../../abstractions/api.service'; import { AuthService } from '../../abstractions/auth.service'; import { CryptoService } from '../../abstractions/crypto.service'; import { I18nService } from '../../abstractions/i18n.service'; +import { PasswordGenerationService } from '../../abstractions/passwordGeneration.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { StateService } from '../../abstractions/state.service'; @@ -20,13 +21,16 @@ export class RegisterComponent { hint: string = ''; showPassword: boolean = false; formPromise: Promise; + masterPasswordScore: number; protected successRoute = 'login'; + private masterPasswordStrengthTimeout: any; constructor(protected authService: AuthService, protected router: Router, protected i18nService: I18nService, protected cryptoService: CryptoService, protected apiService: ApiService, protected stateService: StateService, - protected platformUtilsService: PlatformUtilsService) { } + protected platformUtilsService: PlatformUtilsService, + protected passwordGenerationService: PasswordGenerationService) { } async submit() { if (this.email == null || this.email === '') { @@ -55,6 +59,16 @@ export class RegisterComponent { return; } + const strengthResult = this.passwordGenerationService.passwordStrength(this.masterPassword, null); + if (strengthResult != null && strengthResult.score < 3) { + const result = await this.platformUtilsService.showDialog(this.i18nService.t('weakMasterPasswordDesc'), + this.i18nService.t('weakMasterPassword'), this.i18nService.t('yes'), this.i18nService.t('no'), + 'warning'); + if (!result) { + return; + } + } + this.name = this.name === '' ? null : this.name; this.email = this.email.trim().toLowerCase(); const kdf = KdfType.PBKDF2_SHA256; @@ -87,4 +101,14 @@ export class RegisterComponent { this.showPassword = !this.showPassword; document.getElementById(confirmField ? 'masterPasswordRetype' : 'masterPassword').focus(); } + + updatePasswordStrength() { + if (this.masterPasswordStrengthTimeout != null) { + clearTimeout(this.masterPasswordStrengthTimeout); + } + this.masterPasswordStrengthTimeout = setTimeout(() => { + const strengthResult = this.passwordGenerationService.passwordStrength(this.masterPassword, null); + this.masterPasswordScore = strengthResult == null ? null : strengthResult.score; + }, 300); + } } diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index f5b9b083aa..0eb2d738fe 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -1,3 +1,5 @@ +import * as zxcvbn from 'zxcvbn'; + import { CipherString } from '../models/domain/cipherString'; import { GeneratedPasswordHistory } from '../models/domain/generatedPasswordHistory'; @@ -240,6 +242,20 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return await this.storageService.remove(Keys.history); } + passwordStrength(password: string, userInputs: string[] = null): zxcvbn.ZXCVBNResult { + if (password == null || password.length === 0) { + return null; + } + let globalUserInputs = ['bitwarden', 'bit', 'warden']; + if (userInputs != null) { + globalUserInputs = globalUserInputs.concat(userInputs); + } + // Use a hash set to get rid of any duplicate user inputs + const finalUserInputs = Array.from(new Set(globalUserInputs)); + const result = zxcvbn(password, finalUserInputs); + return result; + } + private async encryptHistory(history: GeneratedPasswordHistory[]): Promise { if (history == null || history.length === 0) { return Promise.resolve([]); From c297728967bc12d7d4444b171a7d1fd79b68ec7d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 12 Nov 2018 23:22:37 -0500 Subject: [PATCH 0629/1626] getPasswordStrengthUserInput --- src/angular/components/register.component.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index 0880385677..92acafe88d 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -59,7 +59,8 @@ export class RegisterComponent { return; } - const strengthResult = this.passwordGenerationService.passwordStrength(this.masterPassword, null); + const strengthResult = this.passwordGenerationService.passwordStrength(this.masterPassword, + this.getPasswordStrengthUserInput()); if (strengthResult != null && strengthResult.score < 3) { const result = await this.platformUtilsService.showDialog(this.i18nService.t('weakMasterPasswordDesc'), this.i18nService.t('weakMasterPassword'), this.i18nService.t('yes'), this.i18nService.t('no'), @@ -69,8 +70,6 @@ export class RegisterComponent { } } - this.name = this.name === '' ? null : this.name; - this.email = this.email.trim().toLowerCase(); const kdf = KdfType.PBKDF2_SHA256; const useLowerKdf = this.platformUtilsService.isEdge() || this.platformUtilsService.isIE(); const kdfIterations = useLowerKdf ? 10000 : 100000; @@ -107,8 +106,21 @@ export class RegisterComponent { clearTimeout(this.masterPasswordStrengthTimeout); } this.masterPasswordStrengthTimeout = setTimeout(() => { - const strengthResult = this.passwordGenerationService.passwordStrength(this.masterPassword, null); + const strengthResult = this.passwordGenerationService.passwordStrength(this.masterPassword, + this.getPasswordStrengthUserInput()); this.masterPasswordScore = strengthResult == null ? null : strengthResult.score; }, 300); } + + private getPasswordStrengthUserInput() { + let userInput: string[] = []; + const atPosition = this.email.indexOf('@'); + if (atPosition > -1) { + userInput = userInput.concat(this.email.substr(0, atPosition).trim().toLowerCase().split(/[^A-Za-z0-9]/)); + } + if (this.name != null && this.name !== '') { + userInput = userInput.concat(this.name.trim().toLowerCase().split(' ')); + } + return userInput; + } } From 17e7ee4838071ea836867b06863a71ab047ed443 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 13 Nov 2018 20:43:45 -0500 Subject: [PATCH 0630/1626] support for new attachment keys --- .../components/attachments.component.ts | 3 +- src/angular/components/view.component.ts | 3 +- src/models/data/attachmentData.ts | 2 ++ src/models/domain/attachment.ts | 34 +++++++++++++++++-- src/models/request/attachmentRequest.ts | 4 +++ src/models/request/cipherRequest.ts | 13 ++++++- src/models/response/attachmentResponse.ts | 2 ++ src/models/view/attachmentView.ts | 2 ++ src/services/cipher.service.ts | 17 ++++++++-- src/services/crypto.service.ts | 3 +- 10 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 src/models/request/attachmentRequest.ts diff --git a/src/angular/components/attachments.component.ts b/src/angular/components/attachments.component.ts index 5ef23b12bb..c05b3026ab 100644 --- a/src/angular/components/attachments.component.ts +++ b/src/angular/components/attachments.component.ts @@ -143,7 +143,8 @@ export class AttachmentsComponent implements OnInit { try { const buf = await response.arrayBuffer(); - const key = await this.cryptoService.getOrgKey(this.cipher.organizationId); + const key = attachment.key != null ? attachment.key : + await this.cryptoService.getOrgKey(this.cipher.organizationId); const decBuf = await this.cryptoService.decryptFromBytes(buf, key); this.platformUtilsService.saveFile(this.win, decBuf, null, attachment.fileName); } catch (e) { diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index c257493e6a..b645f92ff5 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -171,7 +171,8 @@ export class ViewComponent implements OnDestroy, OnInit { try { const buf = await response.arrayBuffer(); - const key = await this.cryptoService.getOrgKey(this.cipher.organizationId); + const key = attachment.key != null ? attachment.key : + await this.cryptoService.getOrgKey(this.cipher.organizationId); const decBuf = await this.cryptoService.decryptFromBytes(buf, key); this.platformUtilsService.saveFile(this.win, decBuf, null, attachment.fileName); } catch (e) { diff --git a/src/models/data/attachmentData.ts b/src/models/data/attachmentData.ts index 63b822174c..004376a8dd 100644 --- a/src/models/data/attachmentData.ts +++ b/src/models/data/attachmentData.ts @@ -4,6 +4,7 @@ export class AttachmentData { id: string; url: string; fileName: string; + key: string; size: number; sizeName: string; @@ -14,6 +15,7 @@ export class AttachmentData { this.id = response.id; this.url = response.url; this.fileName = response.fileName; + this.key = response.key; this.size = response.size; this.sizeName = response.sizeName; } diff --git a/src/models/domain/attachment.ts b/src/models/domain/attachment.ts index cf50a18c03..6db86bebdd 100644 --- a/src/models/domain/attachment.ts +++ b/src/models/domain/attachment.ts @@ -1,15 +1,21 @@ import { AttachmentData } from '../data/attachmentData'; +import { AttachmentView } from '../view/attachmentView'; + import { CipherString } from './cipherString'; import Domain from './domainBase'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; -import { AttachmentView } from '../view/attachmentView'; +import { CryptoService } from '../../abstractions/crypto.service'; + +import { Utils } from '../../misc/utils'; export class Attachment extends Domain { id: string; url: string; size: number; sizeName: string; + key: CipherString; fileName: CipherString; constructor(obj?: AttachmentData, alreadyEncrypted: boolean = false) { @@ -24,13 +30,34 @@ export class Attachment extends Domain { url: null, sizeName: null, fileName: null, + key: null, }, alreadyEncrypted, ['id', 'url', 'sizeName']); } - decrypt(orgId: string): Promise { - return this.decryptObj(new AttachmentView(this), { + async decrypt(orgId: string): Promise { + const view = await this.decryptObj(new AttachmentView(this), { fileName: null, }, orgId); + + if (this.key != null) { + let cryptoService: CryptoService; + const containerService = (Utils.global as any).bitwardenContainerService; + if (containerService) { + cryptoService = containerService.getCryptoService(); + } else { + throw new Error('global bitwardenContainerService not initialized.'); + } + + try { + const orgKey = await cryptoService.getOrgKey(orgId); + const decValue = await cryptoService.decryptToBytes(this.key, orgKey); + view.key = new SymmetricCryptoKey(decValue); + } catch (e) { + // TODO: error? + } + } + + return view; } toAttachmentData(): AttachmentData { @@ -40,6 +67,7 @@ export class Attachment extends Domain { url: null, sizeName: null, fileName: null, + key: null, }, ['id', 'url', 'sizeName']); return a; } diff --git a/src/models/request/attachmentRequest.ts b/src/models/request/attachmentRequest.ts new file mode 100644 index 0000000000..d341ae63a0 --- /dev/null +++ b/src/models/request/attachmentRequest.ts @@ -0,0 +1,4 @@ +export class AttachmentRequest { + fileName: string; + key: string; +} diff --git a/src/models/request/cipherRequest.ts b/src/models/request/cipherRequest.ts index 46d1762018..46717dd6a8 100644 --- a/src/models/request/cipherRequest.ts +++ b/src/models/request/cipherRequest.ts @@ -8,6 +8,7 @@ import { IdentityApi } from '../api/identityApi'; import { LoginApi } from '../api/loginApi'; import { SecureNoteApi } from '../api/secureNoteApi'; +import { AttachmentRequest } from './attachmentRequest'; import { PasswordHistoryRequest } from './passwordHistoryRequest'; export class CipherRequest { @@ -23,7 +24,9 @@ export class CipherRequest { identity: IdentityApi; fields: FieldApi[]; passwordHistory: PasswordHistoryRequest[]; + // Deprecated, remove at some point and rename attachments2 to attachments attachments: { [id: string]: string; }; + attachments2: { [id: string]: AttachmentRequest; }; constructor(cipher: Cipher) { this.type = cipher.type; @@ -119,8 +122,16 @@ export class CipherRequest { if (cipher.attachments) { this.attachments = {}; + this.attachments2 = {}; cipher.attachments.forEach((attachment) => { - this.attachments[attachment.id] = attachment.fileName ? attachment.fileName.encryptedString : null; + const fileName = attachment.fileName ? attachment.fileName.encryptedString : null; + this.attachments[attachment.id] = fileName; + const attachmentRequest = new AttachmentRequest(); + attachmentRequest.fileName = fileName; + if (attachment.key != null) { + attachmentRequest.key = attachment.key.encryptedString; + } + this.attachments2[attachment.id] = attachmentRequest; }); } } diff --git a/src/models/response/attachmentResponse.ts b/src/models/response/attachmentResponse.ts index 1ad530bd6d..fd45c709b6 100644 --- a/src/models/response/attachmentResponse.ts +++ b/src/models/response/attachmentResponse.ts @@ -2,6 +2,7 @@ export class AttachmentResponse { id: string; url: string; fileName: string; + key: string; size: number; sizeName: string; @@ -9,6 +10,7 @@ export class AttachmentResponse { this.id = response.Id; this.url = response.Url; this.fileName = response.FileName; + this.key = response.Key; this.size = response.Size; this.sizeName = response.SizeName; } diff --git a/src/models/view/attachmentView.ts b/src/models/view/attachmentView.ts index efb0081efa..aff342c9f8 100644 --- a/src/models/view/attachmentView.ts +++ b/src/models/view/attachmentView.ts @@ -1,6 +1,7 @@ import { View } from './view'; import { Attachment } from '../domain/attachment'; +import { SymmetricCryptoKey } from '../domain/symmetricCryptoKey'; export class AttachmentView implements View { id: string; @@ -8,6 +9,7 @@ export class AttachmentView implements View { size: number; sizeName: string; fileName: string; + key: SymmetricCryptoKey; constructor(a?: Attachment) { if (!a) { diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 257d59b4a6..4926a6328a 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -173,7 +173,10 @@ export class CipherService implements CipherServiceAbstraction { attachment.url = model.url; const promise = this.encryptObjProperty(model, attachment, { fileName: null, - }, key).then(() => { + }, key).then(async () => { + if (model.key != null) { + attachment.key = await this.cryptoService.encrypt(model.key.key, key); + } encAttachments.push(attachment); }); promises.push(promise); @@ -519,14 +522,18 @@ export class CipherService implements CipherServiceAbstraction { data: ArrayBuffer, admin = false): Promise { const key = await this.cryptoService.getOrgKey(cipher.organizationId); const encFileName = await this.cryptoService.encrypt(filename, key); - const encData = await this.cryptoService.encryptToBytes(data, key); + + const dataEncKey = await this.cryptoService.makeEncKey(key); + const encData = await this.cryptoService.encryptToBytes(data, dataEncKey[0]); const fd = new FormData(); try { + fd.append('key', dataEncKey[1].encryptedString); const blob = new Blob([encData], { type: 'application/octet-stream' }); fd.append('data', blob, encFileName.encryptedString); } catch (e) { if (Utils.isNode && !Utils.isBrowser) { + fd.append('key', dataEncKey[1].encryptedString); fd.append('data', Buffer.from(encData) as any, { filepath: encFileName.encryptedString, contentType: 'application/octet-stream', @@ -753,15 +760,19 @@ export class CipherService implements CipherServiceAbstraction { const buf = await attachmentResponse.arrayBuffer(); const decBuf = await this.cryptoService.decryptFromBytes(buf, null); const key = await this.cryptoService.getOrgKey(organizationId); - const encData = await this.cryptoService.encryptToBytes(decBuf, key); const encFileName = await this.cryptoService.encrypt(attachmentView.fileName, key); + const dataEncKey = await this.cryptoService.makeEncKey(key); + const encData = await this.cryptoService.encryptToBytes(decBuf, dataEncKey[0]); + const fd = new FormData(); try { + fd.append('key', dataEncKey[1].encryptedString); const blob = new Blob([encData], { type: 'application/octet-stream' }); fd.append('data', blob, encFileName.encryptedString); } catch (e) { if (Utils.isNode && !Utils.isBrowser) { + fd.append('key', dataEncKey[1].encryptedString); fd.append('data', Buffer.from(encData) as any, { filepath: encFileName.encryptedString, contentType: 'application/octet-stream', diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 01b4ba4b62..37a2abe43a 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -333,8 +333,9 @@ export class CryptoService implements CryptoServiceAbstraction { } async makeEncKey(key: SymmetricCryptoKey): Promise<[SymmetricCryptoKey, CipherString]> { + const theKey = await this.getKeyForEncryption(key); const encKey = await this.cryptoFunctionService.randomBytes(64); - return this.buildEncKey(key, encKey); + return this.buildEncKey(theKey, encKey); } async remakeEncKey(key: SymmetricCryptoKey): Promise<[SymmetricCryptoKey, CipherString]> { From f485fbb6870203b60ac27bcbc2e12bb45f24b538 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 14 Nov 2018 15:19:59 -0500 Subject: [PATCH 0631/1626] fix old attachments by reuploading them --- .../components/attachments.component.ts | 53 +++++++++++++++++++ src/models/view/cipherView.ts | 11 ++++ 2 files changed, 64 insertions(+) diff --git a/src/angular/components/attachments.component.ts b/src/angular/components/attachments.component.ts index c05b3026ab..af925f23ff 100644 --- a/src/angular/components/attachments.component.ts +++ b/src/angular/components/attachments.component.ts @@ -20,6 +20,7 @@ export class AttachmentsComponent implements OnInit { @Input() cipherId: string; @Output() onUploadedAttachment = new EventEmitter(); @Output() onDeletedAttachment = new EventEmitter(); + @Output() onReuploadedAttachment = new EventEmitter(); cipher: CipherView; cipherDomain: Cipher; @@ -27,6 +28,7 @@ export class AttachmentsComponent implements OnInit { canAccessAttachments: boolean; formPromise: Promise; deletePromises: { [id: string]: Promise; } = {}; + reuploadPromises: { [id: string]: Promise; } = {}; constructor(protected cipherService: CipherService, protected i18nService: I18nService, protected cryptoService: CryptoService, protected userService: UserService, @@ -154,6 +156,57 @@ export class AttachmentsComponent implements OnInit { a.downloading = false; } + protected async reuploadCipherAttachment(attachment: AttachmentView, admin: boolean) { + const a = (attachment as any); + if (attachment.key != null || a.downloading || this.reuploadPromises[attachment.id] != null) { + return; + } + + try { + this.reuploadPromises[attachment.id] = Promise.resolve().then(async () => { + // 1. Download + a.downloading = true; + const response = await fetch(new Request(attachment.url, { cache: 'no-cache' })); + if (response.status !== 200) { + this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred')); + a.downloading = false; + return; + } + + try { + // 2. Resave + const buf = await response.arrayBuffer(); + const key = attachment.key != null ? attachment.key : + await this.cryptoService.getOrgKey(this.cipher.organizationId); + const decBuf = await this.cryptoService.decryptFromBytes(buf, key); + this.cipherDomain = await this.cipherService.saveAttachmentRawWithServer( + this.cipherDomain, attachment.fileName, decBuf, admin); + this.cipher = await this.cipherDomain.decrypt(); + + // 3. Delete old + this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id); + await this.deletePromises[attachment.id]; + const foundAttachment = this.cipher.attachments.filter((a2) => a2.id === attachment.id); + if (foundAttachment.length > 0) { + const i = this.cipher.attachments.indexOf(foundAttachment[0]); + if (i > -1) { + this.cipher.attachments.splice(i, 1); + } + } + + this.platformUtilsService.eventTrack('Reuploaded Attachment'); + this.platformUtilsService.showToast('success', null, this.i18nService.t('attachmentSaved')); + this.onReuploadedAttachment.emit(); + } catch (e) { + this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred')); + } + + a.downloading = false; + }); + await this.reuploadPromises[attachment.id]; + } catch { } + } + protected loadCipher() { return this.cipherService.get(this.cipherId); } diff --git a/src/models/view/cipherView.ts b/src/models/view/cipherView.ts index 5c69728c1c..b231be4f28 100644 --- a/src/models/view/cipherView.ts +++ b/src/models/view/cipherView.ts @@ -74,6 +74,17 @@ export class CipherView implements View { return this.attachments && this.attachments.length > 0; } + get hasOldAttachments(): boolean { + if (this.hasAttachments) { + for (let i = 0; i < this.attachments.length; i++) { + if (this.attachments[i].key == null) { + return true; + } + } + } + return false; + } + get hasFields(): boolean { return this.fields && this.fields.length > 0; } From f514e2bb676cb0ecca81c678cb054ce596999971 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 14 Nov 2018 23:13:34 -0500 Subject: [PATCH 0632/1626] autoConfirmFingerprints constant --- src/services/constants.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index cd0a260359..f492209a53 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -14,6 +14,7 @@ export class ConstantsService { static readonly localeKey: string = 'locale'; static readonly themeKey: string = 'theme'; static readonly collapsedGroupingsKey: string = 'collapsedGroupings'; + static readonly autoConfirmFingerprints: string = 'autoConfirmFingerprints'; readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; readonly disableGaKey: string = ConstantsService.disableGaKey; @@ -29,4 +30,5 @@ export class ConstantsService { readonly localeKey: string = ConstantsService.localeKey; readonly themeKey: string = ConstantsService.themeKey; readonly collapsedGroupingsKey: string = ConstantsService.collapsedGroupingsKey; + readonly autoConfirmFingerprints: string = ConstantsService.autoConfirmFingerprints; } From be080f4f17b782fdb22c77560bd235e81346fb21 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 15 Nov 2018 08:55:29 -0500 Subject: [PATCH 0633/1626] only use shareAttachmentWithServer for old attachments --- src/services/cipher.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 4926a6328a..20538d3ab0 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -467,7 +467,9 @@ export class CipherService implements CipherServiceAbstraction { const attachmentPromises: Array> = []; if (cipher.attachments != null) { cipher.attachments.forEach((attachment) => { - attachmentPromises.push(this.shareAttachmentWithServer(attachment, cipher.id, organizationId)); + if (attachment.key == null) { + attachmentPromises.push(this.shareAttachmentWithServer(attachment, cipher.id, organizationId)); + } }); } await Promise.all(attachmentPromises); From b48c48470801eb88aaec2c2cd84baeb6f1876923 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 15 Nov 2018 15:27:04 -0500 Subject: [PATCH 0634/1626] misc score props moved to jslib --- src/angular/components/register.component.ts | 30 ++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index 92acafe88d..a990aae0b8 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -32,6 +32,36 @@ export class RegisterComponent { protected platformUtilsService: PlatformUtilsService, protected passwordGenerationService: PasswordGenerationService) { } + get masterPasswordScoreWidth() { + return this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20; + } + + get masterPasswordScoreColor() { + switch (this.masterPasswordScore) { + case 4: + return 'success'; + case 3: + return 'primary'; + case 2: + return 'warning'; + default: + return 'danger'; + } + } + + get masterPasswordScoreText() { + switch (this.masterPasswordScore) { + case 4: + return this.i18nService.t('strong'); + case 3: + return this.i18nService.t('good'); + case 2: + return this.i18nService.t('weak'); + default: + return this.masterPasswordScore != null ? this.i18nService.t('weak') : null; + } + } + async submit() { if (this.email == null || this.email === '') { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), From da47faca5c9a41f732136448461a06fc6e6fe023 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 15 Nov 2018 15:33:52 -0500 Subject: [PATCH 0635/1626] allow progress bars to highlight box row still --- src/angular/directives/box-row.directive.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/angular/directives/box-row.directive.ts b/src/angular/directives/box-row.directive.ts index 8269a46b64..9f2cb60368 100644 --- a/src/angular/directives/box-row.directive.ts +++ b/src/angular/directives/box-row.directive.ts @@ -30,7 +30,9 @@ export class BoxRowDirective implements OnInit { } @HostListener('click', ['$event']) onClick(event: Event) { - if (event.target !== this.el) { + const target = event.target as HTMLElement; + if (target !== this.el && !target.classList.contains('progress') && + !target.classList.contains('progress-bar')) { return; } From e13f317aad5378dafb6557d7631c84f0e2ea058d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 15 Nov 2018 16:55:54 -0500 Subject: [PATCH 0636/1626] icon load function --- src/angular/components/icon.component.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/angular/components/icon.component.ts b/src/angular/components/icon.component.ts index be525b926d..21008256fe 100644 --- a/src/angular/components/icon.component.ts +++ b/src/angular/components/icon.component.ts @@ -37,7 +37,7 @@ export class IconComponent implements OnChanges { private iconsUrl: string; - constructor(private environmentService: EnvironmentService, private stateService: StateService) { + constructor(environmentService: EnvironmentService, protected stateService: StateService) { this.iconsUrl = environmentService.iconsUrl; if (!this.iconsUrl) { if (environmentService.baseUrl) { @@ -49,8 +49,16 @@ export class IconComponent implements OnChanges { } async ngOnChanges() { + console.log('load it changes'); this.imageEnabled = !(await this.stateService.get(ConstantsService.disableFaviconKey)); + this.load(); + } + get iconCode(): string { + return IconMap[this.icon]; + } + + protected load() { switch (this.cipher.type) { case CipherType.Login: this.icon = 'fa-globe'; @@ -70,10 +78,6 @@ export class IconComponent implements OnChanges { } } - get iconCode(): string { - return IconMap[this.icon]; - } - private setLoginIcon() { if (this.cipher.login.uri) { let hostnameUri = this.cipher.login.uri; From 464bca8c4d745eb86bdb0b36e6e4a983caa3c891 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 15 Nov 2018 16:56:10 -0500 Subject: [PATCH 0637/1626] remove console log --- src/angular/components/icon.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/angular/components/icon.component.ts b/src/angular/components/icon.component.ts index 21008256fe..cae0655ac1 100644 --- a/src/angular/components/icon.component.ts +++ b/src/angular/components/icon.component.ts @@ -49,7 +49,6 @@ export class IconComponent implements OnChanges { } async ngOnChanges() { - console.log('load it changes'); this.imageEnabled = !(await this.stateService.get(ConstantsService.disableFaviconKey)); this.load(); } From 5d5200b12e36dc170d7d3ec7bd34f0e7f99cd422 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 20 Nov 2018 15:58:27 -0500 Subject: [PATCH 0638/1626] premiumAccessAddon option when creating org --- src/models/request/organizationCreateRequest.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/models/request/organizationCreateRequest.ts b/src/models/request/organizationCreateRequest.ts index 57a5f125ca..3188afb3a1 100644 --- a/src/models/request/organizationCreateRequest.ts +++ b/src/models/request/organizationCreateRequest.ts @@ -9,6 +9,7 @@ export class OrganizationCreateRequest { paymentToken: string; additionalSeats: number; additionalStorageGb: number; + premiumAccessAddon: boolean; collectionName: string; country: string; } From 1536f161f783c5972de1a375aed887710ee39eb8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 21 Nov 2018 14:29:54 -0500 Subject: [PATCH 0639/1626] dont show card/identities constants --- src/services/constants.service.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index f492209a53..246f04c1dd 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -15,6 +15,8 @@ export class ConstantsService { static readonly themeKey: string = 'theme'; static readonly collapsedGroupingsKey: string = 'collapsedGroupings'; static readonly autoConfirmFingerprints: string = 'autoConfirmFingerprints'; + static readonly dontShowCardsCurrentTab: string = 'dontShowCardsCurrentTab'; + static readonly dontShowIdentitiesCurrentTab: string = 'dontShowIdentitiesCurrentTab'; readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; readonly disableGaKey: string = ConstantsService.disableGaKey; @@ -31,4 +33,6 @@ export class ConstantsService { readonly themeKey: string = ConstantsService.themeKey; readonly collapsedGroupingsKey: string = ConstantsService.collapsedGroupingsKey; readonly autoConfirmFingerprints: string = ConstantsService.autoConfirmFingerprints; + readonly dontShowCardsCurrentTab: string = ConstantsService.dontShowCardsCurrentTab; + readonly dontShowIdentitiesCurrentTab: string = ConstantsService.dontShowIdentitiesCurrentTab; } From 46ad44595159bc0e4765b22ec388f6a542f6907c Mon Sep 17 00:00:00 2001 From: h44z Date: Mon, 26 Nov 2018 14:12:39 +0100 Subject: [PATCH 0640/1626] Add support for Steam TOTP (#20) --- src/services/totp.service.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/services/totp.service.ts b/src/services/totp.service.ts index 293810112b..35ed683407 100644 --- a/src/services/totp.service.ts +++ b/src/services/totp.service.ts @@ -48,6 +48,8 @@ export class TotpService implements TotpServiceAbstraction { alg = algParam; } } + } else if(key.toLowerCase().indexOf('steam://') === 0) { + keyB32 = key.substr('steam://'.length); } const epoch = Math.round(new Date().getTime() / 1000.0); @@ -71,6 +73,20 @@ export class TotpService implements TotpServiceAbstraction { /* tslint:enable */ let otp = (binary % Math.pow(10, digits)).toString(); otp = this.leftpad(otp, digits, '0'); + + if(key.toLowerCase().indexOf('steam://') === 0) { + const steamchars = [ + '2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C', + 'D', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', + 'R', 'T', 'V', 'W', 'X', 'Y']; + otp = ""; + let fullcode = binary & 0x7fffffff; + for (var i = 0; i < 5; i++) { + otp += steamchars[fullcode % steamchars.length]; + fullcode = Math.trunc(fullcode / steamchars.length); + } + } + return otp; } From 4b85172b5218c3d82d8d73d2b1c4a60edd628b5e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 26 Nov 2018 08:22:17 -0500 Subject: [PATCH 0641/1626] clean up totp service --- src/services/totp.service.ts | 52 +++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/src/services/totp.service.ts b/src/services/totp.service.ts index 35ed683407..742b5bdc18 100644 --- a/src/services/totp.service.ts +++ b/src/services/totp.service.ts @@ -6,7 +6,9 @@ import { TotpService as TotpServiceAbstraction } from '../abstractions/totp.serv import { Utils } from '../misc/utils'; -const b32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; +const B32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; +const SteamChars = ['2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'M', + 'N', 'P', 'Q', 'R', 'T', 'V', 'W', 'X', 'Y']; export class TotpService implements TotpServiceAbstraction { constructor(private storageService: StorageService, private cryptoFunctionService: CryptoFunctionService) { } @@ -19,7 +21,9 @@ export class TotpService implements TotpServiceAbstraction { let alg: 'sha1' | 'sha256' | 'sha512' = 'sha1'; let digits = 6; let keyB32 = key; - if (key.toLowerCase().indexOf('otpauth://') === 0) { + const isOtpAuth = key.toLowerCase().indexOf('otpauth://') === 0; + const isSteamAuth = !isOtpAuth && key.toLowerCase().indexOf('steam://') === 0; + if (isOtpAuth) { const params = Utils.getQueryParams(key); if (params.has('digits') && params.get('digits') != null) { try { @@ -48,14 +52,14 @@ export class TotpService implements TotpServiceAbstraction { alg = algParam; } } - } else if(key.toLowerCase().indexOf('steam://') === 0) { + } else if (isSteamAuth) { keyB32 = key.substr('steam://'.length); } const epoch = Math.round(new Date().getTime() / 1000.0); - const timeHex = this.leftpad(this.dec2hex(Math.floor(epoch / period)), 16, '0'); + const timeHex = this.leftPad(this.decToHex(Math.floor(epoch / period)), 16, '0'); const timeBytes = Utils.fromHexToArray(timeHex); - const keyBytes = this.b32tobytes(keyB32); + const keyBytes = this.b32ToBytes(keyB32); if (!keyBytes.length || !timeBytes.length) { return null; @@ -71,20 +75,18 @@ export class TotpService implements TotpServiceAbstraction { const binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) | ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff); /* tslint:enable */ - let otp = (binary % Math.pow(10, digits)).toString(); - otp = this.leftpad(otp, digits, '0'); - if(key.toLowerCase().indexOf('steam://') === 0) { - const steamchars = [ - '2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C', - 'D', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', - 'R', 'T', 'V', 'W', 'X', 'Y']; - otp = ""; - let fullcode = binary & 0x7fffffff; - for (var i = 0; i < 5; i++) { - otp += steamchars[fullcode % steamchars.length]; - fullcode = Math.trunc(fullcode / steamchars.length); + let otp = ''; + if (isSteamAuth) { + // tslint:disable-next-line + let fullCode = binary & 0x7fffffff; + for (let i = 0; i < 5; i++) { + otp += SteamChars[fullCode % SteamChars.length]; + fullCode = Math.trunc(fullCode / SteamChars.length); } + } else { + otp = (binary % Math.pow(10, digits)).toString(); + otp = this.leftPad(otp, digits, '0'); } return otp; @@ -109,23 +111,23 @@ export class TotpService implements TotpServiceAbstraction { // Helpers - private leftpad(s: string, l: number, p: string): string { + private leftPad(s: string, l: number, p: string): string { if (l + 1 >= s.length) { s = Array(l + 1 - s.length).join(p) + s; } return s; } - private dec2hex(d: number): string { + private decToHex(d: number): string { return (d < 15.5 ? '0' : '') + Math.round(d).toString(16); } - private b32tohex(s: string): string { + private b32ToHex(s: string): string { s = s.toUpperCase(); let cleanedInput = ''; for (let i = 0; i < s.length; i++) { - if (b32Chars.indexOf(s[i]) < 0) { + if (B32Chars.indexOf(s[i]) < 0) { continue; } @@ -136,11 +138,11 @@ export class TotpService implements TotpServiceAbstraction { let bits = ''; let hex = ''; for (let i = 0; i < s.length; i++) { - const byteIndex = b32Chars.indexOf(s.charAt(i)); + const byteIndex = B32Chars.indexOf(s.charAt(i)); if (byteIndex < 0) { continue; } - bits += this.leftpad(byteIndex.toString(2), 5, '0'); + bits += this.leftPad(byteIndex.toString(2), 5, '0'); } for (let i = 0; i + 4 <= bits.length; i += 4) { const chunk = bits.substr(i, 4); @@ -149,8 +151,8 @@ export class TotpService implements TotpServiceAbstraction { return hex; } - private b32tobytes(s: string): Uint8Array { - return Utils.fromHexToArray(this.b32tohex(s)); + private b32ToBytes(s: string): Uint8Array { + return Utils.fromHexToArray(this.b32ToHex(s)); } private async sign(keyBytes: Uint8Array, timeBytes: Uint8Array, alg: 'sha1' | 'sha256' | 'sha512') { From 5b59f888e03c2246abb6dca14ba19eac0cbe38b1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 26 Nov 2018 08:23:31 -0500 Subject: [PATCH 0642/1626] SteamChars to regular string --- src/services/totp.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/services/totp.service.ts b/src/services/totp.service.ts index 742b5bdc18..ddda7df29b 100644 --- a/src/services/totp.service.ts +++ b/src/services/totp.service.ts @@ -7,8 +7,7 @@ import { TotpService as TotpServiceAbstraction } from '../abstractions/totp.serv import { Utils } from '../misc/utils'; const B32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; -const SteamChars = ['2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'M', - 'N', 'P', 'Q', 'R', 'T', 'V', 'W', 'X', 'Y']; +const SteamChars = '23456789BCDFGHJKMNPQRTVWXY'; export class TotpService implements TotpServiceAbstraction { constructor(private storageService: StorageService, private cryptoFunctionService: CryptoFunctionService) { } From 0ae636aa53dc7e2f39a21e60612f6bd7771a34a0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 26 Nov 2018 08:26:36 -0500 Subject: [PATCH 0643/1626] use digits for steam --- src/services/totp.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/totp.service.ts b/src/services/totp.service.ts index ddda7df29b..aa4f8dd143 100644 --- a/src/services/totp.service.ts +++ b/src/services/totp.service.ts @@ -53,6 +53,7 @@ export class TotpService implements TotpServiceAbstraction { } } else if (isSteamAuth) { keyB32 = key.substr('steam://'.length); + digits = 5; } const epoch = Math.round(new Date().getTime() / 1000.0); @@ -79,7 +80,7 @@ export class TotpService implements TotpServiceAbstraction { if (isSteamAuth) { // tslint:disable-next-line let fullCode = binary & 0x7fffffff; - for (let i = 0; i < 5; i++) { + for (let i = 0; i < digits; i++) { otp += SteamChars[fullCode % SteamChars.length]; fullCode = Math.trunc(fullCode / SteamChars.length); } From 64a6015a67a8110cd8a4ef897e1338c71c4b6b49 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 26 Nov 2018 15:29:54 -0500 Subject: [PATCH 0644/1626] RSA HMAC cipher string types are deprecated --- src/abstractions/crypto.service.ts | 2 +- src/services/crypto.service.ts | 32 ++++-------------------------- 2 files changed, 5 insertions(+), 29 deletions(-) diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 547c142d84..0d0d9b4701 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -36,7 +36,7 @@ export abstract class CryptoService { remakeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise; encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise; - rsaEncrypt: (data: ArrayBuffer, publicKey?: ArrayBuffer, key?: SymmetricCryptoKey) => Promise; + rsaEncrypt: (data: ArrayBuffer, publicKey?: ArrayBuffer) => Promise; decryptToBytes: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise; decryptToUtf8: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise; decryptFromBytes: (encBuf: ArrayBuffer, key: SymmetricCryptoKey) => Promise; diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 37a2abe43a..8c5f68b099 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -308,8 +308,7 @@ export class CryptoService implements CryptoServiceAbstraction { async makeShareKey(): Promise<[CipherString, SymmetricCryptoKey]> { const shareKey = await this.cryptoFunctionService.randomBytes(64); const publicKey = await this.getPublicKey(); - const encKey = await this.getEncKey(); - const encShareKey = await this.rsaEncrypt(shareKey, publicKey, encKey); + const encShareKey = await this.rsaEncrypt(shareKey, publicKey); return [encShareKey, new SymmetricCryptoKey(shareKey)]; } @@ -380,7 +379,7 @@ export class CryptoService implements CryptoServiceAbstraction { return encBytes.buffer; } - async rsaEncrypt(data: ArrayBuffer, publicKey?: ArrayBuffer, key?: SymmetricCryptoKey): Promise { + async rsaEncrypt(data: ArrayBuffer, publicKey?: ArrayBuffer): Promise { if (publicKey == null) { publicKey = await this.getPublicKey(); } @@ -388,15 +387,8 @@ export class CryptoService implements CryptoServiceAbstraction { throw new Error('Public key unavailable.'); } - let type = EncryptionType.Rsa2048_OaepSha1_B64; const encBytes = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, 'sha1'); - let mac: string = null; - if (key != null && key.macKey != null) { - type = EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64; - const macBytes = await this.cryptoFunctionService.hmac(encBytes, key.macKey, 'sha256'); - mac = Utils.fromBufferToB64(macBytes); - } - return new CipherString(type, Utils.fromBufferToB64(encBytes), null, mac); + return new CipherString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encBytes)); } async decryptToBytes(cipherString: CipherString, key?: SymmetricCryptoKey): Promise { @@ -591,15 +583,9 @@ export class CryptoService implements CryptoServiceAbstraction { switch (encType) { case EncryptionType.Rsa2048_OaepSha256_B64: case EncryptionType.Rsa2048_OaepSha1_B64: - if (encPieces.length !== 1) { - throw new Error('Invalid cipher format.'); - } - break; + // HmacSha256 types are deprecated 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.'); @@ -610,16 +596,6 @@ export class CryptoService implements CryptoServiceAbstraction { } const data = Utils.fromB64ToArray(encPieces[0]).buffer; - const key = await this.getEncKey(); - if (key != null && key.macKey != null && encPieces.length > 1) { - const mac = Utils.fromB64ToArray(encPieces[1]).buffer; - const computedMac = await this.cryptoFunctionService.hmac(data, key.macKey, 'sha256'); - const macsEqual = await this.cryptoFunctionService.compare(mac, computedMac); - if (!macsEqual) { - throw new Error('MAC failed.'); - } - } - const privateKey = await this.getPrivateKey(); if (privateKey == null) { throw new Error('No private key.'); From 6920cf77b9373ed25f882ce68f17dde582e0a5be Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 27 Nov 2018 08:31:08 -0500 Subject: [PATCH 0645/1626] show project name in update alert titles --- src/electron/updater.main.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/electron/updater.main.ts b/src/electron/updater.main.ts index 5130159cf4..59f61aaf1d 100644 --- a/src/electron/updater.main.ts +++ b/src/electron/updater.main.ts @@ -28,7 +28,8 @@ export class UpdaterMain { constructor(private i18nService: I18nService, private windowMain: WindowMain, private gitHubProject: string, private onCheckingForUpdate: () => void = null, - private onReset: () => void = null, private onUpdateDownloaded: () => void = null) { + private onReset: () => void = null, private onUpdateDownloaded: () => void = null, + private projectName: string) { autoUpdater.logger = log; const linuxCanUpdate = process.platform === 'linux' && isAppImage(); @@ -58,7 +59,7 @@ export class UpdaterMain { const result = dialog.showMessageBox(this.windowMain.win, { type: 'info', - title: this.i18nService.t('updateAvailable'), + title: this.i18nService.t(this.projectName) + ' - ' + this.i18nService.t('updateAvailable'), message: this.i18nService.t('updateAvailable'), detail: this.i18nService.t('updateAvailableDesc'), buttons: [this.i18nService.t('yes'), this.i18nService.t('no')], @@ -99,7 +100,7 @@ export class UpdaterMain { const result = dialog.showMessageBox(this.windowMain.win, { type: 'info', - title: this.i18nService.t('restartToUpdate'), + title: this.i18nService.t(this.projectName) + ' - ' + this.i18nService.t('restartToUpdate'), message: this.i18nService.t('restartToUpdate'), detail: this.i18nService.t('restartToUpdateDesc', info.version), buttons: [this.i18nService.t('restart'), this.i18nService.t('later')], From 739d308498ab68df3e37772265733c81b27f2cc2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 28 Nov 2018 08:54:15 -0500 Subject: [PATCH 0646/1626] normalize name and email on registration --- src/angular/components/register.component.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index a990aae0b8..ba1002d817 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -100,6 +100,8 @@ export class RegisterComponent { } } + this.name = this.name === '' ? null : this.name; + this.email = this.email.trim().toLowerCase(); const kdf = KdfType.PBKDF2_SHA256; const useLowerKdf = this.platformUtilsService.isEdge() || this.platformUtilsService.isIE(); const kdfIterations = useLowerKdf ? 10000 : 100000; From 2a960da31cda2e0fdc2f891dd6ff835bcb32d074 Mon Sep 17 00:00:00 2001 From: Alexandre Lapeyre Date: Wed, 28 Nov 2018 15:51:02 +0100 Subject: [PATCH 0647/1626] Replace logoType by logoPath in BreachAccountResponse (#23) --- src/models/response/breachAccountResponse.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/response/breachAccountResponse.ts b/src/models/response/breachAccountResponse.ts index 68cf62d4ec..842d8fa860 100644 --- a/src/models/response/breachAccountResponse.ts +++ b/src/models/response/breachAccountResponse.ts @@ -6,7 +6,7 @@ export class BreachAccountResponse { domain: string; isActive: boolean; isVerified: boolean; - logoType: string; + logoPath: string; modifiedDate: string; name: string; pwnCount: number; @@ -20,7 +20,7 @@ export class BreachAccountResponse { this.domain = response.Domain; this.isActive = response.IsActive; this.isVerified = response.IsVerified; - this.logoType = response.LogoType; + this.logoPath = response.LogoPath; this.modifiedDate = response.ModifiedDate; this.name = response.Name; this.pwnCount = response.PwnCount; From 5609fecbcee6d0608ea28985c31688511e735a59 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 30 Nov 2018 09:49:08 -0500 Subject: [PATCH 0648/1626] continue logic --- src/misc/serviceUtils.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/misc/serviceUtils.ts b/src/misc/serviceUtils.ts index a466c83fbd..f70e119ae6 100644 --- a/src/misc/serviceUtils.ts +++ b/src/misc/serviceUtils.ts @@ -14,16 +14,17 @@ export class ServiceUtils { const partName = parts[partIndex]; for (let i = 0; i < nodeTree.length; i++) { - if (nodeTree[i].node.name === parts[partIndex]) { - if (end && nodeTree[i].node.id !== obj.id) { - // Another node with the same name. - nodeTree.push(new TreeNode(obj, partName, parent)); - return; - } - ServiceUtils.nestedTraverse(nodeTree[i].children, partIndex + 1, parts, - obj, nodeTree[i].node, delimiter); + if (nodeTree[i].node.name !== parts[partIndex]) { + continue; + } + if (end && nodeTree[i].node.id !== obj.id) { + // Another node with the same name. + nodeTree.push(new TreeNode(obj, partName, parent)); return; } + ServiceUtils.nestedTraverse(nodeTree[i].children, partIndex + 1, parts, + obj, nodeTree[i].node, delimiter); + return; } if (nodeTree.filter((n) => n.node.name === partName).length === 0) { From d5308a3bf512a53adf2a8db70b53d5a26755edcf Mon Sep 17 00:00:00 2001 From: h44z Date: Mon, 3 Dec 2018 21:55:43 +0100 Subject: [PATCH 0649/1626] Close to tray (#21) * Close to Tray implemented * Enable Tray Icon on Linux * Remove unnecessary function * Revert 26a3a98e384cc62a94f2b213af3a6543874b3d95 --- src/electron/baseMenu.ts | 2 +- src/electron/electronConstants.ts | 1 + src/electron/tray.main.ts | 12 ++++++++++++ src/electron/window.main.ts | 7 +++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/electron/baseMenu.ts b/src/electron/baseMenu.ts index 3e7e3d3c59..cf5676c35b 100644 --- a/src/electron/baseMenu.ts +++ b/src/electron/baseMenu.ts @@ -93,7 +93,7 @@ export class BaseMenu { }, { label: this.i18nService.t('close'), - role: 'close', + role: 'quit', }, ], }; diff --git a/src/electron/electronConstants.ts b/src/electron/electronConstants.ts index c22e2b5640..8887e6142e 100644 --- a/src/electron/electronConstants.ts +++ b/src/electron/electronConstants.ts @@ -1,4 +1,5 @@ export class ElectronConstants { static readonly enableMinimizeToTrayKey: string = 'enableMinimizeToTray'; + static readonly enableCloseToTrayKey: string = 'enableCloseToTray'; static readonly enableTrayKey: string = 'enableTray'; } diff --git a/src/electron/tray.main.ts b/src/electron/tray.main.ts index 5a8ec4b3fc..4ae77343a1 100644 --- a/src/electron/tray.main.ts +++ b/src/electron/tray.main.ts @@ -68,6 +68,17 @@ export class TrayMain { }); } + if (process.platform === 'win32') { + this.windowMain.win.on('close', async (e: Event) => { + if (await this.storageService.get(ElectronConstants.enableCloseToTrayKey)) { + if(!this.windowMain.isQuitting){ + e.preventDefault(); + this.hideToTray(); + } + } + }); + } + this.windowMain.win.on('show', async (e: Event) => { const enableTray = await this.storageService.get(ElectronConstants.enableTrayKey); if (!enableTray) { @@ -124,6 +135,7 @@ export class TrayMain { } private closeWindow() { + this.windowMain.isQuitting = true; if (this.windowMain.win != null) { this.windowMain.win.close(); } diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index d5f9302424..33f7baddd8 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -13,6 +13,7 @@ const Keys = { export class WindowMain { win: BrowserWindow; + isQuitting: boolean = false; private windowStateChangeTimer: NodeJS.Timer; private windowStates: { [key: string]: any; } = {}; @@ -39,6 +40,12 @@ export class WindowMain { } } + // This method will be called when Electron is shutting + // down the application. + app.on('before-quit', () => { + this.isQuitting = true; + }); + // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. From 1da72b9a97111e2cf749dd6f18e85f98f321bf3d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 3 Dec 2018 16:06:28 -0500 Subject: [PATCH 0650/1626] adjustments to close to tray changes --- src/electron/baseMenu.ts | 2 +- src/electron/tray.main.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/electron/baseMenu.ts b/src/electron/baseMenu.ts index cf5676c35b..3e7e3d3c59 100644 --- a/src/electron/baseMenu.ts +++ b/src/electron/baseMenu.ts @@ -93,7 +93,7 @@ export class BaseMenu { }, { label: this.i18nService.t('close'), - role: 'quit', + role: 'close', }, ], }; diff --git a/src/electron/tray.main.ts b/src/electron/tray.main.ts index 4ae77343a1..287d233303 100644 --- a/src/electron/tray.main.ts +++ b/src/electron/tray.main.ts @@ -66,12 +66,10 @@ export class TrayMain { this.hideToTray(); } }); - } - if (process.platform === 'win32') { this.windowMain.win.on('close', async (e: Event) => { if (await this.storageService.get(ElectronConstants.enableCloseToTrayKey)) { - if(!this.windowMain.isQuitting){ + if (!this.windowMain.isQuitting) { e.preventDefault(); this.hideToTray(); } From 9283a29d35a18f058f3c84f8aaa919b911f1940a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 7 Dec 2018 15:41:43 -0500 Subject: [PATCH 0651/1626] Domain match blacklist --- src/services/cipher.service.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 20538d3ab0..3f54ffa718 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -52,6 +52,10 @@ const Keys = { neverDomains: 'neverDomains', }; +const DomainMatchBlacklist = new Map>([ + ['google.com', new Set(['script.google.com'])], +]); + export class CipherService implements CipherServiceAbstraction { // tslint:disable-next-line _decryptedCipherCache: CipherView[]; @@ -352,7 +356,14 @@ export class CipherService implements CipherServiceAbstraction { case undefined: case UriMatchType.Domain: if (domain != null && u.domain != null && matchingDomains.indexOf(u.domain) > -1) { - return true; + if (DomainMatchBlacklist.has(u.domain)) { + const domainUrlHost = Utils.getHost(url); + if (!DomainMatchBlacklist.get(u.domain).has(domainUrlHost)) { + return true; + } + } else { + return true; + } } break; case UriMatchType.Host: From 37616a148afc5d0520cd4b11e816fb1ab238c777 Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Sat, 8 Dec 2018 17:06:11 +0100 Subject: [PATCH 0652/1626] Added password coloring pipe (which also sanitizes HTML) (#24) --- src/angular/pipes/color-password.pipe.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/angular/pipes/color-password.pipe.ts diff --git a/src/angular/pipes/color-password.pipe.ts b/src/angular/pipes/color-password.pipe.ts new file mode 100644 index 0000000000..1867032faf --- /dev/null +++ b/src/angular/pipes/color-password.pipe.ts @@ -0,0 +1,20 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { SafeHtml } from '@angular/platform-browser'; + +/** + * A pipe that sanitizes HTML and highlights numbers and special characters (in different colors each). + */ +@Pipe({ name: 'colorPassword' }) +export class ColorPasswordPipe implements PipeTransform { + transform(password: string): SafeHtml { + return password + // Sanitize HTML first. + .replace(/&/g, '&') + .replace(//g, '>') + // Replace special chars (since that will exclude numbers anyway). + .replace(/((&|<|>|[^\w ])+)/g, `$1`) + // Finally replace the numbers. + .replace(/(\d+)/g, `$1`); + } +} From 9694d2922ef033c6c5eead2c7b4a308b6d93bf2f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 8 Dec 2018 13:48:10 -0500 Subject: [PATCH 0653/1626] wrap every character in a span --- src/angular/pipes/color-password.pipe.ts | 45 ++++++++++++++++++------ 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/angular/pipes/color-password.pipe.ts b/src/angular/pipes/color-password.pipe.ts index 1867032faf..f5de7967e5 100644 --- a/src/angular/pipes/color-password.pipe.ts +++ b/src/angular/pipes/color-password.pipe.ts @@ -1,20 +1,43 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import { SafeHtml } from '@angular/platform-browser'; +import { + Pipe, + PipeTransform, +} from '@angular/core'; /** * A pipe that sanitizes HTML and highlights numbers and special characters (in different colors each). */ @Pipe({ name: 'colorPassword' }) export class ColorPasswordPipe implements PipeTransform { - transform(password: string): SafeHtml { - return password + transform(password: string) { + let colorizedPassword = ''; + for (let i = 0; i < password.length; i++) { + let character = password[i]; + let isSpecial = false; // Sanitize HTML first. - .replace(/&/g, '&') - .replace(//g, '>') - // Replace special chars (since that will exclude numbers anyway). - .replace(/((&|<|>|[^\w ])+)/g, `$1`) - // Finally replace the numbers. - .replace(/(\d+)/g, `$1`); + switch (character) { + case '&': + character = '&'; + isSpecial = true; + break; + case '<': + character = '<'; + isSpecial = true; + break; + case '>': + character = '>'; + isSpecial = true; + break; + default: + break; + } + let type = 'letter'; + if (isSpecial || character.match(/[^\w ]/)) { + type = 'special'; + } else if (character.match(/\d/)) { + type = 'number'; + } + colorizedPassword += '' + character + ''; + } + return colorizedPassword; } } From ed74f73a8ca2d26cacf2f74b4cd14c6150711b87 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 12 Dec 2018 17:06:13 -0500 Subject: [PATCH 0654/1626] dashlane json importer --- src/importers/dashlaneCsvImporter.ts | 90 -------------- src/importers/dashlaneJsonImporter.ts | 164 ++++++++++++++++++++++++++ src/services/import.service.ts | 8 +- 3 files changed, 168 insertions(+), 94 deletions(-) delete mode 100644 src/importers/dashlaneCsvImporter.ts create mode 100644 src/importers/dashlaneJsonImporter.ts diff --git a/src/importers/dashlaneCsvImporter.ts b/src/importers/dashlaneCsvImporter.ts deleted file mode 100644 index 028a79a055..0000000000 --- a/src/importers/dashlaneCsvImporter.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { BaseImporter } from './baseImporter'; -import { Importer } from './importer'; - -import { ImportResult } from '../models/domain/importResult'; - -export class DashlaneCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { - const result = new ImportResult(); - const results = this.parseCsv(data, false); - if (results == null) { - result.success = false; - return result; - } - - results.forEach((value) => { - let skip = false; - if (value.length < 2) { - return; - } - - const cipher = this.initLoginCipher(); - cipher.name = this.getValueOrDefault(value[0], '--'); - - if (value.length === 2) { - cipher.login.uris = this.makeUriArray(value[1]); - } else if (value.length === 3) { - cipher.login.uris = this.makeUriArray(value[1]); - cipher.login.username = this.getValueOrDefault(value[2]); - } else if (value.length === 4) { - if (this.isNullOrWhitespace(value[2]) && this.isNullOrWhitespace(value[3])) { - cipher.login.username = value[1]; - cipher.notes = value[2] + '\n' + value[3]; - } else { - cipher.login.username = value[2]; - cipher.notes = value[1] + '\n' + value[3]; - } - } else if (value.length === 5) { - cipher.login.uris = this.makeUriArray(value[1]); - cipher.login.username = this.getValueOrDefault(value[2]); - cipher.login.password = this.getValueOrDefault(value[3]); - cipher.notes = this.getValueOrDefault(value[4]); - } else if (value.length === 6) { - if (this.isNullOrWhitespace(value[2])) { - cipher.login.username = this.getValueOrDefault(value[3]); - cipher.login.password = this.getValueOrDefault(value[4]); - cipher.notes = this.getValueOrDefault(value[5]); - } else { - cipher.login.username = this.getValueOrDefault(value[2]); - cipher.login.password = this.getValueOrDefault(value[3]); - cipher.notes = this.getValueOrDefault(value[4], '') + '\n' + this.getValueOrDefault(value[5], ''); - } - cipher.login.uris = this.makeUriArray(value[1]); - } else if (value.length === 7) { - if (this.isNullOrWhitespace(value[2])) { - cipher.login.username = this.getValueOrDefault(value[3]); - cipher.notes = this.getValueOrDefault(value[4], '') + '\n' + this.getValueOrDefault(value[6], ''); - } else { - cipher.login.username = this.getValueOrDefault(value[2]); - cipher.notes = this.getValueOrDefault(value[3], '') + '\n' + - this.getValueOrDefault(value[4], '') + '\n' + this.getValueOrDefault(value[6], ''); - } - cipher.login.uris = this.makeUriArray(value[1]); - cipher.login.password = this.getValueOrDefault(value[5]); - } else { - for (let i = 1; i < value.length; i++) { - cipher.notes += (value[i] + '\n'); - if (value[i] === 'NO_TYPE') { - skip = true; - break; - } - } - } - - if (skip) { - return; - } - if (this.isNullOrWhitespace(cipher.login.username)) { - cipher.login.username = null; - } - if (this.isNullOrWhitespace(cipher.login.password)) { - cipher.login.password = null; - } - this.cleanupCipher(cipher); - result.ciphers.push(cipher); - }); - - result.success = true; - return result; - } -} diff --git a/src/importers/dashlaneJsonImporter.ts b/src/importers/dashlaneJsonImporter.ts new file mode 100644 index 0000000000..582f3180c5 --- /dev/null +++ b/src/importers/dashlaneJsonImporter.ts @@ -0,0 +1,164 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CardView } from '../models/view/cardView'; +import { CipherView } from '../models/view/cipherView'; +import { IdentityView } from '../models/view/identityView'; +import { SecureNoteView } from '../models/view/secureNoteView'; + +import { CipherType } from '../enums/cipherType'; + +const HandledResults = new Set(['ADDRESS', 'AUTHENTIFIANT', 'BANKSTATEMENT', 'IDCARD', 'IDENTITY', + 'PAYMENTMEANS_CREDITCARD', 'PAYMENTMEAN_PAYPAL', 'EMAIL']); + +export class DashlaneJsonImporter extends BaseImporter implements Importer { + private result: ImportResult; + + parse(data: string): ImportResult { + this.result = new ImportResult(); + const results = JSON.parse(data); + if (results == null || results.length === 0) { + this.result.success = false; + return this.result; + } + + if (results.ADDRESS != null) { + this.processAddress(results.ADDRESS); + } + if (results.AUTHENTIFIANT != null) { + this.processAuth(results.AUTHENTIFIANT); + } + if (results.BANKSTATEMENT != null) { + this.processNote(results.BANKSTATEMENT, 'BankAccountName'); + } + if (results.IDCARD != null) { + this.processNote(results.IDCARD, 'Fullname'); + } + if (results.PAYMENTMEANS_CREDITCARD != null) { + this.processCard(results.PAYMENTMEANS_CREDITCARD); + } + if (results.IDENTITY != null) { + this.processIdentity(results.IDENTITY); + } + + for (const key in results) { + if (results.hasOwnProperty(key) && !HandledResults.has(key)) { + this.processNote(results[key], null, 'Generic Note'); + } + } + + this.result.success = true; + return this.result; + } + + private processAuth(results: any[]) { + results.forEach((credential: any) => { + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(credential.title); + + cipher.login.username = this.getValueOrDefault(credential.login, + this.getValueOrDefault(credential.secondaryLogin)); + if (this.isNullOrWhitespace(cipher.login.username)) { + cipher.login.username = this.getValueOrDefault(credential.email); + } else if (!this.isNullOrWhitespace(credential.email)) { + cipher.notes = ('Email: ' + credential.email + '\n'); + } + + cipher.login.password = this.getValueOrDefault(credential.password); + cipher.login.uris = this.makeUriArray(credential.domain); + cipher.notes += this.getValueOrDefault(credential.note, ''); + + this.convertToNoteIfNeeded(cipher); + this.cleanupCipher(cipher); + this.result.ciphers.push(cipher); + }); + } + + private processIdentity(results: any[]) { + results.forEach((obj: any) => { + const cipher = new CipherView(); + cipher.identity = new IdentityView(); + cipher.type = CipherType.Identity; + cipher.name = this.getValueOrDefault(obj.fullName, ''); + const nameParts = cipher.name.split(' '); + if (nameParts.length > 0) { + cipher.identity.firstName = this.getValueOrDefault(nameParts[0]); + } + if (nameParts.length === 2) { + cipher.identity.lastName = this.getValueOrDefault(nameParts[1]); + } else if (nameParts.length === 3) { + cipher.identity.middleName = this.getValueOrDefault(nameParts[1]); + cipher.identity.lastName = this.getValueOrDefault(nameParts[2]); + } + cipher.identity.username = this.getValueOrDefault(obj.pseudo); + this.cleanupCipher(cipher); + this.result.ciphers.push(cipher); + }); + } + + private processAddress(results: any[]) { + results.forEach((obj: any) => { + const cipher = new CipherView(); + cipher.identity = new IdentityView(); + cipher.type = CipherType.Identity; + cipher.name = this.getValueOrDefault(obj.addressName); + cipher.identity.address1 = this.getValueOrDefault(obj.addressFull); + cipher.identity.city = this.getValueOrDefault(obj.city); + cipher.identity.state = this.getValueOrDefault(obj.state); + cipher.identity.postalCode = this.getValueOrDefault(obj.zipcode); + cipher.identity.country = this.getValueOrDefault(obj.country); + if (cipher.identity.country != null) { + cipher.identity.country = cipher.identity.country.toUpperCase(); + } + this.cleanupCipher(cipher); + this.result.ciphers.push(cipher); + }); + } + + private processCard(results: any[]) { + results.forEach((obj: any) => { + const cipher = new CipherView(); + cipher.card = new CardView(); + cipher.type = CipherType.Card; + cipher.name = this.getValueOrDefault(obj.bank); + cipher.card.number = this.getValueOrDefault(obj.cardNumber); + cipher.card.brand = this.getCardBrand(cipher.card.number); + cipher.card.cardholderName = this.getValueOrDefault(obj.owner); + if (!this.isNullOrWhitespace(cipher.card.brand)) { + if (this.isNullOrWhitespace(cipher.name)) { + cipher.name = cipher.card.brand; + } else { + cipher.name += (' - ' + cipher.card.brand); + } + } + this.cleanupCipher(cipher); + this.result.ciphers.push(cipher); + }); + } + + private processNote(results: any[], nameProperty: string, name: string = null) { + results.forEach((obj: any) => { + const cipher = new CipherView(); + cipher.secureNote = new SecureNoteView(); + cipher.type = CipherType.SecureNote; + if (name != null) { + cipher.name = name; + } else { + cipher.name = this.getValueOrDefault(obj[nameProperty]); + } + cipher.notes = ''; + for (const key in obj) { + if (obj.hasOwnProperty(key) && key !== nameProperty) { + const val = obj[key].toString(); + if (!this.isNullOrWhitespace(val)) { + cipher.notes += (key + ': ' + obj[key].toString() + '\n'); + } + } + } + this.cleanupCipher(cipher); + this.result.ciphers.push(cipher); + }); + } +} diff --git a/src/services/import.service.ts b/src/services/import.service.ts index c35f916259..784f90ab00 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -25,7 +25,7 @@ import { BitwardenCsvImporter } from '../importers/bitwardenCsvImporter'; import { BlurCsvImporter } from '../importers/blurCsvImporter'; import { ChromeCsvImporter } from '../importers/chromeCsvImporter'; import { ClipperzHtmlImporter } from '../importers/clipperzHtmlImporter'; -import { DashlaneCsvImporter } from '../importers/dashlaneCsvImporter'; +import { DashlaneJsonImporter } from '../importers/dashlaneJsonImporter'; import { EnpassCsvImporter } from '../importers/enpassCsvImporter'; import { FirefoxCsvImporter } from '../importers/firefoxCsvImporter'; import { GnomeJsonImporter } from '../importers/gnomeJsonImporter'; @@ -63,7 +63,7 @@ export class ImportService implements ImportServiceAbstraction { { id: 'firefoxcsv', name: 'Firefox (csv)' }, { id: 'keepass2xml', name: 'KeePass 2 (xml)' }, { id: '1password1pif', name: '1Password (1pif)' }, - { id: 'dashlanecsv', name: 'Dashlane (csv)' }, + { id: 'dashlanejson', name: 'Dashlane (json)' }, ]; regularImportOptions: ImportOption[] = [ @@ -185,8 +185,8 @@ export class ImportService implements ImportServiceAbstraction { return new EnpassCsvImporter(); case 'pwsafexml': return new PasswordSafeXmlImporter(); - case 'dashlanecsv': - return new DashlaneCsvImporter(); + case 'dashlanejson': + return new DashlaneJsonImporter(); case 'msecurecsv': return new MSecureCsvImporter(); case 'stickypasswordxml': From 6a958afd16cd727c1cfade2f4a0aff3ae22ec28a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 12 Dec 2018 19:37:06 -0500 Subject: [PATCH 0655/1626] length check on userInputs --- src/services/passwordGeneration.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 0eb2d738fe..3e5dcd1895 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -247,7 +247,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return null; } let globalUserInputs = ['bitwarden', 'bit', 'warden']; - if (userInputs != null) { + if (userInputs != null && userInputs.length > 0) { globalUserInputs = globalUserInputs.concat(userInputs); } // Use a hash set to get rid of any duplicate user inputs From 18ac2db3232735357f33b29b81ce2daeb9a85011 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 13 Dec 2018 10:58:48 -0500 Subject: [PATCH 0656/1626] normalize boolean type values for custom fields --- src/services/cipher.service.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 3f54ffa718..b9ffee1eb7 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -211,6 +211,10 @@ export class CipherService implements CipherServiceAbstraction { async encryptField(fieldModel: FieldView, key: SymmetricCryptoKey): Promise { const field = new Field(); field.type = fieldModel.type; + // normalize boolean type field values + if (fieldModel.type === FieldType.Boolean && fieldModel.value !== 'true') { + fieldModel.value = 'false'; + } await this.encryptObjProperty(fieldModel, field, { name: null, From e64fdf4e2137cfd28377ab02bf9967442eb599e7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 13 Dec 2018 14:34:37 -0500 Subject: [PATCH 0657/1626] enpass json importer --- src/importers/enpassJsonImporter.ts | 121 ++++++++++++++++++++++++++++ src/services/import.service.ts | 4 + 2 files changed, 125 insertions(+) create mode 100644 src/importers/enpassJsonImporter.ts diff --git a/src/importers/enpassJsonImporter.ts b/src/importers/enpassJsonImporter.ts new file mode 100644 index 0000000000..1a76a2ce6a --- /dev/null +++ b/src/importers/enpassJsonImporter.ts @@ -0,0 +1,121 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CardView } from '../models/view/cardView'; +import { CipherView } from '../models/view/cipherView'; + +import { CipherType } from '../enums/cipherType'; + +export class EnpassJsonImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = JSON.parse(data); + if (results == null || results.items == null || results.items.length === 0) { + result.success = false; + return result; + } + + results.items.forEach((item: any) => { + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(item.title); + cipher.favorite = item.favorite > 0; + + if (item.template_type != null && item.fields != null && item.fields.length > 0) { + if (item.template_type.indexOf('login.') === 0 || item.template_type.indexOf('password.') === 0) { + this.processLogin(cipher, item.fields); + } else if (item.template_type.indexOf('creditcard.') === 0) { + this.processCard(cipher, item.fields); + } else if (item.template_type.indexOf('identity.') < 0 && + item.fields.find((f: any) => f.type === 'password' && !this.isNullOrWhitespace(f.value)) != null) { + this.processLogin(cipher, item.fields); + } else { + this.processNote(cipher, item.fields); + } + } + + cipher.notes += ('\n' + this.getValueOrDefault(item.note, '')); + this.convertToNoteIfNeeded(cipher); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } + + private processLogin(cipher: CipherView, fields: any[]) { + const urls: string[] = []; + fields.forEach((field: any) => { + if (this.isNullOrWhitespace(field.value) || field.type === 'section') { + return; + } + + if ((field.type === 'username' || field.type === 'email') && + this.isNullOrWhitespace(cipher.login.username)) { + cipher.login.username = field.value; + } else if (field.type === 'password' && this.isNullOrWhitespace(cipher.login.password)) { + cipher.login.password = field.value; + } else if (field.type === 'totp' && this.isNullOrWhitespace(cipher.login.totp)) { + cipher.login.totp = field.value; + } else if (field.type === 'url') { + urls.push(field.value); + } else { + this.processKvp(cipher, field.label, field.value); + } + }); + cipher.login.uris = this.makeUriArray(urls); + } + + private processCard(cipher: CipherView, fields: any[]) { + cipher.card = new CardView(); + cipher.type = CipherType.Card; + fields.forEach((field: any) => { + if (this.isNullOrWhitespace(field.value) || field.type === 'section' || field.type === 'ccType') { + return; + } + + if (field.type === 'ccName' && this.isNullOrWhitespace(cipher.card.cardholderName)) { + cipher.card.cardholderName = field.value; + } else if (field.type === 'ccNumber' && this.isNullOrWhitespace(cipher.card.number)) { + cipher.card.number = field.value; + cipher.card.brand = this.getCardBrand(cipher.card.number); + } else if (field.type === 'ccCvc' && this.isNullOrWhitespace(cipher.card.code)) { + cipher.card.code = field.value; + } else if (field.type === 'ccExpiry' && this.isNullOrWhitespace(cipher.card.expYear)) { + const parts = field.value.split('/'); + if (parts.length === 2) { + let month: string = null; + let year: string = null; + if (parts[0].length === 1 || parts[0].length === 2) { + month = parts[0]; + if (month.length === 2 && month[0] === '0') { + month = month.substr(1, 1); + } + } + if (parts[1].length === 2 || parts[1].length === 4) { + year = month.length === 2 ? '20' + parts[1] : parts[1]; + } + if (month != null && year != null) { + cipher.card.expMonth = month; + cipher.card.expYear = year; + } + } else { + this.processKvp(cipher, field.label, field.value); + } + } else { + this.processKvp(cipher, field.label, field.value); + } + }); + } + + private processNote(cipher: CipherView, fields: any[]) { + fields.forEach((field: any) => { + if (this.isNullOrWhitespace(field.value) || field.type === 'section') { + return; + } + this.processKvp(cipher, field.label, field.value); + }); + } +} diff --git a/src/services/import.service.ts b/src/services/import.service.ts index 784f90ab00..3e53753ccd 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -27,6 +27,7 @@ import { ChromeCsvImporter } from '../importers/chromeCsvImporter'; import { ClipperzHtmlImporter } from '../importers/clipperzHtmlImporter'; import { DashlaneJsonImporter } from '../importers/dashlaneJsonImporter'; import { EnpassCsvImporter } from '../importers/enpassCsvImporter'; +import { EnpassJsonImporter } from '../importers/enpassJsonImporter'; import { FirefoxCsvImporter } from '../importers/firefoxCsvImporter'; import { GnomeJsonImporter } from '../importers/gnomeJsonImporter'; import { Importer } from '../importers/importer'; @@ -72,6 +73,7 @@ export class ImportService implements ImportServiceAbstraction { { id: 'roboformcsv', name: 'RoboForm (csv)' }, { id: 'keepercsv', name: 'Keeper (csv)' }, { id: 'enpasscsv', name: 'Enpass (csv)' }, + { id: 'enpassjson', name: 'Enpass (json)' }, { id: 'safeincloudxml', name: 'SafeInCloud (xml)' }, { id: 'pwsafexml', name: 'Password Safe (xml)' }, { id: 'stickypasswordxml', name: 'Sticky Password (xml)' }, @@ -183,6 +185,8 @@ export class ImportService implements ImportServiceAbstraction { return new PasswordDragonXmlImporter(); case 'enpasscsv': return new EnpassCsvImporter(); + case 'enpassjson': + return new EnpassJsonImporter(); case 'pwsafexml': return new PasswordSafeXmlImporter(); case 'dashlanejson': From e55926336c0fc2362d3aa791dc9fe385d03f6ae8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 13 Dec 2018 14:37:55 -0500 Subject: [PATCH 0658/1626] use KVP for dashlane notes --- src/importers/dashlaneJsonImporter.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/importers/dashlaneJsonImporter.ts b/src/importers/dashlaneJsonImporter.ts index 582f3180c5..12241c34ba 100644 --- a/src/importers/dashlaneJsonImporter.ts +++ b/src/importers/dashlaneJsonImporter.ts @@ -148,13 +148,9 @@ export class DashlaneJsonImporter extends BaseImporter implements Importer { } else { cipher.name = this.getValueOrDefault(obj[nameProperty]); } - cipher.notes = ''; for (const key in obj) { if (obj.hasOwnProperty(key) && key !== nameProperty) { - const val = obj[key].toString(); - if (!this.isNullOrWhitespace(val)) { - cipher.notes += (key + ': ' + obj[key].toString() + '\n'); - } + this.processKvp(cipher, key, obj[key].toString()); } } this.cleanupCipher(cipher); From e10523cc6115d94a199f0c3342c5d8040304f7f8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 14 Dec 2018 13:55:44 -0500 Subject: [PATCH 0659/1626] getAll ciphers FromApiForOrganization --- src/abstractions/cipher.service.ts | 1 + src/services/cipher.service.ts | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index 7a0fab508f..97c9c37abc 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -22,6 +22,7 @@ export abstract class CipherService { getAllDecrypted: () => Promise; getAllDecryptedForGrouping: (groupingId: string, folder?: boolean) => Promise; getAllDecryptedForUrl: (url: string, includeOtherTypes?: CipherType[]) => Promise; + getAllFromApiForOrganization: (organizationId: string) => Promise; getLastUsedForUrl: (url: string) => Promise; updateLastUsedDate: (id: string) => Promise; saveNeverDomain: (domain: string) => Promise; diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index b9ffee1eb7..9b90cf50d2 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -405,6 +405,24 @@ export class CipherService implements CipherServiceAbstraction { }); } + async getAllFromApiForOrganization(organizationId: string): Promise { + const ciphers = await this.apiService.getCiphersOrganization(organizationId); + if (ciphers != null && ciphers.data != null && ciphers.data.length) { + const decCiphers: CipherView[] = []; + const promises: any[] = []; + ciphers.data.forEach((r) => { + const data = new CipherData(r); + const cipher = new Cipher(data); + promises.push(cipher.decrypt().then((c) => decCiphers.push(c))); + }); + await Promise.all(promises); + decCiphers.sort(this.getLocaleSortingFunction()); + return decCiphers; + } else { + return []; + } + } + async getLastUsedForUrl(url: string): Promise { const ciphers = await this.getAllDecryptedForUrl(url); if (ciphers.length === 0) { From 27566c3fd5a1040112278c7ad0a50c6b8d45e3e4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 14 Dec 2018 17:19:28 -0500 Subject: [PATCH 0660/1626] support old windows opvault format on 1password importer --- src/importers/onepassword1PifImporter.ts | 97 +++++++++++++++++------- 1 file changed, 71 insertions(+), 26 deletions(-) diff --git a/src/importers/onepassword1PifImporter.ts b/src/importers/onepassword1PifImporter.ts index 798c2cb640..aad0d31d8f 100644 --- a/src/importers/onepassword1PifImporter.ts +++ b/src/importers/onepassword1PifImporter.ts @@ -20,34 +20,11 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { } const item = JSON.parse(line); const cipher = this.initLoginCipher(); - cipher.favorite = item.openContents && item.openContents.faveIndex ? true : false; - cipher.name = this.getValueOrDefault(item.title, '--'); - if (item.typeName === 'securenotes.SecureNote') { - cipher.type = CipherType.SecureNote; - cipher.secureNote = new SecureNoteView(); - cipher.secureNote.type = SecureNoteType.Generic; - } else if (item.typeName === 'wallet.financial.CreditCard') { - cipher.type = CipherType.Card; - cipher.card = new CardView(); + if (this.isNullOrWhitespace(item.hmac)) { + this.processStandardItem(item, cipher); } else { - cipher.login.uris = this.makeUriArray(item.location); - } - - if (item.secureContents != null) { - if (!this.isNullOrWhitespace(item.secureContents.notesPlain)) { - cipher.notes = item.secureContents.notesPlain.split(this.newLineRegex).join('\n') + '\n'; - } - if (item.secureContents.fields != null) { - this.parseFields(item.secureContents.fields, cipher, 'designation', 'value', 'name'); - } - if (item.secureContents.sections != null) { - item.secureContents.sections.forEach((section: any) => { - if (section.fields != null) { - this.parseFields(section.fields, cipher, 'n', 'v', 't'); - } - }); - } + this.processWinOpVaultItem(item, cipher); } this.convertToNoteIfNeeded(cipher); @@ -59,6 +36,74 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { return this.result; } + private processWinOpVaultItem(item: any, cipher: CipherView) { + if (item.overview != null) { + cipher.name = this.getValueOrDefault(item.overview.title); + if (item.overview.URLs != null) { + const urls: string[] = []; + item.overview.URLs.forEach((url: any) => { + if (!this.isNullOrWhitespace(url.u)) { + urls.push(url.u); + } + }); + cipher.login.uris = this.makeUriArray(urls); + } + } + + if (item.details != null) { + if (!this.isNullOrWhitespace(item.details.ccnum) || !this.isNullOrWhitespace(item.details.cvv)) { + cipher.type = CipherType.Card; + cipher.card = new CardView(); + } + + if (!this.isNullOrWhitespace(item.details.notesPlain)) { + cipher.notes = item.details.notesPlain.split(this.newLineRegex).join('\n') + '\n'; + } + if (item.details.fields != null) { + this.parseFields(item.details.fields, cipher, 'designation', 'value', 'name'); + } + if (item.details.sections != null) { + item.details.sections.forEach((section: any) => { + if (section.fields != null) { + this.parseFields(section.fields, cipher, 'n', 'v', 't'); + } + }); + } + } + } + + private processStandardItem(item: any, cipher: CipherView) { + cipher.favorite = item.openContents && item.openContents.faveIndex ? true : false; + cipher.name = this.getValueOrDefault(item.title); + + if (item.typeName === 'securenotes.SecureNote') { + cipher.type = CipherType.SecureNote; + cipher.secureNote = new SecureNoteView(); + cipher.secureNote.type = SecureNoteType.Generic; + } else if (item.typeName === 'wallet.financial.CreditCard') { + cipher.type = CipherType.Card; + cipher.card = new CardView(); + } else { + cipher.login.uris = this.makeUriArray(item.location); + } + + if (item.secureContents != null) { + if (!this.isNullOrWhitespace(item.secureContents.notesPlain)) { + cipher.notes = item.secureContents.notesPlain.split(this.newLineRegex).join('\n') + '\n'; + } + if (item.secureContents.fields != null) { + this.parseFields(item.secureContents.fields, cipher, 'designation', 'value', 'name'); + } + if (item.secureContents.sections != null) { + item.secureContents.sections.forEach((section: any) => { + if (section.fields != null) { + this.parseFields(section.fields, cipher, 'n', 'v', 't'); + } + }); + } + } + } + private parseFields(fields: any[], cipher: CipherView, designationKey: string, valueKey: string, nameKey: string) { fields.forEach((field: any) => { if (field[valueKey] == null || field[valueKey].toString().trim() === '') { From 94f103c474655a81f43ad1f108b4408e4ffdcc17 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 17 Dec 2018 10:29:37 -0500 Subject: [PATCH 0661/1626] move export models to jslib --- src/models/export/card.ts | 44 ++++++++++++ src/models/export/cipher.ts | 100 ++++++++++++++++++++++++++ src/models/export/cipherWithIds.ts | 15 ++++ src/models/export/collection.ts | 27 +++++++ src/models/export/collectionWithId.ts | 13 ++++ src/models/export/field.ts | 34 +++++++++ src/models/export/folder.ts | 21 ++++++ src/models/export/folderWithId.ts | 13 ++++ src/models/export/identity.ts | 92 ++++++++++++++++++++++++ src/models/export/login.ts | 43 +++++++++++ src/models/export/loginUri.ts | 30 ++++++++ src/models/export/secureNote.ts | 26 +++++++ 12 files changed, 458 insertions(+) create mode 100644 src/models/export/card.ts create mode 100644 src/models/export/cipher.ts create mode 100644 src/models/export/cipherWithIds.ts create mode 100644 src/models/export/collection.ts create mode 100644 src/models/export/collectionWithId.ts create mode 100644 src/models/export/field.ts create mode 100644 src/models/export/folder.ts create mode 100644 src/models/export/folderWithId.ts create mode 100644 src/models/export/identity.ts create mode 100644 src/models/export/login.ts create mode 100644 src/models/export/loginUri.ts create mode 100644 src/models/export/secureNote.ts diff --git a/src/models/export/card.ts b/src/models/export/card.ts new file mode 100644 index 0000000000..fda37089c9 --- /dev/null +++ b/src/models/export/card.ts @@ -0,0 +1,44 @@ +import { CardView } from '../view/cardView'; + +export class Card { + static template(): Card { + const req = new Card(); + req.cardholderName = 'John Doe'; + req.brand = 'visa'; + req.number = '4242424242424242'; + req.expMonth = '04'; + req.expYear = '2023'; + req.code = '123'; + return req; + } + + static toView(req: Card, view = new CardView()) { + view.cardholderName = req.cardholderName; + view.brand = req.brand; + view.number = req.number; + view.expMonth = req.expMonth; + view.expYear = req.expYear; + view.code = req.code; + return view; + } + + cardholderName: string; + brand: string; + number: string; + expMonth: string; + expYear: string; + code: string; + + constructor(o?: CardView) { + if (o == null) { + return; + } + + this.cardholderName = o.cardholderName; + this.brand = o.brand; + this.number = o.number; + this.expMonth = o.expMonth; + this.expYear = o.expYear; + this.code = o.code; + } +} diff --git a/src/models/export/cipher.ts b/src/models/export/cipher.ts new file mode 100644 index 0000000000..82f9326021 --- /dev/null +++ b/src/models/export/cipher.ts @@ -0,0 +1,100 @@ +import { CipherType } from '../../enums/cipherType'; + +import { CipherView } from '../view/cipherView'; + +import { Card } from './card'; +import { Field } from './field'; +import { Identity } from './identity'; +import { Login } from './login'; +import { SecureNote } from './secureNote'; + +export class Cipher { + static template(): Cipher { + const req = new Cipher(); + req.organizationId = null; + req.folderId = null; + req.type = CipherType.Login; + req.name = 'Item name'; + req.notes = 'Some notes about this item.'; + req.favorite = false; + req.fields = []; + req.login = null; + req.secureNote = null; + req.card = null; + req.identity = null; + return req; + } + + static toView(req: Cipher, view = new CipherView()) { + view.type = req.type; + view.folderId = req.folderId; + if (view.organizationId == null) { + view.organizationId = req.organizationId; + } + view.name = req.name; + view.notes = req.notes; + view.favorite = req.favorite; + + if (req.fields != null) { + view.fields = req.fields.map((f) => Field.toView(f)); + } + + switch (req.type) { + case CipherType.Login: + view.login = Login.toView(req.login); + break; + case CipherType.SecureNote: + view.secureNote = SecureNote.toView(req.secureNote); + break; + case CipherType.Card: + view.card = Card.toView(req.card); + break; + case CipherType.Identity: + view.identity = Identity.toView(req.identity); + break; + } + + return view; + } + + type: CipherType; + folderId: string; + organizationId: string; + name: string; + notes: string; + favorite: boolean; + fields: Field[]; + login: Login; + secureNote: SecureNote; + card: Card; + identity: Identity; + + // Use build method instead of ctor so that we can control order of JSON stringify for pretty print + build(o: CipherView) { + this.organizationId = o.organizationId; + this.folderId = o.folderId; + this.type = o.type; + this.name = o.name; + this.notes = o.notes; + this.favorite = o.favorite; + + if (o.fields != null) { + this.fields = o.fields.map((f) => new Field(f)); + } + + switch (o.type) { + case CipherType.Login: + this.login = new Login(o.login); + break; + case CipherType.SecureNote: + this.secureNote = new SecureNote(o.secureNote); + break; + case CipherType.Card: + this.card = new Card(o.card); + break; + case CipherType.Identity: + this.identity = new Identity(o.identity); + break; + } + } +} diff --git a/src/models/export/cipherWithIds.ts b/src/models/export/cipherWithIds.ts new file mode 100644 index 0000000000..a9635b999a --- /dev/null +++ b/src/models/export/cipherWithIds.ts @@ -0,0 +1,15 @@ +import { Cipher } from './cipher'; + +import { CipherView } from '../view/cipherView'; + +export class CipherWithIds extends Cipher { + id: string; + collectionIds: string[]; + + // Use build method instead of ctor so that we can control order of JSON stringify for pretty print + build(o: CipherView) { + this.id = o.id; + super.build(o); + this.collectionIds = o.collectionIds; + } +} diff --git a/src/models/export/collection.ts b/src/models/export/collection.ts new file mode 100644 index 0000000000..85080e39f7 --- /dev/null +++ b/src/models/export/collection.ts @@ -0,0 +1,27 @@ +import { CollectionView } from '../view/collectionView'; + +export class Collection { + static template(): Collection { + const req = new Collection(); + req.organizationId = '00000000-0000-0000-0000-000000000000'; + req.name = 'Collection name'; + return req; + } + + static toView(req: Collection, view = new CollectionView()) { + view.name = req.name; + if (view.organizationId == null) { + view.organizationId = req.organizationId; + } + return view; + } + + organizationId: string; + name: string; + + // Use build method instead of ctor so that we can control order of JSON stringify for pretty print + build(o: CollectionView) { + this.organizationId = o.organizationId; + this.name = o.name; + } +} diff --git a/src/models/export/collectionWithId.ts b/src/models/export/collectionWithId.ts new file mode 100644 index 0000000000..10d8181377 --- /dev/null +++ b/src/models/export/collectionWithId.ts @@ -0,0 +1,13 @@ +import { Collection } from './collection'; + +import { CollectionView } from '../view/collectionView'; + +export class CollectionWithId extends Collection { + id: string; + + // Use build method instead of ctor so that we can control order of JSON stringify for pretty print + build(o: CollectionView) { + this.id = o.id; + super.build(o); + } +} diff --git a/src/models/export/field.ts b/src/models/export/field.ts new file mode 100644 index 0000000000..3e07c65eed --- /dev/null +++ b/src/models/export/field.ts @@ -0,0 +1,34 @@ +import { FieldType } from '../../enums/fieldType'; + +import { FieldView } from '../view/fieldView'; + +export class Field { + static template(): Field { + const req = new Field(); + req.name = 'Field name'; + req.value = 'Some value'; + req.type = FieldType.Text; + return req; + } + + static toView(req: Field, view = new FieldView()) { + view.type = req.type; + view.value = req.value; + view.name = req.name; + return view; + } + + name: string; + value: string; + type: FieldType; + + constructor(o?: FieldView) { + if (o == null) { + return; + } + + this.name = o.name; + this.value = o.value; + this.type = o.type; + } +} diff --git a/src/models/export/folder.ts b/src/models/export/folder.ts new file mode 100644 index 0000000000..8c2acf093a --- /dev/null +++ b/src/models/export/folder.ts @@ -0,0 +1,21 @@ +import { FolderView } from '../view/folderView'; + +export class Folder { + static template(): Folder { + const req = new Folder(); + req.name = 'Folder name'; + return req; + } + + static toView(req: Folder, view = new FolderView()) { + view.name = req.name; + return view; + } + + name: string; + + // Use build method instead of ctor so that we can control order of JSON stringify for pretty print + build(o: FolderView) { + this.name = o.name; + } +} diff --git a/src/models/export/folderWithId.ts b/src/models/export/folderWithId.ts new file mode 100644 index 0000000000..775376d203 --- /dev/null +++ b/src/models/export/folderWithId.ts @@ -0,0 +1,13 @@ +import { Folder } from './folder'; + +import { FolderView } from '../view/folderView'; + +export class FolderWithId extends Folder { + id: string; + + // Use build method instead of ctor so that we can control order of JSON stringify for pretty print + build(o: FolderView) { + this.id = o.id; + super.build(o); + } +} diff --git a/src/models/export/identity.ts b/src/models/export/identity.ts new file mode 100644 index 0000000000..a1aae09850 --- /dev/null +++ b/src/models/export/identity.ts @@ -0,0 +1,92 @@ +import { IdentityView } from '../view/identityView'; + +export class Identity { + static template(): Identity { + const req = new Identity(); + req.title = 'Mr'; + req.firstName = 'John'; + req.middleName = 'William'; + req.lastName = 'Doe'; + req.address1 = '123 Any St'; + req.address2 = 'Apt #123'; + req.address3 = null; + req.city = 'New York'; + req.state = 'NY'; + req.postalCode = '10001'; + req.country = 'US'; + req.company = 'Acme Inc.'; + req.email = 'john@company.com'; + req.phone = '5555551234'; + req.ssn = '000-123-4567'; + req.username = 'jdoe'; + req.passportNumber = 'US-123456789'; + req.licenseNumber = 'D123-12-123-12333'; + return req; + } + + static toView(req: Identity, view = new IdentityView()) { + view.title = req.title; + view.firstName = req.firstName; + view.middleName = req.middleName; + view.lastName = req.lastName; + view.address1 = req.address1; + view.address2 = req.address2; + view.address3 = req.address3; + view.city = req.city; + view.state = req.state; + view.postalCode = req.postalCode; + view.country = req.country; + view.company = req.company; + view.email = req.email; + view.phone = req.phone; + view.ssn = req.ssn; + view.username = req.username; + view.passportNumber = req.passportNumber; + view.licenseNumber = req.licenseNumber; + return view; + } + + 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(o?: IdentityView) { + if (o == null) { + return; + } + + this.title = o.title; + this.firstName = o.firstName; + this.middleName = o.middleName; + this.lastName = o.lastName; + this.address1 = o.address1; + this.address2 = o.address2; + this.address3 = o.address3; + this.city = o.city; + this.state = o.state; + this.postalCode = o.postalCode; + this.country = o.country; + this.company = o.company; + this.email = o.email; + this.phone = o.phone; + this.ssn = o.ssn; + this.username = o.username; + this.passportNumber = o.passportNumber; + this.licenseNumber = o.licenseNumber; + } +} diff --git a/src/models/export/login.ts b/src/models/export/login.ts new file mode 100644 index 0000000000..b5401bc939 --- /dev/null +++ b/src/models/export/login.ts @@ -0,0 +1,43 @@ +import { LoginUri } from './loginUri'; + +import { LoginView } from '../view/loginView'; + +export class Login { + static template(): Login { + const req = new Login(); + req.uris = []; + req.username = 'jdoe'; + req.password = 'myp@ssword123'; + req.totp = 'JBSWY3DPEHPK3PXP'; + return req; + } + + static toView(req: Login, view = new LoginView()) { + if (req.uris != null) { + view.uris = req.uris.map((u) => LoginUri.toView(u)); + } + view.username = req.username; + view.password = req.password; + view.totp = req.totp; + return view; + } + + uris: LoginUri[]; + username: string; + password: string; + totp: string; + + constructor(o?: LoginView) { + if (o == null) { + return; + } + + if (o.uris != null) { + this.uris = o.uris.map((u) => new LoginUri(u)); + } + + this.username = o.username; + this.password = o.password; + this.totp = o.totp; + } +} diff --git a/src/models/export/loginUri.ts b/src/models/export/loginUri.ts new file mode 100644 index 0000000000..94c3bbd032 --- /dev/null +++ b/src/models/export/loginUri.ts @@ -0,0 +1,30 @@ +import { UriMatchType } from '../../enums/uriMatchType'; + +import { LoginUriView } from '../view/loginUriView'; + +export class LoginUri { + static template(): LoginUri { + const req = new LoginUri(); + req.uri = 'https://google.com'; + req.match = null; + return req; + } + + static toView(req: LoginUri, view = new LoginUriView()) { + view.uri = req.uri; + view.match = req.match; + return view; + } + + uri: string; + match: UriMatchType = null; + + constructor(o?: LoginUriView) { + if (o == null) { + return; + } + + this.uri = o.uri; + this.match = o.match; + } +} diff --git a/src/models/export/secureNote.ts b/src/models/export/secureNote.ts new file mode 100644 index 0000000000..c2115fc337 --- /dev/null +++ b/src/models/export/secureNote.ts @@ -0,0 +1,26 @@ +import { SecureNoteType } from '../../enums/secureNoteType'; + +import { SecureNoteView } from '../view/secureNoteView'; + +export class SecureNote { + static template(): SecureNote { + const req = new SecureNote(); + req.type = SecureNoteType.Generic; + return req; + } + + static toView(req: SecureNote, view = new SecureNoteView()) { + view.type = req.type; + return view; + } + + type: SecureNoteType; + + constructor(o?: SecureNoteView) { + if (o == null) { + return; + } + + this.type = o.type; + } +} From e7b5868aadf8412f49bfaba378d05c692124e265 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 17 Dec 2018 10:32:02 -0500 Subject: [PATCH 0662/1626] export to json --- src/abstractions/export.service.ts | 2 +- src/services/export.service.ts | 139 +++++++++++++++++++---------- 2 files changed, 91 insertions(+), 50 deletions(-) diff --git a/src/abstractions/export.service.ts b/src/abstractions/export.service.ts index 7f9f27234b..a6b719e8b3 100644 --- a/src/abstractions/export.service.ts +++ b/src/abstractions/export.service.ts @@ -1,5 +1,5 @@ export abstract class ExportService { getExport: (format?: 'csv' | 'json') => Promise; getOrganizationExport: (organizationId: string, format?: 'csv' | 'json') => Promise; - getFileName: (prefix?: string) => string; + getFileName: (prefix?: string, extension?: string) => string; } diff --git a/src/services/export.service.ts b/src/services/export.service.ts index 68f5a7e1d8..a105be6d8b 100644 --- a/src/services/export.service.ts +++ b/src/services/export.service.ts @@ -18,6 +18,10 @@ import { CipherData } from '../models/data/cipherData'; import { CollectionData } from '../models/data/collectionData'; import { CollectionDetailsResponse } from '../models/response/collectionResponse'; +import { CipherWithIds as CipherExport } from '../models/export/cipherWithIds'; +import { CollectionWithId as CollectionExport } from '../models/export/collectionWithId'; +import { FolderWithId as FolderExport } from '../models/export/folderWithId'; + export class ExportService implements ExportServiceAbstraction { constructor(private folderService: FolderService, private cipherService: CipherService, private apiService: ApiService) { } @@ -37,33 +41,54 @@ export class ExportService implements ExportServiceAbstraction { await Promise.all(promises); - const foldersMap = new Map(); - decFolders.forEach((f) => { - foldersMap.set(f.id, f); - }); - - const exportCiphers: any[] = []; - decCiphers.forEach((c) => { - // only export logins and secure notes - if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) { - return; - } - - if (c.organizationId != null) { - return; - } - - const cipher: any = {}; - cipher.folder = c.folderId != null && foldersMap.has(c.folderId) ? foldersMap.get(c.folderId).name : null; - cipher.favorite = c.favorite ? 1 : null; - this.buildCommonCipher(cipher, c); - exportCiphers.push(cipher); - }); - if (format === 'csv') { + const foldersMap = new Map(); + decFolders.forEach((f) => { + foldersMap.set(f.id, f); + }); + + const exportCiphers: any[] = []; + decCiphers.forEach((c) => { + // only export logins and secure notes + if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) { + return; + } + if (c.organizationId != null) { + return; + } + + const cipher: any = {}; + cipher.folder = c.folderId != null && foldersMap.has(c.folderId) ? + foldersMap.get(c.folderId).name : null; + cipher.favorite = c.favorite ? 1 : null; + this.buildCommonCipher(cipher, c); + exportCiphers.push(cipher); + }); + return papa.unparse(exportCiphers); } else { - return JSON.stringify(exportCiphers, null, ' '); + const jsonDoc: any = { + folders: [], + items: [], + }; + + decFolders.forEach((f) => { + const folder = new FolderExport(); + folder.build(f); + jsonDoc.folders.push(folder); + }); + + decCiphers.forEach((c) => { + if (c.organizationId != null) { + return; + } + const cipher = new CipherExport(); + cipher.build(c); + cipher.collectionIds = null; + jsonDoc.items.push(cipher); + }); + + return JSON.stringify(jsonDoc, null, ' '); } } @@ -100,43 +125,59 @@ export class ExportService implements ExportServiceAbstraction { await Promise.all(promises); - const collectionsMap = new Map(); - decCollections.forEach((c) => { - collectionsMap.set(c.id, c); - }); - - const exportCiphers: any[] = []; - decCiphers.forEach((c) => { - // only export logins and secure notes - if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) { - return; - } - - const cipher: any = {}; - cipher.collections = []; - if (c.collectionIds != null) { - cipher.collections = c.collectionIds.filter((id) => collectionsMap.has(id)) - .map((id) => collectionsMap.get(id).name); - } - this.buildCommonCipher(cipher, c); - exportCiphers.push(cipher); - }); - if (format === 'csv') { + const collectionsMap = new Map(); + decCollections.forEach((c) => { + collectionsMap.set(c.id, c); + }); + + const exportCiphers: any[] = []; + decCiphers.forEach((c) => { + // only export logins and secure notes + if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) { + return; + } + + const cipher: any = {}; + cipher.collections = []; + if (c.collectionIds != null) { + cipher.collections = c.collectionIds.filter((id) => collectionsMap.has(id)) + .map((id) => collectionsMap.get(id).name); + } + this.buildCommonCipher(cipher, c); + exportCiphers.push(cipher); + }); + return papa.unparse(exportCiphers); } else { - return JSON.stringify(exportCiphers, null, ' '); + const jsonDoc: any = { + collections: [], + items: [], + }; + + decCollections.forEach((c) => { + const collection = new CollectionExport(); + collection.build(c); + jsonDoc.collections.push(collection); + }); + + decCiphers.forEach((c) => { + const cipher = new CipherExport(); + cipher.build(c); + jsonDoc.items.push(cipher); + }); + return JSON.stringify({}, null, ' '); } } - getFileName(prefix: string = null): string { + getFileName(prefix: string = null, extension: string = 'csv'): string { const now = new Date(); const dateString = now.getFullYear() + '' + this.padNumber(now.getMonth() + 1, 2) + '' + this.padNumber(now.getDate(), 2) + this.padNumber(now.getHours(), 2) + '' + this.padNumber(now.getMinutes(), 2) + this.padNumber(now.getSeconds(), 2); - return 'bitwarden' + (prefix ? ('_' + prefix) : '') + '_export_' + dateString + '.csv'; + return 'bitwarden' + (prefix ? ('_' + prefix) : '') + '_export_' + dateString + '.' + extension; } private padNumber(num: number, width: number, padCharacter: string = '0'): string { From 3b22df15e8cb2f14c2b4dfe996af9bbb721902d0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 17 Dec 2018 10:54:03 -0500 Subject: [PATCH 0663/1626] fix for org export --- src/angular/components/export.component.ts | 5 +++-- src/services/export.service.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/angular/components/export.component.ts b/src/angular/components/export.component.ts index d4192fed26..f85d4ef018 100644 --- a/src/angular/components/export.component.ts +++ b/src/angular/components/export.component.ts @@ -13,6 +13,7 @@ export class ExportComponent { formPromise: Promise; masterPassword: string; + format: 'json' | 'csv' = 'json'; showPassword = false; constructor(protected cryptoService: CryptoService, protected i18nService: I18nService, @@ -53,11 +54,11 @@ export class ExportComponent { } protected getExportData() { - return this.exportService.getExport('csv'); + return this.exportService.getExport(this.format); } protected getFileName(prefix?: string) { - return this.exportService.getFileName(prefix); + return this.exportService.getFileName(prefix, this.format); } private downloadFile(csv: string): void { diff --git a/src/services/export.service.ts b/src/services/export.service.ts index a105be6d8b..6492725494 100644 --- a/src/services/export.service.ts +++ b/src/services/export.service.ts @@ -166,7 +166,7 @@ export class ExportService implements ExportServiceAbstraction { cipher.build(c); jsonDoc.items.push(cipher); }); - return JSON.stringify({}, null, ' '); + return JSON.stringify(jsonDoc, null, ' '); } } From 4d57f44a6944cf4037802388dfdab02dc75b7c37 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 17 Dec 2018 13:21:06 -0500 Subject: [PATCH 0664/1626] bitwarden json importer --- src/importers/bitwardenJsonImporter.ts | 66 ++++++++++++++++++++++++++ src/services/export.service.ts | 3 ++ src/services/import.service.ts | 4 ++ 3 files changed, 73 insertions(+) create mode 100644 src/importers/bitwardenJsonImporter.ts diff --git a/src/importers/bitwardenJsonImporter.ts b/src/importers/bitwardenJsonImporter.ts new file mode 100644 index 0000000000..e4c376940e --- /dev/null +++ b/src/importers/bitwardenJsonImporter.ts @@ -0,0 +1,66 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CipherWithIds } from '../models/export/cipherWithIds'; +import { CollectionWithId } from '../models/export/collectionWithId'; +import { FolderWithId } from '../models/export/folderWithId'; + +export class BitwardenJsonImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = JSON.parse(data); + if (results == null || results.items == null || results.items.length === 0) { + result.success = false; + return result; + } + + const groupingsMap = new Map(); + if (this.organization && results.collections != null) { + results.collections.forEach((c: CollectionWithId) => { + const collection = CollectionWithId.toView(c); + if (collection != null) { + collection.id = null; + collection.organizationId = null; + groupingsMap.set(c.id, result.collections.length); + result.collections.push(collection); + } + }); + } else if (!this.organization && results.folders != null) { + results.folders.forEach((f: FolderWithId) => { + const folder = FolderWithId.toView(f); + if (folder != null) { + folder.id = null; + groupingsMap.set(f.id, result.folders.length); + result.folders.push(folder); + } + }); + } + + results.items.forEach((c: CipherWithIds) => { + const cipher = CipherWithIds.toView(c); + // reset ids incase they were set for some reason + cipher.id = null; + cipher.folderId = null; + cipher.organizationId = null; + cipher.collectionIds = null; + + if (!this.organization && c.folderId != null && groupingsMap.has(c.folderId)) { + result.folderRelationships.push([result.ciphers.length, groupingsMap.get(c.folderId)]); + } else if (this.organization && c.collectionIds != null) { + c.collectionIds.forEach((cId) => { + if (groupingsMap.has(cId)) { + result.collectionRelationships.push([result.ciphers.length, groupingsMap.get(cId)]); + } + }); + } + + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} diff --git a/src/services/export.service.ts b/src/services/export.service.ts index 6492725494..7c5ef3cc83 100644 --- a/src/services/export.service.ts +++ b/src/services/export.service.ts @@ -73,6 +73,9 @@ export class ExportService implements ExportServiceAbstraction { }; decFolders.forEach((f) => { + if (f.id == null) { + return; + } const folder = new FolderExport(); folder.build(f); jsonDoc.folders.push(folder); diff --git a/src/services/import.service.ts b/src/services/import.service.ts index 3e53753ccd..ca32707987 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -22,6 +22,7 @@ import { CipherView } from '../models/view/cipherView'; import { AscendoCsvImporter } from '../importers/ascendoCsvImporter'; import { AviraCsvImporter } from '../importers/aviraCsvImporter'; import { BitwardenCsvImporter } from '../importers/bitwardenCsvImporter'; +import { BitwardenJsonImporter } from '../importers/bitwardenJsonImporter'; import { BlurCsvImporter } from '../importers/blurCsvImporter'; import { ChromeCsvImporter } from '../importers/chromeCsvImporter'; import { ClipperzHtmlImporter } from '../importers/clipperzHtmlImporter'; @@ -58,6 +59,7 @@ import { ZohoVaultCsvImporter } from '../importers/zohoVaultCsvImporter'; export class ImportService implements ImportServiceAbstraction { featuredImportOptions = [ + { id: 'bitwardenjson', name: 'Bitwarden (json)' }, { id: 'bitwardencsv', name: 'Bitwarden (csv)' }, { id: 'lastpasscsv', name: 'LastPass (csv)' }, { id: 'chromecsv', name: 'Chrome (csv)' }, @@ -148,6 +150,8 @@ export class ImportService implements ImportServiceAbstraction { switch (format) { case 'bitwardencsv': return new BitwardenCsvImporter(); + case 'bitwardenjson': + return new BitwardenJsonImporter(); case 'lastpasscsv': case 'passboltcsv': return new LastPassCsvImporter(); From aa1784932945bfd1115f366cf5dafc6b481c19a3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 18 Dec 2018 17:00:07 -0500 Subject: [PATCH 0665/1626] install and use duo_web_sdk w/ npm --- package-lock.json | 4 + package.json | 1 + .../components/two-factor.component.ts | 2 +- src/misc/duo.js | 430 ------------------ 4 files changed, 6 insertions(+), 431 deletions(-) delete mode 100644 src/misc/duo.js diff --git a/package-lock.json b/package-lock.json index 2daa4b22d1..7bc2c4b6f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1957,6 +1957,10 @@ "is-obj": "^1.0.0" } }, + "duo_web_sdk": { + "version": "git+https://github.com/duosecurity/duo_web_sdk.git#410a9186cc34663c4913b17d6528067cd3331f1d", + "from": "git+https://github.com/duosecurity/duo_web_sdk.git" + }, "duplexer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", diff --git a/package.json b/package.json index d1a822950c..e8253446e0 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "@aspnet/signalr-protocol-msgpack": "1.0.4", "big-integer": "1.6.36", "core-js": "2.5.7", + "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", "electron-log": "2.2.14", "electron-updater": "3.0.3", "form-data": "2.3.2", diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 99342b8e9a..e7e41f2f39 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -17,7 +17,7 @@ import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { TwoFactorProviders } from '../../services/auth.service'; -import * as DuoWebSDK from '../../misc/duo'; +import * as DuoWebSDK from 'duo_web_sdk'; import { U2f } from '../../misc/u2f'; export class TwoFactorComponent implements OnInit, OnDestroy { diff --git a/src/misc/duo.js b/src/misc/duo.js deleted file mode 100644 index bb0f58a787..0000000000 --- a/src/misc/duo.js +++ /dev/null @@ -1,430 +0,0 @@ -/** - * Duo Web SDK v2 - * Copyright 2017, Duo Security - */ - -(function (root, factory) { - /*eslint-disable */ - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define([], factory); - /*eslint-enable */ - } else if (typeof module === 'object' && module.exports) { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(); - } else { - // Browser globals (root is window) - var Duo = factory(); - // If the Javascript was loaded via a script tag, attempt to autoload - // the frame. - Duo._onReady(Duo.init); - - // Attach Duo to the `window` object - root.Duo = Duo; - } -}(this, function() { - var DUO_MESSAGE_FORMAT = /^(?:AUTH|ENROLL)+\|[A-Za-z0-9\+\/=]+\|[A-Za-z0-9\+\/=]+$/; - var DUO_ERROR_FORMAT = /^ERR\|[\w\s\.\(\)]+$/; - var DUO_OPEN_WINDOW_FORMAT = /^DUO_OPEN_WINDOW\|/; - var VALID_OPEN_WINDOW_DOMAINS = [ - 'duo.com', - 'duosecurity.com', - 'duomobile.s3-us-west-1.amazonaws.com' - ]; - - var iframeId = 'duo_iframe', - postAction = '', - postArgument = 'sig_response', - host, - sigRequest, - duoSig, - appSig, - iframe, - submitCallback; - - function throwError(message, url) { - throw new Error( - 'Duo Web SDK error: ' + message + - (url ? ('\n' + 'See ' + url + ' for more information') : '') - ); - } - - function hyphenize(str) { - return str.replace(/([a-z])([A-Z])/, '$1-$2').toLowerCase(); - } - - // cross-browser data attributes - function getDataAttribute(element, name) { - if ('dataset' in element) { - return element.dataset[name]; - } else { - return element.getAttribute('data-' + hyphenize(name)); - } - } - - // cross-browser event binding/unbinding - function on(context, event, fallbackEvent, callback) { - if ('addEventListener' in window) { - context.addEventListener(event, callback, false); - } else { - context.attachEvent(fallbackEvent, callback); - } - } - - function off(context, event, fallbackEvent, callback) { - if ('removeEventListener' in window) { - context.removeEventListener(event, callback, false); - } else { - context.detachEvent(fallbackEvent, callback); - } - } - - function onReady(callback) { - on(document, 'DOMContentLoaded', 'onreadystatechange', callback); - } - - function offReady(callback) { - off(document, 'DOMContentLoaded', 'onreadystatechange', callback); - } - - function onMessage(callback) { - on(window, 'message', 'onmessage', callback); - } - - function offMessage(callback) { - off(window, 'message', 'onmessage', callback); - } - - /** - * Parse the sig_request parameter, throwing errors if the token contains - * a server error or if the token is invalid. - * - * @param {String} sig Request token - */ - function parseSigRequest(sig) { - if (!sig) { - // nothing to do - return; - } - - // see if the token contains an error, throwing it if it does - if (sig.indexOf('ERR|') === 0) { - throwError(sig.split('|')[1]); - } - - // validate the token - if (sig.indexOf(':') === -1 || sig.split(':').length !== 2) { - throwError( - 'Duo was given a bad token. This might indicate a configuration ' + - 'problem with one of Duo\'s client libraries.', - 'https://www.duosecurity.com/docs/duoweb#first-steps' - ); - } - - var sigParts = sig.split(':'); - - // hang on to the token, and the parsed duo and app sigs - sigRequest = sig; - duoSig = sigParts[0]; - appSig = sigParts[1]; - - return { - sigRequest: sig, - duoSig: sigParts[0], - appSig: sigParts[1] - }; - } - - /** - * This function is set up to run when the DOM is ready, if the iframe was - * not available during `init`. - */ - function onDOMReady() { - iframe = document.getElementById(iframeId); - - if (!iframe) { - throw new Error( - 'This page does not contain an iframe for Duo to use.' + - 'Add an element like ' + - 'to this page. ' + - 'See https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe ' + - 'for more information.' - ); - } - - // we've got an iframe, away we go! - ready(); - - // always clean up after yourself - offReady(onDOMReady); - } - - /** - * Validate that a MessageEvent came from the Duo service, and that it - * is a properly formatted payload. - * - * The Google Chrome sign-in page injects some JS into pages that also - * make use of postMessage, so we need to do additional validation above - * and beyond the origin. - * - * @param {MessageEvent} event Message received via postMessage - */ - function isDuoMessage(event) { - return Boolean( - event.origin === ('https://' + host) && - typeof event.data === 'string' && - ( - event.data.match(DUO_MESSAGE_FORMAT) || - event.data.match(DUO_ERROR_FORMAT) || - event.data.match(DUO_OPEN_WINDOW_FORMAT) - ) - ); - } - - /** - * Validate the request token and prepare for the iframe to become ready. - * - * All options below can be passed into an options hash to `Duo.init`, or - * specified on the iframe using `data-` attributes. - * - * Options specified using the options hash will take precedence over - * `data-` attributes. - * - * Example using options hash: - * ```javascript - * Duo.init({ - * iframe: "some_other_id", - * host: "api-main.duo.test", - * sig_request: "...", - * post_action: "/auth", - * post_argument: "resp" - * }); - * ``` - * - * Example using `data-` attributes: - * ``` - * - * ``` - * - * @param {Object} options - * @param {String} options.iframe The iframe, or id of an iframe to set up - * @param {String} options.host Hostname - * @param {String} options.sig_request Request token - * @param {String} [options.post_action=''] URL to POST back to after successful auth - * @param {String} [options.post_argument='sig_response'] Parameter name to use for response token - * @param {Function} [options.submit_callback] If provided, duo will not submit the form instead execute - * the callback function with reference to the "duo_form" form object - * submit_callback can be used to prevent the webpage from reloading. - */ - function init(options) { - if (options) { - if (options.host) { - host = options.host; - } - - if (options.sig_request) { - parseSigRequest(options.sig_request); - } - - if (options.post_action) { - postAction = options.post_action; - } - - if (options.post_argument) { - postArgument = options.post_argument; - } - - if (options.iframe) { - if (options.iframe.tagName) { - iframe = options.iframe; - } else if (typeof options.iframe === 'string') { - iframeId = options.iframe; - } - } - - if (typeof options.submit_callback === 'function') { - submitCallback = options.submit_callback; - } - } - - // if we were given an iframe, no need to wait for the rest of the DOM - if (false && iframe) { - ready(); - } else { - // try to find the iframe in the DOM - iframe = document.getElementById(iframeId); - - // iframe is in the DOM, away we go! - if (iframe) { - ready(); - } else { - // wait until the DOM is ready, then try again - onReady(onDOMReady); - } - } - - // always clean up after yourself! - offReady(init); - } - - /** - * This function is called when a message was received from another domain - * using the `postMessage` API. Check that the event came from the Duo - * service domain, and that the message is a properly formatted payload, - * then perform the post back to the primary service. - * - * @param event Event object (contains origin and data) - */ - function onReceivedMessage(event) { - if (isDuoMessage(event)) { - if (event.data.match(DUO_OPEN_WINDOW_FORMAT)) { - var url = event.data.substring("DUO_OPEN_WINDOW|".length); - if (isValidUrlToOpen(url)) { - // Open the URL that comes after the DUO_WINDOW_OPEN token. - window.open(url, "_self"); - } - } - else { - // the event came from duo, do the post back - doPostBack(event.data); - - // always clean up after yourself! - offMessage(onReceivedMessage); - } - } - } - - /** - * Validate that this passed in URL is one that we will actually allow to - * be opened. - * @param url String URL that the message poster wants to open - * @returns {boolean} true if we allow this url to be opened in the window - */ - function isValidUrlToOpen(url) { - if (!url) { - return false; - } - - var parser = document.createElement('a'); - parser.href = url; - - if (parser.protocol === "duotrustedendpoints:") { - return true; - } else if (parser.protocol !== "https:") { - return false; - } - - for (var i = 0; i < VALID_OPEN_WINDOW_DOMAINS.length; i++) { - if (parser.hostname.endsWith("." + VALID_OPEN_WINDOW_DOMAINS[i]) || - parser.hostname === VALID_OPEN_WINDOW_DOMAINS[i]) { - return true; - } - } - return false; - } - - /** - * Point the iframe at Duo, then wait for it to postMessage back to us. - */ - function ready() { - if (!host) { - host = getDataAttribute(iframe, 'host'); - - if (!host) { - throwError( - 'No API hostname is given for Duo to use. Be sure to pass ' + - 'a `host` parameter to Duo.init, or through the `data-host` ' + - 'attribute on the iframe element.', - 'https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe' - ); - } - } - - if (!duoSig || !appSig) { - parseSigRequest(getDataAttribute(iframe, 'sigRequest')); - - if (!duoSig || !appSig) { - throwError( - 'No valid signed request is given. Be sure to give the ' + - '`sig_request` parameter to Duo.init, or use the ' + - '`data-sig-request` attribute on the iframe element.', - 'https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe' - ); - } - } - - // if postAction/Argument are defaults, see if they are specified - // as data attributes on the iframe - if (postAction === '') { - postAction = getDataAttribute(iframe, 'postAction') || postAction; - } - - if (postArgument === 'sig_response') { - postArgument = getDataAttribute(iframe, 'postArgument') || postArgument; - } - - // point the iframe at Duo - iframe.src = [ - 'https://', host, '/frame/web/v1/auth?tx=', duoSig, - '&parent=', encodeURIComponent(document.location.href), - '&v=2.6' - ].join(''); - - // listen for the 'message' event - onMessage(onReceivedMessage); - } - - /** - * We received a postMessage from Duo. POST back to the primary service - * with the response token, and any additional user-supplied parameters - * given in form#duo_form. - */ - function doPostBack(response) { - // create a hidden input to contain the response token - var input = document.createElement('input'); - input.type = 'hidden'; - input.name = postArgument; - input.value = response + ':' + appSig; - - // user may supply their own form with additional inputs - var form = document.getElementById('duo_form'); - - // if the form doesn't exist, create one - if (!form) { - form = document.createElement('form'); - - // insert the new form after the iframe - iframe.parentElement.insertBefore(form, iframe.nextSibling); - } - - // make sure we are actually posting to the right place - form.method = 'POST'; - form.action = postAction; - - // add the response token input to the form - form.appendChild(input); - - // away we go! - if (typeof submitCallback === "function") { - submitCallback.call(null, form); - } else { - form.submit(); - } - } - - return { - init: init, - _onReady: onReady, - _parseSigRequest: parseSigRequest, - _isDuoMessage: isDuoMessage, - _doPostBack: doPostBack - }; -})); From cddeeefdbb14d5f70020fb705885eb05a0bb4339 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 19 Dec 2018 11:29:44 -0500 Subject: [PATCH 0666/1626] twoFactorEnabled on org user details --- src/models/response/organizationUserResponse.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/models/response/organizationUserResponse.ts b/src/models/response/organizationUserResponse.ts index 7f377b5e95..144bcf9539 100644 --- a/src/models/response/organizationUserResponse.ts +++ b/src/models/response/organizationUserResponse.ts @@ -21,11 +21,13 @@ export class OrganizationUserResponse { export class OrganizationUserUserDetailsResponse extends OrganizationUserResponse { name: string; email: string; + twoFactorEnabled: string; constructor(response: any) { super(response); this.name = response.Name; this.email = response.Email; + this.twoFactorEnabled = response.TwoFactorEnabled; } } From 7a3462afdabaab0b15b1a41b9b173d5efd23bb4a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 26 Dec 2018 09:41:46 -0500 Subject: [PATCH 0667/1626] string typeof check --- src/importers/baseImporter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index b70a566a59..76069e261b 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -155,7 +155,7 @@ export abstract class BaseImporter { } protected isNullOrWhitespace(str: string): boolean { - return str == null || str.trim() === ''; + return str == null || typeof str !== 'string' || str.trim() === ''; } protected getValueOrDefault(str: string, defaultValue: string = null): string { From 58ed2ed0a2f7609f1333af22cbb5ede939d1ac78 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 26 Dec 2018 14:55:21 -0500 Subject: [PATCH 0668/1626] update electron --- package-lock.json | 187 ++++++++++++++++-------------------- package.json | 4 +- src/electron/window.main.ts | 23 ++--- 3 files changed, 96 insertions(+), 118 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7bc2c4b6f1..137b1e0dbc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -213,15 +213,15 @@ "dev": true }, "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz", + "integrity": "sha512-FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g==", "dev": true, "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", + "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, "align-text": { @@ -760,7 +760,6 @@ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", "dev": true, - "optional": true, "requires": { "tweetnacl": "^0.14.3" } @@ -1375,12 +1374,6 @@ "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", "dev": true }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -1978,7 +1971,6 @@ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", "dev": true, - "optional": true, "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -1997,71 +1989,57 @@ "dev": true }, "electron": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/electron/-/electron-2.0.11.tgz", - "integrity": "sha512-bFTMDQN3epfiymqTPdgffyTxuy/7A52sIkW7Hos+hY5XLPArOXLXAKx1JtB3dM7CcPfZa+5qp/J3cPCidh5WXg==", + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/electron/-/electron-3.0.13.tgz", + "integrity": "sha512-tfx5jFgXhCmpe6oPjcesaRj7geHqQxrJdbpseanRzL9BbyYUtsj0HoxwPAUvCx4+52P6XryBwWTvne/1eBVf9Q==", "dev": true, "requires": { "@types/node": "^8.0.24", - "electron-download": "^3.0.1", + "electron-download": "^4.1.0", "extract-zip": "^1.0.3" }, "dependencies": { "@types/node": { - "version": "8.10.36", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.36.tgz", - "integrity": "sha512-SL6KhfM7PTqiFmbCW3eVNwVBZ+88Mrzbuvn9olPsfv43mbiWaFY+nRcz/TGGku0/lc2FepdMbImdMY1JrQ+zbw==", + "version": "8.10.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.39.tgz", + "integrity": "sha512-rE7fktr02J8ybFf6eysife+WF+L4sAHWzw09DgdCebEu+qDwMvv4zl6Bc+825ttGZP73kCKxa3dhJOoGJ8+5mA==", "dev": true } } }, "electron-download": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-3.3.0.tgz", - "integrity": "sha1-LP1U1pZsAZxNSa1l++Zcyc3vaMg=", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-4.1.1.tgz", + "integrity": "sha512-FjEWG9Jb/ppK/2zToP+U5dds114fM1ZOJqMAR4aXXL5CvyPE9fiqBK/9YcwC9poIFQTEJk/EM/zyRwziziRZrg==", "dev": true, "requires": { - "debug": "^2.2.0", - "fs-extra": "^0.30.0", - "home-path": "^1.0.1", + "debug": "^3.0.0", + "env-paths": "^1.0.0", + "fs-extra": "^4.0.1", "minimist": "^1.2.0", - "nugget": "^2.0.0", - "path-exists": "^2.1.0", - "rc": "^1.1.2", - "semver": "^5.3.0", - "sumchecker": "^1.2.0" + "nugget": "^2.0.1", + "path-exists": "^3.0.0", + "rc": "^1.2.1", + "semver": "^5.4.1", + "sumchecker": "^2.0.2" }, "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, "fs-extra": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", - "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", "dev": true, "requires": { "graceful-fs": "^4.1.2", - "jsonfile": "^2.1.0", - "klaw": "^1.0.0", - "path-is-absolute": "^1.0.0", - "rimraf": "^2.2.8" + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" } }, - "jsonfile": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true } } }, @@ -2071,9 +2049,9 @@ "integrity": "sha1-FOb9pcaOnk7L7/nM8DfL18BcWv4=" }, "electron-log": { - "version": "2.2.14", - "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.14.tgz", - "integrity": "sha512-Rj+XyK4nShe/nv9v1Uks4KEfjtQ6N+eSnx5CLpAjG6rlyUdAflyFHoybcHSLoq9l9pGavclULWS5IXgk8umc2g==" + "version": "2.2.17", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.17.tgz", + "integrity": "sha512-v+Af5W5z99ehhaLOfE9eTSXUwjzh2wFlQjz51dvkZ6ZIrET6OB/zAZPvsuwT6tm3t5x+M1r+Ed3U3xtPZYAyuQ==" }, "electron-updater": { "version": "3.0.3", @@ -2172,6 +2150,12 @@ "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", "dev": true }, + "env-paths": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz", + "integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=", + "dev": true + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2190,12 +2174,6 @@ "stackframe": "^1.0.4" } }, - "es6-promise": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.5.tgz", - "integrity": "sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg==", - "dev": true - }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2533,9 +2511,9 @@ "dev": true }, "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", "dev": true }, "fast-json-stable-stringify": { @@ -3412,12 +3390,12 @@ "dev": true }, "har-validator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", - "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", "dev": true, "requires": { - "ajv": "^5.3.0", + "ajv": "^6.5.5", "har-schema": "^2.0.0" } }, @@ -3563,12 +3541,6 @@ "minimalistic-crypto-utils": "^1.0.1" } }, - "home-path": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/home-path/-/home-path-1.0.6.tgz", - "integrity": "sha512-wo+yjrdAtoXt43Vy92a+0IPCYViiyLAHyp0QVS4xL/tfvVz5sXIW1ubLZk3nhVkD92fQpUMKX+fzMjr5F489vw==", - "dev": true - }, "hosted-git-info": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", @@ -4170,8 +4142,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true, - "optional": true + "dev": true }, "jsesc": { "version": "1.3.0", @@ -4186,9 +4157,9 @@ "dev": true }, "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, "json-stringify-safe": { @@ -4441,15 +4412,6 @@ "is-buffer": "^1.1.5" } }, - "klaw": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", - "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.9" - } - }, "latest-version": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", @@ -5926,9 +5888,9 @@ "dev": true }, "psl": { - "version": "1.1.29", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", - "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==", + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==", "dev": true }, "pstree.remy": { @@ -6780,9 +6742,9 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "sshpk": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", - "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.0.tgz", + "integrity": "sha512-Zhev35/y7hRMcID/upReIvRse+I9SVhyVre/KTJSJQWMz3C3+G+HpO7m1wK/yckEtujKZ7dS4hkVxAnmHaIGVQ==", "dev": true, "requires": { "asn1": "~0.2.3", @@ -6969,13 +6931,12 @@ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "sumchecker": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-1.3.1.tgz", - "integrity": "sha1-ebs7RFbdBPGOvbwNcDodHa7FEF0=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-2.0.2.tgz", + "integrity": "sha1-D0LBDl0F2l1C7qPlbDOZo31sWz4=", "dev": true, "requires": { - "debug": "^2.2.0", - "es6-promise": "^4.0.5" + "debug": "^2.2.0" }, "dependencies": { "debug": { @@ -7361,8 +7322,7 @@ "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true, - "optional": true + "dev": true }, "type-check": { "version": "0.3.2", @@ -7634,6 +7594,23 @@ } } }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + } + } + }, "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", diff --git a/package.json b/package.json index e8253446e0..63a26a531b 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@types/webcrypto": "0.0.28", "@types/zxcvbn": "^4.4.0", "concurrently": "3.5.1", - "electron": "2.0.11", + "electron": "3.0.13", "jasmine": "^3.2.0", "jasmine-core": "^3.2.1", "jasmine-spec-reporter": "^4.2.1", @@ -74,7 +74,7 @@ "big-integer": "1.6.36", "core-js": "2.5.7", "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", - "electron-log": "2.2.14", + "electron-log": "2.2.17", "electron-updater": "3.0.3", "form-data": "2.3.2", "keytar": "4.2.1", diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index 33f7baddd8..db1d7c0e11 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -24,19 +24,20 @@ export class WindowMain { return new Promise((resolve, reject) => { try { if (!isMacAppStore() && !isSnapStore()) { - const shouldQuit = app.makeSingleInstance((args, dir) => { - // Someone tried to run a second instance, we should focus our window. - if (this.win != null) { - if (this.win.isMinimized()) { - this.win.restore(); - } - this.win.focus(); - } - }); - - if (shouldQuit) { + const gotTheLock = app.requestSingleInstanceLock(); + if (!gotTheLock) { app.quit(); return; + } else { + app.on('second-instance', (event, commandLine, workingDirectory) => { + // Someone tried to run a second instance, we should focus our window. + if (this.win != null) { + if (this.win.isMinimized()) { + this.win.restore(); + } + this.win.focus(); + } + }); } } From af4e01c238b88ac296edfbf300a180e956da7d4b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 31 Dec 2018 12:39:59 -0500 Subject: [PATCH 0669/1626] avast passwords csv importer --- src/importers/avastCsvImporter.ts | 28 ++++++++++++++++++++++++++++ src/services/import.service.ts | 4 ++++ 2 files changed, 32 insertions(+) create mode 100644 src/importers/avastCsvImporter.ts diff --git a/src/importers/avastCsvImporter.ts b/src/importers/avastCsvImporter.ts new file mode 100644 index 0000000000..a1eb16574f --- /dev/null +++ b/src/importers/avastCsvImporter.ts @@ -0,0 +1,28 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +export class AvastCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(value.name); + cipher.login.uris = this.makeUriArray(value.web); + cipher.login.password = this.getValueOrDefault(value.password); + cipher.login.username = this.getValueOrDefault(value.login); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} diff --git a/src/services/import.service.ts b/src/services/import.service.ts index ca32707987..fef62131d8 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -20,6 +20,7 @@ import { KvpRequest } from '../models/request/kvpRequest'; import { CipherView } from '../models/view/cipherView'; import { AscendoCsvImporter } from '../importers/ascendoCsvImporter'; +import { AvastCsvImporter } from '../importers/avastCsvImporter'; import { AviraCsvImporter } from '../importers/aviraCsvImporter'; import { BitwardenCsvImporter } from '../importers/bitwardenCsvImporter'; import { BitwardenJsonImporter } from '../importers/bitwardenJsonImporter'; @@ -101,6 +102,7 @@ export class ImportService implements ImportServiceAbstraction { { id: 'passwordagentcsv', name: 'Password Agent (csv)' }, { id: 'passpackcsv', name: 'Passpack (csv)' }, { id: 'passmanjson', name: 'Passman (json)' }, + { id: 'avastcsv', name: 'Avast Passwords (csv)' }, ]; constructor(private cipherService: CipherService, private folderService: FolderService, @@ -223,6 +225,8 @@ export class ImportService implements ImportServiceAbstraction { return new PasspackCsvImporter(); case 'passmanjson': return new PassmanJsonImporter(); + case 'avastcsv': + return new AvastCsvImporter(); default: return null; } From e408189cf99ecc97fb5d0e03de6d4d933d558215 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 1 Jan 2019 23:11:16 -0500 Subject: [PATCH 0670/1626] space is nbsp --- src/angular/pipes/color-password.pipe.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/angular/pipes/color-password.pipe.ts b/src/angular/pipes/color-password.pipe.ts index f5de7967e5..96c4d497cd 100644 --- a/src/angular/pipes/color-password.pipe.ts +++ b/src/angular/pipes/color-password.pipe.ts @@ -27,6 +27,10 @@ export class ColorPasswordPipe implements PipeTransform { character = '>'; isSpecial = true; break; + case ' ': + character = ' '; + isSpecial = true; + break; default: break; } From c15beac78900bc9bca8337d829bffa61066c12a4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 3 Jan 2019 00:08:26 -0500 Subject: [PATCH 0671/1626] importer fixes --- src/importers/onepassword1PifImporter.ts | 20 +++++++++++++++++++- src/importers/roboformCsvImporter.ts | 18 ++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/importers/onepassword1PifImporter.ts b/src/importers/onepassword1PifImporter.ts index aad0d31d8f..eb004fd78d 100644 --- a/src/importers/onepassword1PifImporter.ts +++ b/src/importers/onepassword1PifImporter.ts @@ -55,7 +55,9 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { cipher.type = CipherType.Card; cipher.card = new CardView(); } - + if (cipher.type === CipherType.Login && !this.isNullOrWhitespace(item.details.password)) { + cipher.login.password = item.details.password; + } if (!this.isNullOrWhitespace(item.details.notesPlain)) { cipher.notes = item.details.notesPlain.split(this.newLineRegex).join('\n') + '\n'; } @@ -91,6 +93,22 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { if (!this.isNullOrWhitespace(item.secureContents.notesPlain)) { cipher.notes = item.secureContents.notesPlain.split(this.newLineRegex).join('\n') + '\n'; } + if (cipher.type === CipherType.Login) { + if (!this.isNullOrWhitespace(item.secureContents.password)) { + cipher.login.password = item.secureContents.password; + } + if (item.secureContents.URLs != null) { + const urls: string[] = []; + item.secureContents.URLs.forEach((u: any) => { + if (!this.isNullOrWhitespace(u.url)) { + urls.push(u.url); + } + }); + if (urls.length > 0) { + cipher.login.uris = this.makeUriArray(urls); + } + } + } if (item.secureContents.fields != null) { this.parseFields(item.secureContents.fields, cipher, 'designation', 'value', 'name'); } diff --git a/src/importers/roboformCsvImporter.ts b/src/importers/roboformCsvImporter.ts index 741cc69e4e..056479db83 100644 --- a/src/importers/roboformCsvImporter.ts +++ b/src/importers/roboformCsvImporter.ts @@ -25,11 +25,29 @@ export class RoboFormCsvImporter extends BaseImporter implements Importer { cipher.login.username = this.getValueOrDefault(value.Login); cipher.login.password = this.getValueOrDefault(value.Pwd); cipher.login.uris = this.makeUriArray(value.Url); + + if (!this.isNullOrWhitespace(value.Rf_fields)) { + let fields: string[] = [value.Rf_fields]; + if (value.__parsed_extra != null && value.__parsed_extra.length > 0) { + fields = fields.concat(value.__parsed_extra); + } + fields.forEach((field: string) => { + const parts = field.split(':'); + if (parts.length < 3) { + return; + } + const key = parts[0] === '-no-name-' ? null : parts[0]; + const val = parts.length === 4 && parts[2] === 'rck' ? parts[1] : parts[2]; + this.processKvp(cipher, key, val); + }); + } + this.cleanupCipher(cipher); if (i === results.length && cipher.name === '--' && this.isNullOrWhitespace(cipher.login.password)) { return; } + result.ciphers.push(cipher); i++; }); From 91bebbbd62c00cccb605acb0e5d760e22c68d9eb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 3 Jan 2019 09:58:37 -0500 Subject: [PATCH 0672/1626] f secure key importer, setCardExpiration helper --- src/importers/baseImporter.ts | 25 ++++++++++++ src/importers/enpassCsvImporter.ts | 20 +--------- src/importers/enpassJsonImporter.ts | 19 +-------- src/importers/fsecureFskImporter.ts | 60 +++++++++++++++++++++++++++++ src/services/import.service.ts | 4 ++ 5 files changed, 92 insertions(+), 36 deletions(-) create mode 100644 src/importers/fsecureFskImporter.ts diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index 76069e261b..67d9f3f62e 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -227,6 +227,31 @@ export abstract class BaseImporter { return null; } + protected setCardExpiration(cipher: CipherView, expiration: string): boolean { + if (!this.isNullOrWhitespace(expiration)) { + const parts = expiration.split('/'); + if (parts.length === 2) { + let month: string = null; + let year: string = null; + if (parts[0].length === 1 || parts[0].length === 2) { + month = parts[0]; + if (month.length === 2 && month[0] === '0') { + month = month.substr(1, 1); + } + } + if (parts[1].length === 2 || parts[1].length === 4) { + year = month.length === 2 ? '20' + parts[1] : parts[1]; + } + if (month != null && year != null) { + cipher.card.expMonth = month; + cipher.card.expYear = year; + return true; + } + } + } + return false; + } + protected moveFoldersToCollections(result: ImportResult) { result.folderRelationships.forEach((r) => result.collectionRelationships.push(r)); result.collections = result.folders.map((f) => { diff --git a/src/importers/enpassCsvImporter.ts b/src/importers/enpassCsvImporter.ts index bf5bd5fb57..02fa9ab3ca 100644 --- a/src/importers/enpassCsvImporter.ts +++ b/src/importers/enpassCsvImporter.ts @@ -81,24 +81,8 @@ export class EnpassCsvImporter extends BaseImporter implements Importer { continue; } else if (fieldNameLower === 'expiry date' && this.isNullOrWhitespace(cipher.card.expMonth) && this.isNullOrWhitespace(cipher.card.expYear)) { - const parts = fieldValue.split('/'); - if (parts.length === 2) { - let month: string = null; - let year: string = null; - if (parts[0].length === 1 || parts[0].length === 2) { - month = parts[0]; - if (month.length === 2 && month[0] === '0') { - month = month.substr(1, 1); - } - } - if (parts[1].length === 2 || parts[1].length === 4) { - year = month.length === 2 ? '20' + parts[1] : parts[1]; - } - if (month != null && year != null) { - cipher.card.expMonth = month; - cipher.card.expYear = year; - continue; - } + if (this.setCardExpiration(cipher, fieldValue)) { + continue; } } else if (fieldNameLower === 'type') { // Skip since brand was determined from number above diff --git a/src/importers/enpassJsonImporter.ts b/src/importers/enpassJsonImporter.ts index 1a76a2ce6a..b21e454e51 100644 --- a/src/importers/enpassJsonImporter.ts +++ b/src/importers/enpassJsonImporter.ts @@ -84,24 +84,7 @@ export class EnpassJsonImporter extends BaseImporter implements Importer { } else if (field.type === 'ccCvc' && this.isNullOrWhitespace(cipher.card.code)) { cipher.card.code = field.value; } else if (field.type === 'ccExpiry' && this.isNullOrWhitespace(cipher.card.expYear)) { - const parts = field.value.split('/'); - if (parts.length === 2) { - let month: string = null; - let year: string = null; - if (parts[0].length === 1 || parts[0].length === 2) { - month = parts[0]; - if (month.length === 2 && month[0] === '0') { - month = month.substr(1, 1); - } - } - if (parts[1].length === 2 || parts[1].length === 4) { - year = month.length === 2 ? '20' + parts[1] : parts[1]; - } - if (month != null && year != null) { - cipher.card.expMonth = month; - cipher.card.expYear = year; - } - } else { + if (!this.setCardExpiration(cipher, field.value)) { this.processKvp(cipher, field.label, field.value); } } else { diff --git a/src/importers/fsecureFskImporter.ts b/src/importers/fsecureFskImporter.ts new file mode 100644 index 0000000000..81ca6d5f30 --- /dev/null +++ b/src/importers/fsecureFskImporter.ts @@ -0,0 +1,60 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CardView } from '../models/view/cardView'; + +import { CipherType } from '../enums/cipherType'; + +export class FSecureFskImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = JSON.parse(data); + if (results == null || results.data == null) { + result.success = false; + return result; + } + + for (const key in results.data) { + if (!results.data.hasOwnProperty(key)) { + continue; + } + + const value = results.data[key]; + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(value.service); + cipher.notes = this.getValueOrDefault(value.notes); + + if (value.style === 'website') { + cipher.login.username = this.getValueOrDefault(value.username); + cipher.login.password = this.getValueOrDefault(value.password); + cipher.login.uris = this.makeUriArray(value.url); + } else if (value.style === 'creditcard') { + cipher.type = CipherType.Card; + cipher.card = new CardView(); + cipher.card.cardholderName = this.getValueOrDefault(value.username); + cipher.card.number = this.getValueOrDefault(value.creditNumber); + cipher.card.brand = this.getCardBrand(cipher.card.number); + cipher.card.code = this.getValueOrDefault(value.creditCvv); + if (!this.isNullOrWhitespace(value.creditExpiry)) { + if (!this.setCardExpiration(cipher, value.creditExpiry)) { + this.processKvp(cipher, 'Expiration', value.creditExpiry); + } + } + if (!this.isNullOrWhitespace(value.password)) { + this.processKvp(cipher, 'PIN', value.password); + } + } else { + continue; + } + + this.convertToNoteIfNeeded(cipher); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + } + + result.success = true; + return result; + } +} diff --git a/src/services/import.service.ts b/src/services/import.service.ts index fef62131d8..32a7a625c8 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -31,6 +31,7 @@ import { DashlaneJsonImporter } from '../importers/dashlaneJsonImporter'; import { EnpassCsvImporter } from '../importers/enpassCsvImporter'; import { EnpassJsonImporter } from '../importers/enpassJsonImporter'; import { FirefoxCsvImporter } from '../importers/firefoxCsvImporter'; +import { FSecureFskImporter } from '../importers/fsecureFskImporter'; import { GnomeJsonImporter } from '../importers/gnomeJsonImporter'; import { Importer } from '../importers/importer'; import { KeePass2XmlImporter } from '../importers/keepass2XmlImporter'; @@ -103,6 +104,7 @@ export class ImportService implements ImportServiceAbstraction { { id: 'passpackcsv', name: 'Passpack (csv)' }, { id: 'passmanjson', name: 'Passman (json)' }, { id: 'avastcsv', name: 'Avast Passwords (csv)' }, + { id: 'fsecurefsk', name: 'F-Secure KEY (fsk)' }, ]; constructor(private cipherService: CipherService, private folderService: FolderService, @@ -227,6 +229,8 @@ export class ImportService implements ImportServiceAbstraction { return new PassmanJsonImporter(); case 'avastcsv': return new AvastCsvImporter(); + case 'fsecurefsk': + return new FSecureFskImporter(); default: return null; } From fc5fcb905fe465e99e85540f6b645c0f11b6704d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 3 Jan 2019 10:22:34 -0500 Subject: [PATCH 0673/1626] send message when ciphers added/edited --- src/angular/components/add-edit.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 607c95b691..837898a1c2 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -16,6 +16,7 @@ import { CipherService } from '../../abstractions/cipher.service'; import { CollectionService } from '../../abstractions/collection.service'; import { FolderService } from '../../abstractions/folder.service'; import { I18nService } from '../../abstractions/i18n.service'; +import { MessagingService } from '../../abstractions/messaging.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { StateService } from '../../abstractions/state.service'; import { UserService } from '../../abstractions/user.service'; @@ -74,7 +75,8 @@ export class AddEditComponent implements OnInit { constructor(protected cipherService: CipherService, protected folderService: FolderService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected auditService: AuditService, protected stateService: StateService, - protected userService: UserService, protected collectionService: CollectionService) { + protected userService: UserService, protected collectionService: CollectionService, + protected messagingService: MessagingService) { this.typeOptions = [ { name: i18nService.t('typeLogin'), value: CipherType.Login }, { name: i18nService.t('typeCard'), value: CipherType.Card }, @@ -215,6 +217,7 @@ export class AddEditComponent implements OnInit { this.platformUtilsService.showToast('success', null, this.i18nService.t(this.editMode ? 'editedItem' : 'addedItem')); this.onSavedCipher.emit(this.cipher); + this.messagingService.send(this.editMode ? 'editedCipher' : 'addedCipher'); return true; } catch { } From 035b4e1dd573049231517205eb43c125bb2b7be2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 4 Jan 2019 23:54:57 -0500 Subject: [PATCH 0674/1626] parse validation errors from error response model --- src/angular/services/validation.service.ts | 21 +++--------- src/models/response/errorResponse.ts | 38 +++++++++++++++++----- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/angular/services/validation.service.ts b/src/angular/services/validation.service.ts index c5c3e293ae..c5857221db 100644 --- a/src/angular/services/validation.service.ts +++ b/src/angular/services/validation.service.ts @@ -3,6 +3,8 @@ import { Injectable } from '@angular/core'; import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { ErrorResponse } from '../../models/response/errorResponse'; + @Injectable() export class ValidationService { constructor(private i18nService: I18nService, private platformUtilsService: PlatformUtilsService) { } @@ -15,23 +17,10 @@ export class ValidationService { errors.push(data); } else if (data == null || typeof data !== 'object') { errors.push(defaultErrorMessage); - } else if (data.validationErrors == null) { - errors.push(data.message ? data.message : defaultErrorMessage); + } else if (data.validationErrors != null) { + errors.concat((data as ErrorResponse).getAllMessages()); } else { - for (const key in data.validationErrors) { - if (!data.validationErrors.hasOwnProperty(key)) { - continue; - } - - data.validationErrors[key].forEach((item: string) => { - let prefix = ''; - if (key.indexOf('[') > -1 && key.indexOf(']') > -1) { - const lastSep = key.lastIndexOf('.'); - prefix = key.substr(0, lastSep > -1 ? lastSep : key.length) + ': '; - } - errors.push(prefix + item); - }); - } + errors.push(data.message ? data.message : defaultErrorMessage); } if (errors.length === 1) { diff --git a/src/models/response/errorResponse.ts b/src/models/response/errorResponse.ts index 74abda15f4..87afde4837 100644 --- a/src/models/response/errorResponse.ts +++ b/src/models/response/errorResponse.ts @@ -23,16 +23,38 @@ export class ErrorResponse { } 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]; - } + if (this.validationErrors == null) { + return this.message; + } + 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; } + + getAllMessages(): string[] { + const messages: string[] = []; + if (this.validationErrors == null) { + return messages; + } + for (const key in this.validationErrors) { + if (!this.validationErrors.hasOwnProperty(key)) { + continue; + } + this.validationErrors[key].forEach((item: string) => { + let prefix = ''; + if (key.indexOf('[') > -1 && key.indexOf(']') > -1) { + const lastSep = key.lastIndexOf('.'); + prefix = key.substr(0, lastSep > -1 ? lastSep : key.length) + ': '; + } + messages.push(prefix + item); + }); + } + return messages; + } } From 1542dd45d36e3f6ab3684be0a83bef663ba987af Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 5 Jan 2019 14:19:21 -0500 Subject: [PATCH 0675/1626] show last 5 on amex --- src/models/view/cardView.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/models/view/cardView.ts b/src/models/view/cardView.ts index 1630c67482..df3497c96c 100644 --- a/src/models/view/cardView.ts +++ b/src/models/view/cardView.ts @@ -47,7 +47,10 @@ export class CardView implements View { } else { this._subTitle = ''; } - this._subTitle += ('*' + this.number.substr(this.number.length - 4)); + + // Show last 5 on amex, last 4 for all others + const count = this.number.length >= 5 && this.number.match(new RegExp('^3[47]')) != null ? 5 : 4; + this._subTitle += ('*' + this.number.substr(this.number.length - count)); } } return this._subTitle; From e7464785e134e5d723a4901cf54c5dd1668efb5c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 7 Jan 2019 10:33:13 -0500 Subject: [PATCH 0676/1626] allow launching URLs without protocol than end with tld --- src/angular/components/view.component.ts | 2 +- src/misc/utils.ts | 9 ++++++++- src/models/view/loginUriView.ts | 10 ++++++++-- src/models/view/loginView.ts | 4 ++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index b645f92ff5..1fd26d1015 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -134,7 +134,7 @@ export class ViewComponent implements OnDestroy, OnInit { } this.platformUtilsService.eventTrack('Launched Login URI'); - this.platformUtilsService.launchUri(uri.uri); + this.platformUtilsService.launchUri(uri.launchUri); } copy(value: string, typeI18nKey: string, aType: string) { diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 2760d2af6c..d07c6f6af4 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -12,6 +12,7 @@ export class Utils { static isBrowser = true; static isMobileBrowser = false; static global: any = null; + static tldEndingRegex = /.*\.(com|net|org|edu|uk|gov|ca|de|jp|fr|au|ru|ch|io|es|us|co|xyz|info|ly|mil)$/; static init() { if (Utils.inited) { @@ -175,7 +176,13 @@ export class Utils { return null; } - if (uriString.startsWith('http://') || uriString.startsWith('https://')) { + let httpUrl = uriString.startsWith('http://') || uriString.startsWith('https://'); + if (!httpUrl && Utils.tldEndingRegex.test(uriString)) { + uriString = 'http://' + uriString; + httpUrl = true; + } + + if (httpUrl) { try { const url = Utils.getUrlObject(uriString); if (url.hostname === 'localhost' || Utils.validIpAddress(url.hostname)) { diff --git a/src/models/view/loginUriView.ts b/src/models/view/loginUriView.ts index 2ac71bd8f2..70698ca33d 100644 --- a/src/models/view/loginUriView.ts +++ b/src/models/view/loginUriView.ts @@ -70,7 +70,8 @@ export class LoginUriView implements View { } get isWebsite(): boolean { - return this.uri != null && (this.uri.indexOf('http://') === 0 || this.uri.indexOf('https://') === 0); + return this.uri != null && (this.uri.indexOf('http://') === 0 || this.uri.indexOf('https://') === 0 || + (this.uri.indexOf('://') < 0 && Utils.tldEndingRegex.test(this.uri))); } get canLaunch(): boolean { @@ -78,8 +79,9 @@ export class LoginUriView implements View { return this._canLaunch; } if (this.uri != null) { + const uri = this.launchUri; for (let i = 0; i < CanLaunchWhitelist.length; i++) { - if (this.uri.indexOf(CanLaunchWhitelist[i]) === 0) { + if (uri.indexOf(CanLaunchWhitelist[i]) === 0) { this._canLaunch = true; return this._canLaunch; } @@ -88,4 +90,8 @@ export class LoginUriView implements View { this._canLaunch = false; return this._canLaunch; } + + get launchUri(): string { + return this.uri.indexOf('://') < 0 && Utils.tldEndingRegex.test(this.uri) ? ('http://' + this.uri) : this.uri; + } } diff --git a/src/models/view/loginView.ts b/src/models/view/loginView.ts index e0c7b06bf0..822a9361d3 100644 --- a/src/models/view/loginView.ts +++ b/src/models/view/loginView.ts @@ -34,6 +34,10 @@ export class LoginView implements View { return this.hasUris && this.uris[0].canLaunch; } + get launchUri(): string { + return this.canLaunch ? this.uris[0].launchUri : null; + } + get hasUris(): boolean { return this.uris != null && this.uris.length > 0; } From f3f17138c82b59a65c68cc2c20d8bb6112192743 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 7 Jan 2019 10:40:11 -0500 Subject: [PATCH 0677/1626] check for existing protocol --- src/misc/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/misc/utils.ts b/src/misc/utils.ts index d07c6f6af4..2c60a1a7a3 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -177,7 +177,7 @@ export class Utils { } let httpUrl = uriString.startsWith('http://') || uriString.startsWith('https://'); - if (!httpUrl && Utils.tldEndingRegex.test(uriString)) { + if (!httpUrl && uriString.indexOf('://') < 0 && Utils.tldEndingRegex.test(uriString)) { uriString = 'http://' + uriString; httpUrl = true; } From 65bd33d860f3af71968a15e78491221091c4369e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 7 Jan 2019 23:30:04 -0500 Subject: [PATCH 0678/1626] expose email on init --- src/angular/components/lock.component.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 6e8caea78a..8ebc938e01 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -1,3 +1,4 @@ +import { OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { CryptoService } from '../../abstractions/crypto.service'; @@ -6,9 +7,10 @@ import { MessagingService } from '../../abstractions/messaging.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { UserService } from '../../abstractions/user.service'; -export class LockComponent { +export class LockComponent implements OnInit { masterPassword: string = ''; showPassword: boolean = false; + email: string; protected successRoute: string = 'vault'; protected onSuccessfulSubmit: () => void; @@ -17,6 +19,10 @@ export class LockComponent { protected platformUtilsService: PlatformUtilsService, protected messagingService: MessagingService, protected userService: UserService, protected cryptoService: CryptoService) { } + async ngOnInit() { + this.email = await this.userService.getEmail(); + } + async submit() { if (this.masterPassword == null || this.masterPassword === '') { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), @@ -24,10 +30,9 @@ export class LockComponent { return; } - const email = await this.userService.getEmail(); const kdf = await this.userService.getKdf(); const kdfIterations = await this.userService.getKdfIterations(); - const key = await this.cryptoService.makeKey(this.masterPassword, email, kdf, kdfIterations); + const key = await this.cryptoService.makeKey(this.masterPassword, this.email, kdf, kdfIterations); const keyHash = await this.cryptoService.hashPassword(this.masterPassword, key); const storedKeyHash = await this.cryptoService.getKeyHash(); From 2e9ce1571514e2b4ae9ed61a1368b2db03a0ca5d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 9 Jan 2019 11:59:11 -0500 Subject: [PATCH 0679/1626] default match detection setting --- src/services/cipher.service.ts | 12 +++++++++--- src/services/constants.service.ts | 2 ++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 9b90cf50d2..957feedc46 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -43,6 +43,8 @@ import { SettingsService } from '../abstractions/settings.service'; import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; +import { ConstantsService } from './constants.service'; + import { sequentialize } from '../misc/sequentialize'; import { Utils } from '../misc/utils'; @@ -343,6 +345,11 @@ export class CipherService implements CipherServiceAbstraction { const matchingDomains = result[0]; const ciphers = result[1]; + let defaultMatch = await this.storageService.get(ConstantsService.defaultUriMatch); + if (defaultMatch == null) { + defaultMatch = UriMatchType.Domain; + } + return ciphers.filter((cipher) => { if (includeOtherTypes && includeOtherTypes.indexOf(cipher.type) > -1) { return true; @@ -355,9 +362,8 @@ export class CipherService implements CipherServiceAbstraction { continue; } - switch (u.match) { - case null: - case undefined: + const match = u.match == null ? defaultMatch : u.match; + switch (match) { case UriMatchType.Domain: if (domain != null && u.domain != null && matchingDomains.indexOf(u.domain) > -1) { if (DomainMatchBlacklist.has(u.domain)) { diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index 246f04c1dd..dc6d0cf89d 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -17,6 +17,7 @@ export class ConstantsService { static readonly autoConfirmFingerprints: string = 'autoConfirmFingerprints'; static readonly dontShowCardsCurrentTab: string = 'dontShowCardsCurrentTab'; static readonly dontShowIdentitiesCurrentTab: string = 'dontShowIdentitiesCurrentTab'; + static readonly defaultUriMatch: string = 'defaultUriMatch'; readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; readonly disableGaKey: string = ConstantsService.disableGaKey; @@ -35,4 +36,5 @@ export class ConstantsService { readonly autoConfirmFingerprints: string = ConstantsService.autoConfirmFingerprints; readonly dontShowCardsCurrentTab: string = ConstantsService.dontShowCardsCurrentTab; readonly dontShowIdentitiesCurrentTab: string = ConstantsService.dontShowIdentitiesCurrentTab; + readonly defaultUriMatch: string = ConstantsService.defaultUriMatch; } From f4c4f2802637f775c3bfba2ef4109350bc842b8f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 15 Jan 2019 11:02:15 -0500 Subject: [PATCH 0680/1626] disable analytics --- src/misc/analytics.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/misc/analytics.ts b/src/misc/analytics.ts index 687d6fc4c9..b6d0808cb9 100644 --- a/src/misc/analytics.ts +++ b/src/misc/analytics.ts @@ -53,6 +53,8 @@ export class Analytics { } async ga(action: string, param1: any, param2?: any) { + return; + if (this.gaFilter != null && this.gaFilter()) { return; } From cb7336c0e8ae1f0f74c76a4d6704555cc7440a3b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 15 Jan 2019 11:34:35 -0500 Subject: [PATCH 0681/1626] null or whitespace util --- src/importers/baseImporter.ts | 2 +- src/misc/utils.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index 67d9f3f62e..a1196b587c 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -155,7 +155,7 @@ export abstract class BaseImporter { } protected isNullOrWhitespace(str: string): boolean { - return str == null || typeof str !== 'string' || str.trim() === ''; + return Utils.isNullOrWhitespace(str); } protected getValueOrDefault(str: string, defaultValue: string = null): string { diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 2c60a1a7a3..c7f3eeccca 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -236,6 +236,10 @@ export class Utils { }; } + static isNullOrWhitespace(str: string): boolean { + return str == null || typeof str !== 'string' || str.trim() === ''; + } + private static validIpAddress(ipString: string): boolean { // tslint:disable-next-line const ipRegex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; From 80c0da766e549d519f91c0b9ab6db925a93ab55a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 16 Jan 2019 12:53:59 -0500 Subject: [PATCH 0682/1626] change backslash to fowardslash for lastpass import --- src/importers/lastpassCsvImporter.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/importers/lastpassCsvImporter.ts b/src/importers/lastpassCsvImporter.ts index acb839c589..a20008c569 100644 --- a/src/importers/lastpassCsvImporter.ts +++ b/src/importers/lastpassCsvImporter.ts @@ -68,6 +68,9 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { if (addFolder) { const f = new FolderView(); f.name = value.grouping; + if (f.name != null) { + f.name = f.name.replace(/\\/g, '/'); + } result.folders.push(f); } if (hasFolder) { From cbcf0adad5f313c1f37232311ddce318365911b3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 16 Jan 2019 13:51:14 -0500 Subject: [PATCH 0683/1626] full address --- src/models/view/identityView.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/models/view/identityView.ts b/src/models/view/identityView.ts index 4ae5fa0cf9..246ed7a763 100644 --- a/src/models/view/identityView.ts +++ b/src/models/view/identityView.ts @@ -2,6 +2,8 @@ import { View } from './view'; import { Identity } from '../domain/identity'; +import { Utils } from '../../misc/utils'; + export class IdentityView implements View { title: string = null; middleName: string; @@ -83,4 +85,21 @@ export class IdentityView implements View { return null; } + + get fullAddress(): string { + let address = this.address1; + if (Utils.isNullOrWhitespace(this.address2)) { + if (Utils.isNullOrWhitespace(address)) { + address += ', '; + } + address += this.address2; + } + if (Utils.isNullOrWhitespace(this.address3)) { + if (Utils.isNullOrWhitespace(address)) { + address += ', '; + } + address += this.address3; + } + return address; + } } From 7cd8b63b941e199e5c8eb3438d8a87317315edbc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 17 Jan 2019 10:46:24 -0500 Subject: [PATCH 0684/1626] use getHibpBreach proxy --- src/abstractions/api.service.ts | 3 +++ src/services/api.service.ts | 8 ++++++++ src/services/audit.service.ts | 17 +++++++++-------- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index ee27049121..3fd8e0e430 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -52,6 +52,7 @@ import { VerifyDeleteRecoverRequest } from '../models/request/verifyDeleteRecove import { VerifyEmailRequest } from '../models/request/verifyEmailRequest'; import { BillingResponse } from '../models/response/billingResponse'; +import { BreachAccountResponse } from '../models/response/breachAccountResponse'; import { CipherResponse } from '../models/response/cipherResponse'; import { CollectionGroupDetailsResponse, @@ -247,6 +248,8 @@ export abstract class ApiService { getUserPublicKey: (id: string) => Promise; + getHibpBreach: (username: string) => Promise; + getActiveBearerToken: () => Promise; fetch: (request: Request) => Promise; } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 40c7c6c78c..bb9f7faa55 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -58,6 +58,7 @@ import { VerifyDeleteRecoverRequest } from '../models/request/verifyDeleteRecove import { VerifyEmailRequest } from '../models/request/verifyEmailRequest'; import { BillingResponse } from '../models/response/billingResponse'; +import { BreachAccountResponse } from '../models/response/breachAccountResponse'; import { CipherResponse } from '../models/response/cipherResponse'; import { CollectionGroupDetailsResponse, @@ -818,6 +819,13 @@ export class ApiService implements ApiServiceAbstraction { return new UserKeyResponse(r); } + // HIBP APIs + + async getHibpBreach(username: string): Promise { + const r = await this.send('GET', '/hibp/breach?username=' + username, null, true, true); + return r.map((a: any) => new BreachAccountResponse(a)); + } + // Helpers async getActiveBearerToken(): Promise { diff --git a/src/services/audit.service.ts b/src/services/audit.service.ts index ff7a90b996..7939149299 100644 --- a/src/services/audit.service.ts +++ b/src/services/audit.service.ts @@ -5,9 +5,9 @@ import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; import { Utils } from '../misc/utils'; import { BreachAccountResponse } from '../models/response/breachAccountResponse'; +import { ErrorResponse } from '../models/response/errorResponse'; const PwnedPasswordsApi = 'https://api.pwnedpasswords.com/range/'; -const HibpBreachApi = 'https://haveibeenpwned.com/api/v2/breachedaccount/'; export class AuditService implements AuditServiceAbstraction { constructor(private cryptoFunctionService: CryptoFunctionService, private apiService: ApiService) { } @@ -18,7 +18,7 @@ export class AuditService implements AuditServiceAbstraction { const hashStart = hash.substr(0, 5); const hashEnding = hash.substr(5); - const response = await fetch(new Request(PwnedPasswordsApi + hashStart)); + const response = await fetch(PwnedPasswordsApi + hashStart); const leakedHashes = await response.text(); const match = leakedHashes.split(/\r?\n/).find((v) => { return v.split(':')[0] === hashEnding; @@ -28,13 +28,14 @@ export class AuditService implements AuditServiceAbstraction { } async breachedAccounts(username: string): Promise { - const response = await fetch(new Request(HibpBreachApi + username)); - if (response.status === 404) { - return []; - } else if (response.status !== 200) { + try { + return await this.apiService.getHibpBreach(username); + } catch (e) { + const error = e as ErrorResponse; + if (error.statusCode === 404) { + return []; + } throw new Error(); } - const responseJson = await response.json(); - return responseJson.map((a: any) => new BreachAccountResponse(a)); } } From fa65b5637b5cd2e93b5d886584d6876c2c5eb571 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 17 Jan 2019 10:46:43 -0500 Subject: [PATCH 0685/1626] use some() --- src/importers/enpassJsonImporter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/importers/enpassJsonImporter.ts b/src/importers/enpassJsonImporter.ts index b21e454e51..79310220b4 100644 --- a/src/importers/enpassJsonImporter.ts +++ b/src/importers/enpassJsonImporter.ts @@ -28,7 +28,7 @@ export class EnpassJsonImporter extends BaseImporter implements Importer { } else if (item.template_type.indexOf('creditcard.') === 0) { this.processCard(cipher, item.fields); } else if (item.template_type.indexOf('identity.') < 0 && - item.fields.find((f: any) => f.type === 'password' && !this.isNullOrWhitespace(f.value)) != null) { + item.fields.some((f: any) => f.type === 'password' && !this.isNullOrWhitespace(f.value))) { this.processLogin(cipher, item.fields); } else { this.processNote(cipher, item.fields); From 50d242e53a4b553a6cb3575945a84812eda75b1d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 18 Jan 2019 23:42:03 -0500 Subject: [PATCH 0686/1626] fix kvp splits on secure note mapping --- src/importers/lastpassCsvImporter.ts | 31 +++++++++++++++++++--------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/importers/lastpassCsvImporter.ts b/src/importers/lastpassCsvImporter.ts index a20008c569..cbfebfec41 100644 --- a/src/importers/lastpassCsvImporter.ts +++ b/src/importers/lastpassCsvImporter.ts @@ -210,28 +210,39 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { const dataObj: any = {}; extraParts.forEach((extraPart) => { - const fieldParts = extraPart.split(':'); - if (fieldParts.length < 1 || this.isNullOrWhitespace(fieldParts[0]) || - this.isNullOrWhitespace(fieldParts[1]) || fieldParts[0] === 'NoteType') { + if (this.isNullOrWhitespace(extraPart)) { + return; + } + let key: string = null; + let val: string = null; + const colonIndex = extraPart.indexOf(':'); + if (colonIndex === -1) { + key = extraPart; + } else { + key = extraPart.substring(0, colonIndex); + if (extraPart.length > colonIndex) { + val = extraPart.substring(colonIndex + 1); + } + } + if (this.isNullOrWhitespace(key) || this.isNullOrWhitespace(val) || key === 'NoteType') { return; } - if (fieldParts[0] === 'Notes') { + if (key === 'Notes') { if (!this.isNullOrWhitespace(notes)) { - notes += ('\n' + fieldParts[1]); + notes += ('\n' + val); } else { - notes = fieldParts[1]; + notes = val; } - } else if (map.hasOwnProperty(fieldParts[0])) { - dataObj[map[fieldParts[0]]] = fieldParts[1]; + } else if (map.hasOwnProperty(key)) { + dataObj[map[key]] = val; } else { if (!this.isNullOrWhitespace(notes)) { notes += '\n'; } else { notes = ''; } - - notes += (fieldParts[0] + ': ' + fieldParts[1]); + notes += (key + ': ' + val); } }); From 5c7a911b2e84f89d4509b2fd786d47c4fb5c3719 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sun, 20 Jan 2019 23:03:09 -0500 Subject: [PATCH 0687/1626] re-assign concated errors --- src/angular/services/validation.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/angular/services/validation.service.ts b/src/angular/services/validation.service.ts index c5857221db..73e0555a3f 100644 --- a/src/angular/services/validation.service.ts +++ b/src/angular/services/validation.service.ts @@ -11,14 +11,14 @@ export class ValidationService { showError(data: any): string[] { const defaultErrorMessage = this.i18nService.t('unexpectedError'); - const errors: string[] = []; + let errors: string[] = []; if (data != null && typeof data === 'string') { errors.push(data); } else if (data == null || typeof data !== 'object') { errors.push(defaultErrorMessage); } else if (data.validationErrors != null) { - errors.concat((data as ErrorResponse).getAllMessages()); + errors = errors.concat((data as ErrorResponse).getAllMessages()); } else { errors.push(data.message ? data.message : defaultErrorMessage); } From 39b2ba1548b1843cd0cdce610da955844eecddae Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 21 Jan 2019 23:38:16 -0500 Subject: [PATCH 0688/1626] update angular and electron --- package-lock.json | 216 ++++++++++++++++++++++++++-------------------- package.json | 34 ++++---- src/globals.d.ts | 1 + 3 files changed, 140 insertions(+), 111 deletions(-) diff --git a/package-lock.json b/package-lock.json index 137b1e0dbc..9feb7bb90e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,81 +5,81 @@ "requires": true, "dependencies": { "@angular/animations": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-6.1.7.tgz", - "integrity": "sha512-bjX3VEVEh5scGDDmxEKPzYI8DWUbqOFA34aYDY2cHPnDkLM0I7pEtO44qb72FSbWwXn77sYlby/dx2gtRayOOA==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-7.2.1.tgz", + "integrity": "sha512-2AHc4HYz2cUVW3E0oYOTyUzBTnPJdtmVOx/Uo6+jnRqikvOGFOc5VXzFIYODe1Iiy+EYcSZ1lvQqwUbpZd6gwA==", "requires": { "tslib": "^1.9.0" } }, "@angular/common": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-6.1.7.tgz", - "integrity": "sha512-zFK2xM0hqR2ZWIfUsn+06jg+0K5PolzTxPjfUtVQDCZo+JHHKTVHEwtfORUaMTMfH9EqKrvfB3t6fCwK0523ag==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-7.2.1.tgz", + "integrity": "sha512-lYf3MeVMz69EriS5ANFY5PerJK0i4xHp/Jy67reb8ydZ+sfW320PUMuFtx3bZvk9PD7NdL3QZvXmla/ogrltTQ==", "requires": { "tslib": "^1.9.0" } }, "@angular/compiler": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-6.1.7.tgz", - "integrity": "sha512-JKuK/fzKNCF+mNPmPmGQjr0uHVpfxmrOqXBriJMklCtdsKeQW94BLUoNjn8h1H10rFbUqYuD5v9AAKdH77FgnA==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-7.2.1.tgz", + "integrity": "sha512-wf9w882hNoRaTDRqkEvQxV7nGB3liTX/LWEMunmm/Yz0nWkvgErR9pIHv3Sm4Ox0hyG3GdMpcVBzQ8qPomGOag==", "requires": { "tslib": "^1.9.0" } }, "@angular/core": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-6.1.7.tgz", - "integrity": "sha512-3MtS8EQy+saNcImDWghphOr/h3l5CpFnZW6aaHiL8T5CpTBNdB86uEmAwtiNQkJ0UeO+cztF1zNCzhm9R93/3w==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-7.2.1.tgz", + "integrity": "sha512-FYNAf4chxBoIVGCW2+fwR2MB2Ur5v1aG9L6zCcMXlZLbR64bu5j2m4e70RhXk/VptKvYWJ45od3xE5KfcaeEig==", "requires": { "tslib": "^1.9.0" } }, "@angular/forms": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-6.1.7.tgz", - "integrity": "sha512-McCElnn6Abr+HAjwxa1ldvIMs101TT0NGq8EHXLyF9QcKG24dU7425+MdLuW0OrtgBql2+RjlqnSiKuxDQHxJA==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-7.2.1.tgz", + "integrity": "sha512-MxinNUvl02UfpY9gJtbTU6Mdt9fjIJYOGskcpwm+5u9jiMeRvOnG94ySoNrygg3EWwScX+P0mM4KN6fJWau7gQ==", "requires": { "tslib": "^1.9.0" } }, "@angular/http": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/@angular/http/-/http-6.1.7.tgz", - "integrity": "sha512-N0wXHpEL/CsNM4l44Z+dU51Y994mBEHjt9yb0SeKf02mdrsTJK+cEvfZ0JkVDjGddqdWHvWFn3zSmkR79qLrSQ==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@angular/http/-/http-7.2.1.tgz", + "integrity": "sha512-3xfdN2bmCbzATwRGUEZQVkGn3IN6tMX/whLWGWgcEV3CENJqTUjfjn1+nSHASQLUnmOr5T7yTiWK5P7bDrHYzw==", "requires": { "tslib": "^1.9.0" } }, "@angular/platform-browser": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-6.1.7.tgz", - "integrity": "sha512-YOYg944aefCWElJhnma8X+3wJDb6nHf6aBAVN+YPg0bUplEFacR4y6PeM9QR8vjh5Y0kbGG9ZPGDT/WwP2t4sQ==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-7.2.1.tgz", + "integrity": "sha512-/6uHdFLmRkrkeOo+TzScrLG2YydG8kBNyT6ZpSOBf+bmB5DHyIGd55gh/tQJteKrnyadxRhqWCLBTYAbVX9Pnw==", "requires": { "tslib": "^1.9.0" } }, "@angular/platform-browser-dynamic": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-6.1.7.tgz", - "integrity": "sha512-sSF7n4SpwPiP1fMwocu/RUegpp/45jHK/+r9biXUXUBD12zO5QMcLHU393sjoNi7e6+meuXEH0pnWa66dTznjw==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.2.1.tgz", + "integrity": "sha512-hrSkI7aESEkqYnu628Z/LvYNlUNMqIqkXYAkT3urxFdCw7UwNeZKrDmd9sRwK3gK3sC1VeD9pXtqaKmGsnBjOA==", "requires": { "tslib": "^1.9.0" } }, "@angular/router": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-6.1.7.tgz", - "integrity": "sha512-YaOTq2icKAd9FDls2qo2Qp8FrmLGke3eA+bZ3FvOhFydxyUAvlU96N9Y9Gb05tXTtBaQNzAInov2bbp2YMFEFA==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-7.2.1.tgz", + "integrity": "sha512-3qMZnhFr6xx3dMy14rKwIw9ISTOZlsp9jAkthXVsfA2/thffScXHPBrH4SipkySLmOAtPmF5m5jscy8mx/1mJQ==", "requires": { "tslib": "^1.9.0" } }, "@angular/upgrade": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/@angular/upgrade/-/upgrade-6.1.7.tgz", - "integrity": "sha512-vPXK2lweiGXCigrels43QlEfMvvVnUcN0lhZq+Mjb3gmyEByRcsATH2DgxxTVIXj5zVytMOXR89lC/sSEaQHAA==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@angular/upgrade/-/upgrade-7.2.1.tgz", + "integrity": "sha512-2y41RbGt1MCEEGjeEpS32hL2sCAI39/BtqPq9L2yGfKdehVTavsh86Fpy8KHhLqgM9WxcisLqpwqj3IjXXkGBg==", "requires": { "tslib": "^1.9.0" } @@ -213,9 +213,9 @@ "dev": true }, "ajv": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz", - "integrity": "sha512-FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz", + "integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==", "dev": true, "requires": { "fast-deep-equal": "^2.0.1", @@ -1118,14 +1118,29 @@ "dev": true }, "builder-util-runtime": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-4.4.1.tgz", - "integrity": "sha512-8L2pbL6D3VdI1f8OMknlZJpw0c7KK15BRz3cY77AOUElc4XlCv2UhVV01jJM7+6Lx7henaQh80ALULp64eFYAQ==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.1.1.tgz", + "integrity": "sha512-+ieS4PMB33vVE2S3ZNWBEQJ1zKmAs/agrBdh7XadE1lKLjrH4aXYuOh9OOGdxqIRldhlhNBaF+yKMMEFOdNVig==", "requires": { - "bluebird-lst": "^1.0.5", - "debug": "^3.1.0", - "fs-extra-p": "^4.6.1", + "bluebird-lst": "^1.0.6", + "debug": "^4.1.1", + "fs-extra-p": "^7.0.0", "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } } }, "builtin-modules": { @@ -1599,9 +1614,9 @@ "dev": true }, "core-js": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", - "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.2.tgz", + "integrity": "sha512-NdBPF/RVwPW6jr0NCILuyN9RiqLo2b1mddWHkUL+VnvcB7dzlnBJ1bXYntjpTGOgkZiiLWj2JxmOr7eGE3qK6g==" }, "core-util-is": { "version": "1.0.2", @@ -1746,6 +1761,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, "requires": { "ms": "2.0.0" } @@ -1989,9 +2005,9 @@ "dev": true }, "electron": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/electron/-/electron-3.0.13.tgz", - "integrity": "sha512-tfx5jFgXhCmpe6oPjcesaRj7geHqQxrJdbpseanRzL9BbyYUtsj0HoxwPAUvCx4+52P6XryBwWTvne/1eBVf9Q==", + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/electron/-/electron-3.0.14.tgz", + "integrity": "sha512-1fG9bE0LzL5QXeEq2MC0dHdVO0pbZOnNlVAIyOyJaCFAu/TjLhxQfWj38bFUEojzuVlaR87tZz0iy2qlVZj3sw==", "dev": true, "requires": { "@types/node": "^8.0.24", @@ -2043,30 +2059,37 @@ } } }, - "electron-is-dev": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-0.3.0.tgz", - "integrity": "sha1-FOb9pcaOnk7L7/nM8DfL18BcWv4=" - }, "electron-log": { "version": "2.2.17", "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.17.tgz", "integrity": "sha512-v+Af5W5z99ehhaLOfE9eTSXUwjzh2wFlQjz51dvkZ6ZIrET6OB/zAZPvsuwT6tm3t5x+M1r+Ed3U3xtPZYAyuQ==" }, "electron-updater": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-3.0.3.tgz", - "integrity": "sha512-7gJLZp34Db+lXiJsFzW8DunGnvxJgZclBZa1DNLbXOet3lRXkVKbFJ73mClbv+UTW6hW/EJ6MmSsofRiK1s6Dw==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.0.6.tgz", + "integrity": "sha512-JPGLME6fxJcHG8hX7HWFl6Aew6iVm0DkcrENreKa5SUJCHG+uUaAhxDGDt+YGcNkyx1uJ6eBGMvFxDTLUv67pg==", "requires": { - "bluebird-lst": "^1.0.5", - "builder-util-runtime": "~4.4.1", - "electron-is-dev": "^0.3.0", - "fs-extra-p": "^4.6.1", + "bluebird-lst": "^1.0.6", + "builder-util-runtime": "~8.1.0", + "fs-extra-p": "^7.0.0", "js-yaml": "^3.12.0", "lazy-val": "^1.0.3", "lodash.isequal": "^4.5.0", - "semver": "^5.5.0", - "source-map-support": "^0.5.6" + "pako": "^1.0.7", + "semver": "^5.6.0", + "source-map-support": "^0.5.9" + }, + "dependencies": { + "pako": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.8.tgz", + "integrity": "sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA==" + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" + } } }, "elliptic": { @@ -2673,9 +2696,9 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, "fs-extra": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", - "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "requires": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -2683,12 +2706,12 @@ } }, "fs-extra-p": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.6.1.tgz", - "integrity": "sha512-IsTMbUS0svZKZTvqF4vDS9c/L7Mw9n8nZQWWeSzAGacOSe+8CzowhUN0tdZEZFIJNP5HC7L9j3MMikz/G4hDeQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-7.0.0.tgz", + "integrity": "sha512-5tg5jBOd0xIXjwj4PDnafOXL5TyPVzjxLby4DPKev53wurEXp7IsojBaD4Lj5M5w7jxw0pbkEU0fFEPmcKoMnA==", "requires": { - "bluebird-lst": "^1.0.5", - "fs-extra": "^6.0.1" + "bluebird-lst": "^1.0.6", + "fs-extra": "^7.0.0" } }, "fs.realpath": { @@ -2738,12 +2761,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2763,7 +2788,8 @@ "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", @@ -2911,6 +2937,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4800,7 +4827,8 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true }, "msgpack5": { "version": "4.2.1", @@ -6290,9 +6318,9 @@ "dev": true }, "rxjs": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.2.tgz", - "integrity": "sha512-hV7criqbR0pe7EeL3O66UYVg92IR0XsA97+9y+BWTePK9SKmEI5Qd3Zj6uPnGkNzXsBywBQWTvujPl+1Kn9Zjw==", + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", + "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==", "requires": { "tslib": "^1.9.0" } @@ -6639,9 +6667,9 @@ } }, "source-map-support": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz", - "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==", + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.10.tgz", + "integrity": "sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ==", "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -7233,9 +7261,9 @@ "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" }, "tslint": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.11.0.tgz", - "integrity": "sha1-mPMMAurjzecAYgHkwzywi0hYHu0=", + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.12.1.tgz", + "integrity": "sha512-sfodBHOucFg6egff8d1BvuofoOQ/nOeYNfbp7LDlKBcLNrL3lmS5zoiDGyOMdT7YsEXAwWpTdAHwOGOc8eRZAw==", "dev": true, "requires": { "babel-code-frame": "^6.22.0", @@ -7262,9 +7290,9 @@ } }, "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { "ansi-styles": "^3.2.1", @@ -7273,9 +7301,9 @@ } }, "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", "dev": true }, "has-flag": { @@ -7285,9 +7313,9 @@ "dev": true }, "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { "has-flag": "^3.0.0" @@ -7361,9 +7389,9 @@ } }, "typescript": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", - "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.4.tgz", + "integrity": "sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg==", "dev": true }, "uglify-js": { @@ -7904,9 +7932,9 @@ "dev": true }, "zone.js": { - "version": "0.8.26", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.26.tgz", - "integrity": "sha512-W9Nj+UmBJG251wkCacIkETgra4QgBo/vgoEkb4a2uoLzpQG7qF9nzwoLXWU5xj3Fg2mxGvEDh47mg24vXccYjA==" + "version": "0.8.28", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.28.tgz", + "integrity": "sha512-MjwlvV0wr65IQiT0WSHedo/zUhAqtypMdTUjqroV81RohGj1XANwHuC37dwYxphTRbZBYidk0gNS0dQrU2Q3Pw==" }, "zxcvbn": { "version": "4.4.2", diff --git a/package.json b/package.json index 63a26a531b..8692494aab 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@types/webcrypto": "0.0.28", "@types/zxcvbn": "^4.4.0", "concurrently": "3.5.1", - "electron": "3.0.13", + "electron": "3.0.14", "jasmine": "^3.2.0", "jasmine-core": "^3.2.1", "jasmine-spec-reporter": "^4.2.1", @@ -54,28 +54,28 @@ "karma-typescript": "^3.0.13", "nodemon": "^1.17.3", "rimraf": "^2.6.2", - "tslint": "^5.8.0", + "tslint": "^5.12.1", "typemoq": "^2.1.0", - "typescript": "^2.7.2" + "typescript": "3.2.4" }, "dependencies": { - "@angular/animations": "6.1.7", - "@angular/common": "6.1.7", - "@angular/compiler": "6.1.7", - "@angular/core": "6.1.7", - "@angular/forms": "6.1.7", - "@angular/http": "6.1.7", - "@angular/platform-browser": "6.1.7", - "@angular/platform-browser-dynamic": "6.1.7", - "@angular/router": "6.1.7", - "@angular/upgrade": "6.1.7", + "@angular/animations": "7.2.1", + "@angular/common": "7.2.1", + "@angular/compiler": "7.2.1", + "@angular/core": "7.2.1", + "@angular/forms": "7.2.1", + "@angular/http": "7.2.1", + "@angular/platform-browser": "7.2.1", + "@angular/platform-browser-dynamic": "7.2.1", + "@angular/router": "7.2.1", + "@angular/upgrade": "7.2.1", "@aspnet/signalr": "1.0.4", "@aspnet/signalr-protocol-msgpack": "1.0.4", "big-integer": "1.6.36", - "core-js": "2.5.7", + "core-js": "2.6.2", "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", "electron-log": "2.2.17", - "electron-updater": "3.0.3", + "electron-updater": "4.0.6", "form-data": "2.3.2", "keytar": "4.2.1", "lowdb": "1.0.0", @@ -83,9 +83,9 @@ "node-fetch": "2.2.0", "node-forge": "0.7.6", "papaparse": "4.6.0", - "rxjs": "6.3.2", + "rxjs": "6.3.3", "tldjs": "2.3.1", - "zone.js": "0.8.26", + "zone.js": "0.8.28", "zxcvbn": "4.4.2" } } diff --git a/src/globals.d.ts b/src/globals.d.ts index 4859a0869e..8116ab173f 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -1,2 +1,3 @@ declare function escape(s: string): string; declare function unescape(s: string): string; +declare module 'duo_web_sdk'; From 6dc44c088590355b1dd760f1767c668c3feebfd7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 22 Jan 2019 16:12:34 -0500 Subject: [PATCH 0689/1626] generic broadcaster service --- src/abstractions/broadcaster.service.ts | 5 ++++ src/angular/services/broadcaster.service.ts | 32 ++------------------- src/services/broadcaster.service.ts | 32 +++++++++++++++++++++ 3 files changed, 40 insertions(+), 29 deletions(-) create mode 100644 src/abstractions/broadcaster.service.ts create mode 100644 src/services/broadcaster.service.ts diff --git a/src/abstractions/broadcaster.service.ts b/src/abstractions/broadcaster.service.ts new file mode 100644 index 0000000000..9d0c8de950 --- /dev/null +++ b/src/abstractions/broadcaster.service.ts @@ -0,0 +1,5 @@ +export abstract class BroadcasterService { + send: (message: any, id?: string) => void; + subscribe: (id: string, messageCallback: (message: any) => any) => void; + unsubscribe: (id: string) => void; +} diff --git a/src/angular/services/broadcaster.service.ts b/src/angular/services/broadcaster.service.ts index fe747e7b0b..3b61a7e463 100644 --- a/src/angular/services/broadcaster.service.ts +++ b/src/angular/services/broadcaster.service.ts @@ -1,33 +1,7 @@ import { Injectable } from '@angular/core'; +import { BroadcasterService as BaseBroadcasterService } from '../../services/broadcaster.service'; + @Injectable() -export class BroadcasterService { - subscribers: Map any> = new Map any>(); - - send(message: any, id?: string) { - if (id != null) { - if (this.subscribers.has(id)) { - this.subscribers.get(id)(message); - } - return; - } - - this.subscribers.forEach((value) => { - value(message); - }); - } - - subscribe(id: string, messageCallback: (message: any) => any) { - if (this.subscribers.has(id)) { - return; - } - - this.subscribers.set(id, messageCallback); - } - - unsubscribe(id: string) { - if (this.subscribers.has(id)) { - this.subscribers.delete(id); - } - } +export class BroadcasterService extends BaseBroadcasterService { } diff --git a/src/services/broadcaster.service.ts b/src/services/broadcaster.service.ts new file mode 100644 index 0000000000..9f46ccc6f3 --- /dev/null +++ b/src/services/broadcaster.service.ts @@ -0,0 +1,32 @@ +import { BroadcasterService as BroadcasterServiceAbstraction } from '../abstractions/broadcaster.service'; + +export class BroadcasterService implements BroadcasterServiceAbstraction { + subscribers: Map any> = new Map any>(); + + send(message: any, id?: string) { + if (id != null) { + if (this.subscribers.has(id)) { + this.subscribers.get(id)(message); + } + return; + } + + this.subscribers.forEach((value) => { + value(message); + }); + } + + subscribe(id: string, messageCallback: (message: any) => any) { + if (this.subscribers.has(id)) { + return; + } + + this.subscribers.set(id, messageCallback); + } + + unsubscribe(id: string) { + if (this.subscribers.has(id)) { + this.subscribers.delete(id); + } + } +} From 09df62db4cb29832313bf612fa8b77c63a11f026 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 23 Jan 2019 16:22:38 -0500 Subject: [PATCH 0690/1626] flex copy directive --- src/angular/directives/flex-copy.directive.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/angular/directives/flex-copy.directive.ts diff --git a/src/angular/directives/flex-copy.directive.ts b/src/angular/directives/flex-copy.directive.ts new file mode 100644 index 0000000000..5b8636f0ab --- /dev/null +++ b/src/angular/directives/flex-copy.directive.ts @@ -0,0 +1,28 @@ +import { + Directive, + ElementRef, + HostListener, +} from '@angular/core'; + +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +@Directive({ + selector: '[appFlexCopy]', +}) +export class FlexCopyDirective { + constructor(private el: ElementRef, private platformUtilsService: PlatformUtilsService) { } + + @HostListener('copy') onCopy() { + if (window == null) { + return; + } + let copyText = ''; + const selection = window.getSelection(); + for (let i = 0; i < selection.rangeCount; i++) { + const range = selection.getRangeAt(i); + const text = range.toString(); + copyText += text; + } + this.platformUtilsService.copyToClipboard(copyText, { window: window }); + } +} From 62b074ae2269c72c08df64d7d2fa83ecbd00b723 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 24 Jan 2019 12:04:42 -0500 Subject: [PATCH 0691/1626] fix grouping first --- src/importers/lastpassCsvImporter.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/importers/lastpassCsvImporter.ts b/src/importers/lastpassCsvImporter.ts index cbfebfec41..a78b23c6c5 100644 --- a/src/importers/lastpassCsvImporter.ts +++ b/src/importers/lastpassCsvImporter.ts @@ -25,12 +25,16 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { results.forEach((value, index) => { const cipherIndex = result.ciphers.length; let folderIndex = result.folders.length; - const hasFolder = this.getValueOrDefault(value.grouping, '(none)') !== '(none)'; + let grouping = value.grouping; + if (grouping != null) { + grouping = grouping.replace(/\\/g, '/'); + } + const hasFolder = this.getValueOrDefault(grouping, '(none)') !== '(none)'; let addFolder = hasFolder; if (hasFolder) { for (let i = 0; i < result.folders.length; i++) { - if (result.folders[i].name === value.grouping) { + if (result.folders[i].name === grouping) { addFolder = false; folderIndex = i; break; @@ -67,10 +71,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { if (addFolder) { const f = new FolderView(); - f.name = value.grouping; - if (f.name != null) { - f.name = f.name.replace(/\\/g, '/'); - } + f.name = grouping; result.folders.push(f); } if (hasFolder) { From b01709240e4fc0674caea4edfebe5e46249c2bd2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 25 Jan 2019 09:30:21 -0500 Subject: [PATCH 0692/1626] init view properties --- src/models/view/attachmentView.ts | 12 ++++----- src/models/view/cardView.ts | 10 +++---- src/models/view/cipherView.ts | 32 +++++++++++------------ src/models/view/collectionView.ts | 8 +++--- src/models/view/fieldView.ts | 6 ++--- src/models/view/folderView.ts | 4 +-- src/models/view/identityView.ts | 36 +++++++++++++------------- src/models/view/loginUriView.ts | 8 +++--- src/models/view/loginView.ts | 10 +++---- src/models/view/passwordHistoryView.ts | 4 +-- src/models/view/secureNoteView.ts | 2 +- src/services/import.service.ts | 6 ++++- src/services/search.service.ts | 4 +-- 13 files changed, 73 insertions(+), 69 deletions(-) diff --git a/src/models/view/attachmentView.ts b/src/models/view/attachmentView.ts index aff342c9f8..43f0716763 100644 --- a/src/models/view/attachmentView.ts +++ b/src/models/view/attachmentView.ts @@ -4,12 +4,12 @@ import { Attachment } from '../domain/attachment'; import { SymmetricCryptoKey } from '../domain/symmetricCryptoKey'; export class AttachmentView implements View { - id: string; - url: string; - size: number; - sizeName: string; - fileName: string; - key: SymmetricCryptoKey; + id: string = null; + url: string = null; + size: number = null; + sizeName: string = null; + fileName: string = null; + key: SymmetricCryptoKey = null; constructor(a?: Attachment) { if (!a) { diff --git a/src/models/view/cardView.ts b/src/models/view/cardView.ts index df3497c96c..3add0356b6 100644 --- a/src/models/view/cardView.ts +++ b/src/models/view/cardView.ts @@ -3,15 +3,15 @@ import { View } from './view'; import { Card } from '../domain/card'; export class CardView implements View { - cardholderName: string; + cardholderName: string = null; expMonth: string = null; - expYear: string; - code: string; + expYear: string = null; + code: string = null; // tslint:disable private _brand: string = null; - private _number: string; - private _subTitle: string; + private _number: string = null; + private _subTitle: string = null; // tslint:enable constructor(c?: Card) { diff --git a/src/models/view/cipherView.ts b/src/models/view/cipherView.ts index b231be4f28..14feea8c42 100644 --- a/src/models/view/cipherView.ts +++ b/src/models/view/cipherView.ts @@ -12,25 +12,25 @@ import { SecureNoteView } from './secureNoteView'; import { View } from './view'; export class CipherView implements View { - id: string; - organizationId: string; - folderId: string; - name: string; - notes: string; - type: CipherType; + id: string = null; + organizationId: string = null; + folderId: string = null; + name: string = null; + notes: string = null; + type: CipherType = null; favorite = false; organizationUseTotp = false; edit = false; localData: any; - login: LoginView; - identity: IdentityView; - card: CardView; - secureNote: SecureNoteView; - attachments: AttachmentView[]; - fields: FieldView[]; - passwordHistory: PasswordHistoryView[]; - collectionIds: string[]; - revisionDate: Date; + login = new LoginView(); + identity = new IdentityView(); + card = new CardView(); + secureNote = new SecureNoteView(); + attachments: AttachmentView[] = null; + fields: FieldView[] = null; + passwordHistory: PasswordHistoryView[] = null; + collectionIds: string[] = null; + revisionDate: Date = null; constructor(c?: Cipher) { if (!c) { @@ -90,7 +90,7 @@ export class CipherView implements View { } get passwordRevisionDisplayDate(): Date { - if (this.login == null) { + if (this.type !== CipherType.Login || this.login == null) { return null; } else if (this.login.password == null || this.login.password === '') { return null; diff --git a/src/models/view/collectionView.ts b/src/models/view/collectionView.ts index f667f742db..7f4e52ed90 100644 --- a/src/models/view/collectionView.ts +++ b/src/models/view/collectionView.ts @@ -4,10 +4,10 @@ import { Collection } from '../domain/collection'; import { ITreeNodeObject } from '../domain/treeNode'; export class CollectionView implements View, ITreeNodeObject { - id: string; - organizationId: string; - name: string; - readOnly: boolean; + id: string = null; + organizationId: string = null; + name: string = null; + readOnly: boolean = null; constructor(c?: Collection) { if (!c) { diff --git a/src/models/view/fieldView.ts b/src/models/view/fieldView.ts index e09d60960e..20d695da15 100644 --- a/src/models/view/fieldView.ts +++ b/src/models/view/fieldView.ts @@ -5,9 +5,9 @@ import { View } from './view'; import { Field } from '../domain/field'; export class FieldView implements View { - name: string; - value: string; - type: FieldType; + name: string = null; + value: string = null; + type: FieldType = null; constructor(f?: Field) { if (!f) { diff --git a/src/models/view/folderView.ts b/src/models/view/folderView.ts index a61091b2fd..1446a972bd 100644 --- a/src/models/view/folderView.ts +++ b/src/models/view/folderView.ts @@ -5,8 +5,8 @@ import { ITreeNodeObject } from '../domain/treeNode'; export class FolderView implements View, ITreeNodeObject { id: string = null; - name: string; - revisionDate: Date; + name: string = null; + revisionDate: Date = null; constructor(f?: Folder) { if (!f) { diff --git a/src/models/view/identityView.ts b/src/models/view/identityView.ts index 246ed7a763..b6adae4d69 100644 --- a/src/models/view/identityView.ts +++ b/src/models/view/identityView.ts @@ -6,26 +6,26 @@ import { Utils } from '../../misc/utils'; export class IdentityView implements View { title: string = null; - middleName: 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; + middleName: string = null; + address1: string = null; + address2: string = null; + address3: string = null; + city: string = null; + state: string = null; + postalCode: string = null; + country: string = null; + company: string = null; + email: string = null; + phone: string = null; + ssn: string = null; + username: string = null; + passportNumber: string = null; + licenseNumber: string = null; // tslint:disable - private _firstName: string; - private _lastName: string; - private _subTitle: string; + private _firstName: string = null; + private _lastName: string = null; + private _subTitle: string = null; // tslint:enable constructor(i?: Identity) { diff --git a/src/models/view/loginUriView.ts b/src/models/view/loginUriView.ts index 70698ca33d..84e37176c1 100644 --- a/src/models/view/loginUriView.ts +++ b/src/models/view/loginUriView.ts @@ -20,10 +20,10 @@ export class LoginUriView implements View { match: UriMatchType = null; // tslint:disable - private _uri: string; - private _domain: string; - private _hostname: string; - private _canLaunch: boolean; + private _uri: string = null; + private _domain: string = null; + private _hostname: string = null; + private _canLaunch: boolean = null; // tslint:enable constructor(u?: LoginUri) { diff --git a/src/models/view/loginView.ts b/src/models/view/loginView.ts index 822a9361d3..8f4173ea92 100644 --- a/src/models/view/loginView.ts +++ b/src/models/view/loginView.ts @@ -4,11 +4,11 @@ import { View } from './view'; import { Login } from '../domain/login'; export class LoginView implements View { - username: string; - password: string; - passwordRevisionDate?: Date; - totp: string; - uris: LoginUriView[]; + username: string = null; + password: string = null; + passwordRevisionDate?: Date = null; + totp: string = null; + uris: LoginUriView[] = null; constructor(l?: Login) { if (!l) { diff --git a/src/models/view/passwordHistoryView.ts b/src/models/view/passwordHistoryView.ts index 946424ffa9..5a0ca0e6c9 100644 --- a/src/models/view/passwordHistoryView.ts +++ b/src/models/view/passwordHistoryView.ts @@ -3,8 +3,8 @@ import { View } from './view'; import { Password } from '../domain/password'; export class PasswordHistoryView implements View { - password: string; - lastUsedDate: Date; + password: string = null; + lastUsedDate: Date = null; constructor(ph?: Password) { if (!ph) { diff --git a/src/models/view/secureNoteView.ts b/src/models/view/secureNoteView.ts index 1047c41086..6bd4cde70c 100644 --- a/src/models/view/secureNoteView.ts +++ b/src/models/view/secureNoteView.ts @@ -5,7 +5,7 @@ import { View } from './view'; import { SecureNote } from '../domain/secureNote'; export class SecureNoteView implements View { - type: SecureNoteType; + type: SecureNoteType = null; constructor(n?: SecureNote) { if (!n) { diff --git a/src/services/import.service.ts b/src/services/import.service.ts index 32a7a625c8..72e12181a5 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -10,6 +10,10 @@ import { import { ImportResult } from '../models/domain/importResult'; +import { CipherType } from '../enums/cipherType'; + +import { Utils } from '../misc/utils'; + import { CipherRequest } from '../models/request/cipherRequest'; import { CollectionRequest } from '../models/request/collectionRequest'; import { FolderRequest } from '../models/request/folderRequest'; @@ -278,6 +282,6 @@ export class ImportService implements ImportServiceAbstraction { private badData(c: CipherView) { return (c.name == null || c.name === '--') && - (c.login != null && (c.login.password == null || c.login.password === '')); + (c.type === CipherType.Login && c.login != null && Utils.isNullOrWhitespace(c.login.password)); } } diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 5c5c5aa51f..e279c22670 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -54,7 +54,7 @@ export class SearchService implements SearchServiceAbstraction { }); builder.field('notes'); (builder as any).field('login.username', { - extractor: (c: CipherView) => c.login != null ? c.login.username : null, + extractor: (c: CipherView) => c.type === CipherType.Login && c.login != null ? c.login.username : null, }); (builder as any).field('login.uris', { boost: 2, extractor: (c: CipherView) => this.uriExtractor(c) }); (builder as any).field('fields', { extractor: (c: CipherView) => this.fieldExtractor(c, false) }); @@ -198,7 +198,7 @@ export class SearchService implements SearchServiceAbstraction { } private uriExtractor(c: CipherView) { - if (c.login == null || !c.login.hasUris) { + if (c.type !== CipherType.Login || c.login == null || !c.login.hasUris) { return null; } const uris: string[] = []; From d9f68b35a67d225b0a52201f47cba2d36ae4d527 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 26 Jan 2019 21:30:53 -0500 Subject: [PATCH 0693/1626] escape $ --- src/services/i18n.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/i18n.service.ts b/src/services/i18n.service.ts index b826f97234..345bf4b5ac 100644 --- a/src/services/i18n.service.ts +++ b/src/services/i18n.service.ts @@ -97,7 +97,7 @@ export class I18nService implements I18nServiceAbstraction { const replaceToken = '\\$' + placeProp.toUpperCase() + '\\$'; let replaceContent = locales[prop].placeholders[placeProp].content; if (replaceContent === '$1' || replaceContent === '$2' || replaceContent === '$3') { - replaceContent = '__' + replaceContent + '__'; + replaceContent = '__$' + replaceContent + '__'; } messagesObj[prop] = messagesObj[prop].replace(new RegExp(replaceToken, 'g'), replaceContent); } From fa5b3ea0eb066eebec79bcc14af45dca53e0f556 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sun, 27 Jan 2019 17:43:29 -0500 Subject: [PATCH 0694/1626] add missing secure note type --- src/importers/dashlaneJsonImporter.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/importers/dashlaneJsonImporter.ts b/src/importers/dashlaneJsonImporter.ts index 12241c34ba..fdf2ebf0fc 100644 --- a/src/importers/dashlaneJsonImporter.ts +++ b/src/importers/dashlaneJsonImporter.ts @@ -9,6 +9,7 @@ import { IdentityView } from '../models/view/identityView'; import { SecureNoteView } from '../models/view/secureNoteView'; import { CipherType } from '../enums/cipherType'; +import { SecureNoteType } from '../enums/secureNoteType'; const HandledResults = new Set(['ADDRESS', 'AUTHENTIFIANT', 'BANKSTATEMENT', 'IDCARD', 'IDENTITY', 'PAYMENTMEANS_CREDITCARD', 'PAYMENTMEAN_PAYPAL', 'EMAIL']); @@ -143,6 +144,7 @@ export class DashlaneJsonImporter extends BaseImporter implements Importer { const cipher = new CipherView(); cipher.secureNote = new SecureNoteView(); cipher.type = CipherType.SecureNote; + cipher.secureNote.type = SecureNoteType.Generic; if (name != null) { cipher.name = name; } else { From d79865fce265b50511d25cf7c5385ce3b5624bc6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 28 Jan 2019 09:20:51 -0500 Subject: [PATCH 0695/1626] added kaspersky importer --- src/importers/kasperskyTxtImporter.ts | 124 ++++++++++++++++++++++++++ src/services/import.service.ts | 4 + 2 files changed, 128 insertions(+) create mode 100644 src/importers/kasperskyTxtImporter.ts diff --git a/src/importers/kasperskyTxtImporter.ts b/src/importers/kasperskyTxtImporter.ts new file mode 100644 index 0000000000..895c693950 --- /dev/null +++ b/src/importers/kasperskyTxtImporter.ts @@ -0,0 +1,124 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +const NotesHeader = 'Notes\n\n'; +const ApplicationsHeader = 'Applications\n\n'; +const WebsitesHeader = 'Websites\n\n'; +const Delimiter = '\n---\n'; + +export class KasperskyTxtImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + + let notesData: string; + let applicationsData: string; + let websitesData: string; + let workingData = this.splitNewLine(data).join('\n'); + + if (workingData.indexOf(NotesHeader) !== -1) { + const parts = workingData.split(NotesHeader); + if (parts.length > 1) { + workingData = parts[0]; + notesData = parts[1]; + } + } + if (workingData.indexOf(ApplicationsHeader) !== -1) { + const parts = workingData.split(ApplicationsHeader); + if (parts.length > 1) { + workingData = parts[0]; + applicationsData = parts[1]; + } + } + if (workingData.indexOf(WebsitesHeader) === 0) { + const parts = workingData.split(WebsitesHeader); + if (parts.length > 1) { + workingData = parts[0]; + websitesData = parts[1]; + } + } + + const notes = this.parseDataCategory(notesData); + const applications = this.parseDataCategory(applicationsData); + const websites = this.parseDataCategory(websitesData); + + notes.forEach((n) => { + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(n.get('Name')); + cipher.notes = this.getValueOrDefault(n.get('Text')); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + websites.concat(applications).forEach((w) => { + const cipher = this.initLoginCipher(); + const nameKey = w.has('Website name') ? 'Website name' : 'Application'; + cipher.name = this.getValueOrDefault(w.get(nameKey), ''); + if (!this.isNullOrWhitespace(w.get('Login name'))) { + if (!this.isNullOrWhitespace(cipher.name)) { + cipher.name += ': '; + } + cipher.name += w.get('Login name'); + } + cipher.notes = this.getValueOrDefault(w.get('Comment')); + if (w.has('Website URL')) { + cipher.login.uris = this.makeUriArray(w.get('Website URL')); + } + cipher.login.username = this.getValueOrDefault(w.get('Login')); + cipher.login.password = this.getValueOrDefault(w.get('Password')); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } + + private parseDataCategory(data: string): Array> { + if (this.isNullOrWhitespace(data) || data.indexOf(Delimiter) === -1) { + return []; + } + const items: Array> = []; + data.split(Delimiter).forEach((p) => { + if (p.indexOf('\n') === -1) { + return; + } + const item = new Map(); + let itemComment: string; + let itemCommentKey: string; + p.split('\n').forEach((l) => { + if (itemComment != null) { + itemComment += ('\n' + l); + return; + } + const colonIndex = l.indexOf(':'); + let key: string; + let val: string; + if (colonIndex === -1) { + return; + } else { + key = l.substring(0, colonIndex); + if (l.length > colonIndex + 1) { + val = l.substring(colonIndex + 2); + } + } + if (key != null) { + item.set(key, val); + } + if (key === 'Comment' || key === 'Text') { + itemComment = val; + itemCommentKey = key; + } + }); + if (itemComment != null && itemCommentKey != null) { + item.set(itemCommentKey, itemComment); + } + if (item.size === 0) { + return; + } + items.push(item); + }); + return items; + } +} diff --git a/src/services/import.service.ts b/src/services/import.service.ts index 72e12181a5..6f39bf52c6 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -38,6 +38,7 @@ import { FirefoxCsvImporter } from '../importers/firefoxCsvImporter'; import { FSecureFskImporter } from '../importers/fsecureFskImporter'; import { GnomeJsonImporter } from '../importers/gnomeJsonImporter'; import { Importer } from '../importers/importer'; +import { KasperskyTxtImporter } from '../importers/kasperskyTxtImporter'; import { KeePass2XmlImporter } from '../importers/keepass2XmlImporter'; import { KeePassXCsvImporter } from '../importers/keepassxCsvImporter'; import { KeeperCsvImporter } from '../importers/keeperCsvImporter'; @@ -109,6 +110,7 @@ export class ImportService implements ImportServiceAbstraction { { id: 'passmanjson', name: 'Passman (json)' }, { id: 'avastcsv', name: 'Avast Passwords (csv)' }, { id: 'fsecurefsk', name: 'F-Secure KEY (fsk)' }, + { id: 'kasperskytxt', name: 'Kaspersky Password Manager (txt)' }, ]; constructor(private cipherService: CipherService, private folderService: FolderService, @@ -235,6 +237,8 @@ export class ImportService implements ImportServiceAbstraction { return new AvastCsvImporter(); case 'fsecurefsk': return new FSecureFskImporter(); + case 'kasperskytxt': + return new KasperskyTxtImporter(); default: return null; } From 9e97b1e65641452ec8ae2d2e1fca4bfddcd769b7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 28 Jan 2019 11:06:28 -0500 Subject: [PATCH 0696/1626] add each token as a search term --- src/services/search.service.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index e279c22670..550a6ab933 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -111,11 +111,12 @@ export class SearchService implements SearchServiceAbstraction { // tslint:disable-next-line const soWild = lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING; searchResults = index.query((q) => { - q.term(query, { fields: ['name'], wildcard: soWild }); - q.term(query, { fields: ['subtitle'], wildcard: soWild }); - q.term(query, { fields: ['login.uris'], wildcard: soWild }); lunr.tokenizer(query).forEach((token) => { - q.term(token.toString(), {}); + const t = token.toString(); + q.term(t, { fields: ['name'], wildcard: soWild }); + q.term(t, { fields: ['subtitle'], wildcard: soWild }); + q.term(t, { fields: ['login.uris'], wildcard: soWild }); + q.term(t, {}); }); }); } From e7f4dccfc3de2e3829ee09a5e9b439c9c52660f9 Mon Sep 17 00:00:00 2001 From: Fred Cox Date: Sat, 2 Feb 2019 16:23:16 +0200 Subject: [PATCH 0697/1626] Clear sequentialize cache when empty to remove chance of memory leaks (#26) --- spec/common/misc/sequentialize.spec.ts | 4 ++-- src/misc/sequentialize.ts | 17 ++++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/spec/common/misc/sequentialize.spec.ts b/spec/common/misc/sequentialize.spec.ts index 8bb2701cb2..86e846b38e 100644 --- a/spec/common/misc/sequentialize.spec.ts +++ b/spec/common/misc/sequentialize.spec.ts @@ -120,7 +120,7 @@ class Foo { calls = 0; @sequentialize((args) => 'bar' + args[0]) - bar(a: any) { + bar(a: number) { this.calls++; return new Promise((res) => { setTimeout(() => { @@ -130,7 +130,7 @@ class Foo { } @sequentialize((args) => 'baz' + args[0]) - baz(a: any) { + baz(a: number) { this.calls++; return new Promise((res) => { setTimeout(() => { diff --git a/src/misc/sequentialize.ts b/src/misc/sequentialize.ts index b9501a8ad2..221b9da23b 100644 --- a/src/misc/sequentialize.ts +++ b/src/misc/sequentialize.ts @@ -6,7 +6,6 @@ * * Results are not cached, once the promise has returned, the next call will result in a fresh call * - * WARNING: The decorator's scope is singleton, so using it on transient objects can lead to memory leaks. * Read more at https://github.com/bitwarden/jslib/pull/7 */ export function sequentialize(cacheKey: (args: any[]) => string) { @@ -26,20 +25,20 @@ export function sequentialize(cacheKey: (args: any[]) => string) { return { value: function(...args: any[]) { - const argsCacheKey = cacheKey(args); const cache = getCache(this); + const argsCacheKey = cacheKey(args); let response = cache.get(argsCacheKey); if (response != null) { return response; } - response = originalMethod.apply(this, args).then((val: any) => { - cache.delete(argsCacheKey); - return val; - }).catch((err: any) => { - cache.delete(argsCacheKey); - throw err; - }); + response = originalMethod.apply(this, args) + .finally(() => { + cache.delete(argsCacheKey); + if (cache.size === 0) { + caches.delete(this); + } + }); cache.set(argsCacheKey, response); return response; From ff0e1667554ef237833894e187552b585ebe666c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 2 Feb 2019 10:06:44 -0500 Subject: [PATCH 0698/1626] formatting --- src/misc/sequentialize.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/misc/sequentialize.ts b/src/misc/sequentialize.ts index 221b9da23b..ab64074472 100644 --- a/src/misc/sequentialize.ts +++ b/src/misc/sequentialize.ts @@ -32,13 +32,12 @@ export function sequentialize(cacheKey: (args: any[]) => string) { return response; } - response = originalMethod.apply(this, args) - .finally(() => { - cache.delete(argsCacheKey); - if (cache.size === 0) { - caches.delete(this); - } - }); + response = originalMethod.apply(this, args).finally(() => { + cache.delete(argsCacheKey); + if (cache.size === 0) { + caches.delete(this); + } + }); cache.set(argsCacheKey, response); return response; From db37a831e438bf6411e6f10c10af29c70e8f23b3 Mon Sep 17 00:00:00 2001 From: Fred Cox Date: Sat, 2 Feb 2019 17:17:44 +0200 Subject: [PATCH 0699/1626] Throttle calls to HIBP api (#25) Randomly failing to check by passwords, I'm pretty sure its because ~2000 connections are made at the same time. --- spec/common/misc/throttle.spec.ts | 110 ++++++++++++++++++++++++++++++ src/misc/throttle.ts | 57 ++++++++++++++++ src/services/audit.service.ts | 2 + 3 files changed, 169 insertions(+) create mode 100644 spec/common/misc/throttle.spec.ts create mode 100644 src/misc/throttle.ts diff --git a/spec/common/misc/throttle.spec.ts b/spec/common/misc/throttle.spec.ts new file mode 100644 index 0000000000..3e5a988341 --- /dev/null +++ b/spec/common/misc/throttle.spec.ts @@ -0,0 +1,110 @@ +import { throttle } from '../../../src/misc/throttle'; +import { sequentialize } from '../../../src/misc/sequentialize'; + +describe('throttle decorator', () => { + it('should call the function once at a time', async () => { + const foo = new Foo(); + const promises = []; + for (let i = 0; i < 10; i++) { + promises.push(foo.bar(1)); + } + await Promise.all(promises); + + expect(foo.calls).toBe(10); + }); + + it('should call the function once at a time for each object', async () => { + const foo = new Foo(); + const foo2 = new Foo(); + const promises = []; + for (let i = 0; i < 10; i++) { + promises.push(foo.bar(1)); + promises.push(foo2.bar(1)); + } + await Promise.all(promises); + + expect(foo.calls).toBe(10); + expect(foo2.calls).toBe(10); + }); + + it('should call the function limit at a time', async () => { + const foo = new Foo(); + const promises = []; + for (let i = 0; i < 10; i++) { + promises.push(foo.baz(1)); + } + await Promise.all(promises); + + expect(foo.calls).toBe(10); + }); + + it('should call the function limit at a time for each object', async () => { + const foo = new Foo(); + const foo2 = new Foo(); + const promises = []; + for (let i = 0; i < 10; i++) { + promises.push(foo.baz(1)); + promises.push(foo2.baz(1)); + } + await Promise.all(promises); + + expect(foo.calls).toBe(10); + expect(foo2.calls).toBe(10); + }); + + it('should work together with sequentialize', async () => { + const foo = new Foo(); + const promises = []; + for (let i = 0; i < 10; i++) { + promises.push(foo.qux(Math.floor(i / 2) * 2)); + } + await Promise.all(promises); + + expect(foo.calls).toBe(5); + }); +}); + +class Foo { + calls = 0; + inflight = 0; + + @throttle(1, () => 'bar') + bar(a: number) { + this.calls++; + this.inflight++; + return new Promise((res) => { + setTimeout(() => { + expect(this.inflight).toBe(1); + this.inflight--; + res(a * 2); + }, Math.random() * 10); + }); + } + + @throttle(5, () => 'baz') + baz(a: number) { + this.calls++; + this.inflight++; + return new Promise((res) => { + setTimeout(() => { + expect(this.inflight).toBeLessThanOrEqual(5); + this.inflight--; + res(a * 3); + }, Math.random() * 10); + }); + } + + @sequentialize((args) => 'qux' + args[0]) + @throttle(1, () => 'qux') + qux(a: number) { + this.calls++; + this.inflight++; + return new Promise((res) => { + setTimeout(() => { + expect(this.inflight).toBe(1); + this.inflight--; + res(a * 3); + }, Math.random() * 10); + }); + } +} diff --git a/src/misc/throttle.ts b/src/misc/throttle.ts new file mode 100644 index 0000000000..ace5cc0e7e --- /dev/null +++ b/src/misc/throttle.ts @@ -0,0 +1,57 @@ +/** + * Use as a Decorator on async functions, it will limit how many times the function can be + * in-flight at a time. + * + * Calls beyond the limit will be queued, and run when one of the active calls finishes + */ +export function throttle(limit: number, throttleKey: (args: any[]) => string) { + return (target: any, propertyKey: string | symbol, + descriptor: TypedPropertyDescriptor<(...args: any[]) => Promise>) => { + const originalMethod: () => Promise = descriptor.value; + const allThrottles = new Map void>>>(); + + const getThrottles = (obj: any) => { + let throttles = allThrottles.get(obj); + if (throttles != null) { + return throttles; + } + throttles = new Map void>>(); + allThrottles.set(obj, throttles); + return throttles; + }; + + return { + value: function(...args: any[]) { + const throttles = getThrottles(this); + const argsThrottleKey = throttleKey(args); + let queue = throttles.get(argsThrottleKey); + if (!queue) { + queue = []; + throttles.set(argsThrottleKey, queue); + } + + return new Promise((resolve, reject) => { + const exec = () => { + originalMethod.apply(this, args) + .finally(() => { + queue.splice(queue.indexOf(exec), 1); + if (queue.length >= limit) { + queue[limit - 1](); + } else if (queue.length === 0) { + throttles.delete(argsThrottleKey); + if (throttles.size === 0) { + allThrottles.delete(this); + } + } + }) + .then(resolve, reject); + }; + queue.push(exec); + if (queue.length <= limit) { + exec(); + } + }); + }, + }; + }; +} diff --git a/src/services/audit.service.ts b/src/services/audit.service.ts index 7939149299..7b821afff4 100644 --- a/src/services/audit.service.ts +++ b/src/services/audit.service.ts @@ -2,6 +2,7 @@ import { ApiService } from '../abstractions/api.service'; import { AuditService as AuditServiceAbstraction } from '../abstractions/audit.service'; import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; +import { throttle } from '../misc/throttle'; import { Utils } from '../misc/utils'; import { BreachAccountResponse } from '../models/response/breachAccountResponse'; @@ -12,6 +13,7 @@ const PwnedPasswordsApi = 'https://api.pwnedpasswords.com/range/'; export class AuditService implements AuditServiceAbstraction { constructor(private cryptoFunctionService: CryptoFunctionService, private apiService: ApiService) { } + @throttle(100, () => 'passwordLeaked') async passwordLeaked(password: string): Promise { const hashBytes = await this.cryptoFunctionService.hash(password, 'sha1'); const hash = Utils.fromBufferToHex(hashBytes).toUpperCase(); From 4634dd2e8368d9fa9396483e1543192b73199fcd Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 2 Feb 2019 12:08:53 -0500 Subject: [PATCH 0700/1626] formatting --- src/misc/throttle.ts | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/misc/throttle.ts b/src/misc/throttle.ts index ace5cc0e7e..48f3b879ab 100644 --- a/src/misc/throttle.ts +++ b/src/misc/throttle.ts @@ -25,26 +25,24 @@ export function throttle(limit: number, throttleKey: (args: any[]) => string) { const throttles = getThrottles(this); const argsThrottleKey = throttleKey(args); let queue = throttles.get(argsThrottleKey); - if (!queue) { + if (queue == null) { queue = []; throttles.set(argsThrottleKey, queue); } return new Promise((resolve, reject) => { const exec = () => { - originalMethod.apply(this, args) - .finally(() => { - queue.splice(queue.indexOf(exec), 1); - if (queue.length >= limit) { - queue[limit - 1](); - } else if (queue.length === 0) { - throttles.delete(argsThrottleKey); - if (throttles.size === 0) { - allThrottles.delete(this); - } + originalMethod.apply(this, args).finally(() => { + queue.splice(queue.indexOf(exec), 1); + if (queue.length >= limit) { + queue[limit - 1](); + } else if (queue.length === 0) { + throttles.delete(argsThrottleKey); + if (throttles.size === 0) { + allThrottles.delete(this); } - }) - .then(resolve, reject); + } + }).then(resolve, reject); }; queue.push(exec); if (queue.length <= limit) { From 9795f02b74adda981153ac7776a5413306a484be Mon Sep 17 00:00:00 2001 From: h44z Date: Sat, 2 Feb 2019 18:13:30 +0100 Subject: [PATCH 0701/1626] Allow app to start minimized (as tray icon) (#22) --- src/electron/electronConstants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/electron/electronConstants.ts b/src/electron/electronConstants.ts index 8887e6142e..431e9eaf0f 100644 --- a/src/electron/electronConstants.ts +++ b/src/electron/electronConstants.ts @@ -2,4 +2,5 @@ export class ElectronConstants { static readonly enableMinimizeToTrayKey: string = 'enableMinimizeToTray'; static readonly enableCloseToTrayKey: string = 'enableCloseToTray'; static readonly enableTrayKey: string = 'enableTray'; + static readonly enableStartMinimizedKey: string = 'enableStartMinimizedKey'; } From 04e6fac5e46b4ab1ab62592c6654396f384bdf76 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 2 Feb 2019 12:26:46 -0500 Subject: [PATCH 0702/1626] start to tray updates --- src/electron/electronConstants.ts | 2 +- src/misc/sequentialize.ts | 2 +- src/misc/throttle.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/electron/electronConstants.ts b/src/electron/electronConstants.ts index 431e9eaf0f..3226486d34 100644 --- a/src/electron/electronConstants.ts +++ b/src/electron/electronConstants.ts @@ -2,5 +2,5 @@ export class ElectronConstants { static readonly enableMinimizeToTrayKey: string = 'enableMinimizeToTray'; static readonly enableCloseToTrayKey: string = 'enableCloseToTray'; static readonly enableTrayKey: string = 'enableTray'; - static readonly enableStartMinimizedKey: string = 'enableStartMinimizedKey'; + static readonly enableStartToTrayKey: string = 'enableStartToTrayKey'; } diff --git a/src/misc/sequentialize.ts b/src/misc/sequentialize.ts index ab64074472..de223e2e25 100644 --- a/src/misc/sequentialize.ts +++ b/src/misc/sequentialize.ts @@ -24,7 +24,7 @@ export function sequentialize(cacheKey: (args: any[]) => string) { }; return { - value: function(...args: any[]) { + value: function (...args: any[]) { const cache = getCache(this); const argsCacheKey = cacheKey(args); let response = cache.get(argsCacheKey); diff --git a/src/misc/throttle.ts b/src/misc/throttle.ts index 48f3b879ab..d4fc553f06 100644 --- a/src/misc/throttle.ts +++ b/src/misc/throttle.ts @@ -6,7 +6,7 @@ */ export function throttle(limit: number, throttleKey: (args: any[]) => string) { return (target: any, propertyKey: string | symbol, - descriptor: TypedPropertyDescriptor<(...args: any[]) => Promise>) => { + descriptor: TypedPropertyDescriptor<(...args: any[]) => Promise>) => { const originalMethod: () => Promise = descriptor.value; const allThrottles = new Map void>>>(); @@ -21,7 +21,7 @@ export function throttle(limit: number, throttleKey: (args: any[]) => string) { }; return { - value: function(...args: any[]) { + value: function (...args: any[]) { const throttles = getThrottles(this); const argsThrottleKey = throttleKey(args); let queue = throttles.get(argsThrottleKey); From d1f2932f1cf22ceb400ec4dc4c6f4c1a685e43cc Mon Sep 17 00:00:00 2001 From: ShirokaiLon <6662937+ShirokaiLon@users.noreply.github.com> Date: Sun, 3 Feb 2019 03:29:29 +0000 Subject: [PATCH 0703/1626] Add Angular trackBy function (#27) --- src/angular/components/add-edit.component.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 837898a1c2..c9f5102c96 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -264,6 +264,10 @@ export class AddEditComponent implements OnInit { } } + trackByFunction(index: number, uri: LoginUriView) { + return index; + } + cancel() { this.onCancelled.emit(this.cipher); } From cdbe08ae7eb9f0b127ebc2e28de287ff8096aebc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 2 Feb 2019 22:33:28 -0500 Subject: [PATCH 0704/1626] item is any --- src/angular/components/add-edit.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index c9f5102c96..8cd755cfa4 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -264,7 +264,7 @@ export class AddEditComponent implements OnInit { } } - trackByFunction(index: number, uri: LoginUriView) { + trackByFunction(index: number, item: any) { return index; } From dd46d5ecdd51f91dace5488272dd1f7bafd995c5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 7 Feb 2019 15:43:11 -0500 Subject: [PATCH 0705/1626] formatting fix --- src/misc/sequentialize.ts | 2 +- src/misc/throttle.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/misc/sequentialize.ts b/src/misc/sequentialize.ts index de223e2e25..ab64074472 100644 --- a/src/misc/sequentialize.ts +++ b/src/misc/sequentialize.ts @@ -24,7 +24,7 @@ export function sequentialize(cacheKey: (args: any[]) => string) { }; return { - value: function (...args: any[]) { + value: function(...args: any[]) { const cache = getCache(this); const argsCacheKey = cacheKey(args); let response = cache.get(argsCacheKey); diff --git a/src/misc/throttle.ts b/src/misc/throttle.ts index d4fc553f06..3a295ba69d 100644 --- a/src/misc/throttle.ts +++ b/src/misc/throttle.ts @@ -21,7 +21,7 @@ export function throttle(limit: number, throttleKey: (args: any[]) => string) { }; return { - value: function (...args: any[]) { + value: function(...args: any[]) { const throttles = getThrottles(this); const argsThrottleKey = throttleKey(args); let queue = throttles.get(argsThrottleKey); From 93244b5c90b02ec7a333ffafe8b32a6026c1c7d3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 7 Feb 2019 16:55:49 -0500 Subject: [PATCH 0706/1626] use jsdom for DOMParser --- package-lock.json | 411 ++++++++++++++---- package.json | 1 + .../importers/keepass2XmlImporter.spec.ts | 198 +++++++++ spec/support/karma.conf.js | 1 + 4 files changed, 528 insertions(+), 83 deletions(-) create mode 100644 spec/common/importers/keepass2XmlImporter.spec.ts diff --git a/package-lock.json b/package-lock.json index 9feb7bb90e..ee4d33fe8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -184,6 +184,11 @@ "integrity": "sha512-GQLOT+SN20a+AI51y3fAimhyTF4Y0RG+YP3gf91OibIZ7CJmPFgoZi+ZR5a+vRbS01LbQosITWum4ATmJ1Z6Pg==", "dev": true }, + "abab": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", + "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==" + }, "abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", @@ -206,6 +211,27 @@ "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", "dev": true }, + "acorn-globals": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.0.tgz", + "integrity": "sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw==", + "requires": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.7.tgz", + "integrity": "sha512-HNJNgE60C9eOTgn974Tlp3dpLZdUr+SoxxDwPaY9J/kDNOLQTkaDgwBUXAF4SSsrAwD9RpdxuHK/EbuF+W9Ahw==" + } + } + }, + "acorn-walk": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", + "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==" + }, "after": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", @@ -216,7 +242,6 @@ "version": "6.7.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz", "integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==", - "dev": true, "requires": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", @@ -377,6 +402,11 @@ "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=", "dev": true }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=" + }, "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", @@ -405,7 +435,6 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, "requires": { "safer-buffer": "~2.1.0" } @@ -450,8 +479,7 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, "assign-symbols": { "version": "1.0.0", @@ -477,8 +505,7 @@ "async-limiter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", - "dev": true + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" }, "asynckit": { "version": "0.4.0", @@ -494,14 +521,12 @@ "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, "babel-code-frame": { "version": "6.26.0", @@ -759,7 +784,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, "requires": { "tweetnacl": "^0.14.3" } @@ -989,6 +1013,11 @@ "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", "dev": true }, + "browser-process-hrtime": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", + "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==" + }, "browser-resolve": { "version": "1.11.3", "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", @@ -1223,8 +1252,7 @@ "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "center-align": { "version": "0.1.3", @@ -1705,6 +1733,19 @@ "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", "dev": true }, + "cssom": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz", + "integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A==" + }, + "cssstyle": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.1.1.tgz", + "integrity": "sha512-364AI1l/M5TYcFH83JnOH/pSqgaNnKmYgKrm0didZMGKWjQB60dymwWy1rKUgL3J1ffdq9xVi2yGLHdSjjSNog==", + "requires": { + "cssom": "0.3.x" + } + }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", @@ -1724,11 +1765,20 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, "requires": { "assert-plus": "^1.0.0" } }, + "data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "requires": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + } + }, "date-fns": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.29.0.tgz", @@ -1794,8 +1844,7 @@ "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" }, "default-require-extensions": { "version": "2.0.0", @@ -1957,6 +2006,14 @@ "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", "dev": true }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "requires": { + "webidl-conversions": "^4.0.2" + } + }, "dot-prop": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", @@ -1986,7 +2043,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -2254,8 +2310,7 @@ "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" }, "event-stream": { "version": "3.3.4", @@ -2413,8 +2468,7 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "extend-shallow": { "version": "1.1.4", @@ -2530,26 +2584,22 @@ "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, "fd-slicer": { "version": "1.0.1", @@ -2653,8 +2703,7 @@ "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { "version": "2.3.2", @@ -2761,14 +2810,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2788,8 +2835,7 @@ "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", @@ -2937,7 +2983,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3297,7 +3342,6 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, "requires": { "assert-plus": "^1.0.0" } @@ -3413,14 +3457,12 @@ "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, "requires": { "ajv": "^6.5.5", "har-schema": "^2.0.0" @@ -3574,6 +3616,14 @@ "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", "dev": true }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "requires": { + "whatwg-encoding": "^1.0.1" + } + }, "http-errors": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", @@ -3601,7 +3651,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -3905,8 +3954,7 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, "is-utf8": { "version": "0.2.1", @@ -3949,8 +3997,7 @@ "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "istanbul": { "version": "0.4.5", @@ -4168,8 +4215,91 @@ "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "jsdom": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-13.2.0.tgz", + "integrity": "sha512-cG1NtMWO9hWpqRNRR3dSvEQa8bFI6iLlqU2x4kwX51FQjp0qus8T9aBaAO6iGp3DeBrhdwuKxckknohkmfvsFw==", + "requires": { + "abab": "^2.0.0", + "acorn": "^6.0.4", + "acorn-globals": "^4.3.0", + "array-equal": "^1.0.0", + "cssom": "^0.3.4", + "cssstyle": "^1.1.1", + "data-urls": "^1.1.0", + "domexception": "^1.0.1", + "escodegen": "^1.11.0", + "html-encoding-sniffer": "^1.0.2", + "nwsapi": "^2.0.9", + "parse5": "5.1.0", + "pn": "^1.1.0", + "request": "^2.88.0", + "request-promise-native": "^1.0.5", + "saxes": "^3.1.5", + "symbol-tree": "^3.2.2", + "tough-cookie": "^2.5.0", + "w3c-hr-time": "^1.0.1", + "w3c-xmlserializer": "^1.0.1", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^7.0.0", + "ws": "^6.1.2", + "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "acorn": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.7.tgz", + "integrity": "sha512-HNJNgE60C9eOTgn974Tlp3dpLZdUr+SoxxDwPaY9J/kDNOLQTkaDgwBUXAF4SSsrAwD9RpdxuHK/EbuF+W9Ahw==" + }, + "escodegen": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.0.tgz", + "integrity": "sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw==", + "requires": { + "esprima": "^3.1.3", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "ws": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.3.tgz", + "integrity": "sha512-tbSxiT+qJI223AP4iLfQbkbxkwdFcneYinM2+x46Gx2wgvbaOMO36czfdfVUBRTHvzAMRhDd98sA5d/BuWbQdg==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } }, "jsesc": { "version": "1.3.0", @@ -4180,20 +4310,17 @@ "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "jsonfile": { "version": "4.0.0", @@ -4207,7 +4334,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -4464,7 +4590,6 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, "requires": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" @@ -4513,6 +4638,11 @@ "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=", "dev": true }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + }, "log4js": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/log4js/-/log4js-3.0.5.tgz", @@ -5437,11 +5567,15 @@ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, + "nwsapi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.0.tgz", + "integrity": "sha512-ZG3bLAvdHmhIjaQ/Db1qvBxsGvFMLIRpQszyqbg31VJ53UP++uZX1/gf3Ut96pdwN9AuDwlMqIYLm0UPCdUeHg==" + }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, "object-assign": { "version": "4.1.1", @@ -5555,7 +5689,6 @@ "version": "0.8.2", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, "requires": { "deep-is": "~0.1.3", "fast-levenshtein": "~2.0.4", @@ -5568,8 +5701,7 @@ "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" } } }, @@ -5650,6 +5782,11 @@ "error-ex": "^1.2.0" } }, + "parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==" + }, "parseqs": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", @@ -5775,8 +5912,7 @@ "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "pify": { "version": "3.0.0", @@ -5823,6 +5959,11 @@ } } }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==" + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -5860,8 +6001,7 @@ "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" }, "prepend-http": { "version": "1.0.4", @@ -5918,8 +6058,7 @@ "psl": { "version": "1.1.31", "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", - "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==", - "dev": true + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" }, "pstree.remy": { "version": "1.1.0", @@ -5966,8 +6105,7 @@ "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "querystring": { "version": "0.2.0", @@ -6225,7 +6363,6 @@ "version": "2.88.0", "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "dev": true, "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -6249,6 +6386,24 @@ "uuid": "^3.3.2" } }, + "request-promise-core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", + "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", + "requires": { + "lodash": "^4.13.1" + } + }, + "request-promise-native": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.5.tgz", + "integrity": "sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=", + "requires": { + "request-promise-core": "1.1.1", + "stealthy-require": "^1.1.0", + "tough-cookie": ">=2.3.3" + } + }, "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -6342,14 +6497,21 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, + "saxes": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.6.tgz", + "integrity": "sha512-LAYs+lChg1v5uKNzPtsgTxSS5hLo8aIhSMCJt1WMpefAxm3D1RTpMwSpb6ebdL31cubiLTnhokVktBW+cv9Y9w==", + "requires": { + "xmlchars": "^1.3.1" + } + }, "semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", @@ -6773,7 +6935,6 @@ "version": "1.16.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.0.tgz", "integrity": "sha512-Zhev35/y7hRMcID/upReIvRse+I9SVhyVre/KTJSJQWMz3C3+G+HpO7m1wK/yckEtujKZ7dS4hkVxAnmHaIGVQ==", - "dev": true, "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -6819,6 +6980,11 @@ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", "dev": true }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" + }, "steno": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/steno/-/steno-0.4.4.tgz", @@ -6987,6 +7153,11 @@ "has-flag": "^1.0.0" } }, + "symbol-tree": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", + "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=" + }, "tar-fs": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", @@ -7231,12 +7402,26 @@ "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, "requires": { "psl": "^1.1.24", "punycode": "^1.4.1" } }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } + } + }, "tree-kill": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.0.tgz", @@ -7349,14 +7534,12 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, "requires": { "prelude-ls": "~1.1.2" } @@ -7626,7 +7809,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, "requires": { "punycode": "^2.1.0" }, @@ -7634,8 +7816,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" } } }, @@ -7719,8 +7900,7 @@ "uuid": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "dev": true + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" }, "validate-npm-package-license": { "version": "3.0.4", @@ -7736,7 +7916,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -7758,6 +7937,24 @@ "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", "dev": true }, + "w3c-hr-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", + "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "requires": { + "browser-process-hrtime": "^0.1.2" + } + }, + "w3c-xmlserializer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.0.1.tgz", + "integrity": "sha512-XZGI1OH/OLQr/NaJhhPmzhngwcAnZDLytsvXnRmlYeRkmbb0I7sqFFA22erq4WQR0sUu17ZSQOAV9mFwCqKRNg==", + "requires": { + "domexception": "^1.0.1", + "webidl-conversions": "^4.0.2", + "xml-name-validator": "^3.0.0" + } + }, "wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -7767,6 +7964,44 @@ "defaults": "^1.0.3" } }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "requires": { + "iconv-lite": "0.4.24" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" + }, + "whatwg-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", + "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -7877,6 +8112,16 @@ "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", "dev": true }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" + }, + "xmlchars": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-1.3.1.tgz", + "integrity": "sha512-tGkGJkN8XqCod7OT+EvGYK5Z4SfDQGD30zAa58OcnAa0RRWgzUEK72tkXhsX1FZd+rgnhRxFtmO+ihkp8LHSkw==" + }, "xmlhttprequest-ssl": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", diff --git a/package.json b/package.json index 8692494aab..0b62df2db0 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "electron-log": "2.2.17", "electron-updater": "4.0.6", "form-data": "2.3.2", + "jsdom": "13.2.0", "keytar": "4.2.1", "lowdb": "1.0.0", "lunr": "2.3.3", diff --git a/spec/common/importers/keepass2XmlImporter.spec.ts b/spec/common/importers/keepass2XmlImporter.spec.ts new file mode 100644 index 0000000000..757b53e69c --- /dev/null +++ b/spec/common/importers/keepass2XmlImporter.spec.ts @@ -0,0 +1,198 @@ +import { KeePass2XmlImporter as Importer } from '../../../src/importers/keepass2XmlImporter'; + +import { Utils } from '../../../src/misc/utils'; + +if (Utils.isNode) { + // Polyfills + // tslint:disable-next-line + const jsdom: any = require('jsdom'); + (global as any).DOMParser = new jsdom.JSDOM().window.DOMParser; +} + +const TestData: string = ` + + + KeePass + + 2016-12-31T21:33:52Z + + 2016-12-31T21:33:52Z + + 2016-12-31T21:33:52Z + 365 + + 2016-12-31T21:33:59Z + -1 + -1 + + False + False + True + False + False + + True + AAAAAAAAAAAAAAAAAAAAAA== + 2016-12-31T21:33:52Z + AAAAAAAAAAAAAAAAAAAAAA== + 2016-12-31T21:33:52Z + 10 + 6291456 + AAAAAAAAAAAAAAAAAAAAAA== + AAAAAAAAAAAAAAAAAAAAAA== + + + + + + KvS57lVwl13AfGFLwkvq4Q== + Root + + 48 + + 2016-12-31T21:33:52Z + 2016-12-31T21:33:52Z + 2017-01-01T22:58:00Z + 2016-12-31T21:33:52Z + False + 1 + 2016-12-31T21:33:52Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + P0ParXgGMBW6caOL2YrhqQ== + Folder2 + a note about the folder + 48 + + 2016-12-31T21:43:30Z + 2016-12-31T21:43:43Z + 2017-01-01T22:58:00Z + 2016-12-31T21:43:30Z + False + 1 + 2016-12-31T21:43:43Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + fAa543oYlgnJKkhKag5HLw== + 1 + + + + + + 2016-12-31T21:34:13Z + 2016-12-31T21:40:23Z + 2016-12-31T21:40:23Z + 2016-12-31T21:34:13Z + False + 0 + 2016-12-31T21:43:48Z + + + att2 + att2value + + + attr1 + att1value + +line1 +line2 + + + Notes + This is a note!!! + +line1 +line2 + + + Password + googpass + + + Title + Google + + + URL + google.com + + + UserName + googleuser + + + True + 0 + + + + fAa543oYlgnJKkhKag5HLw== + 0 + + + + + + 2016-12-31T21:34:13Z + 2016-12-31T21:34:40Z + 2016-12-31T21:34:40Z + 2016-12-31T21:34:13Z + False + 0 + 2016-12-31T21:34:40Z + + + Notes + This is a note!!! + +line1 +line2 + + + Password + googpass + + + Title + Google + + + URL + google.com + + + UserName + googleuser + + + True + 0 + + + + + + + + +`; + +describe('KeePass2 Xml Importer', () => { + it('should parse XML data', async () => { + const importer = new Importer(); + const result = importer.parse(TestData); + expect(result != null).toBe(true); + }); +}); diff --git a/spec/support/karma.conf.js b/spec/support/karma.conf.js index d23eff033c..0e3870d8b9 100644 --- a/spec/support/karma.conf.js +++ b/spec/support/karma.conf.js @@ -12,6 +12,7 @@ module.exports = (config) => { 'spec/common/**/*.ts', 'spec/web/**/*.ts', 'src/abstractions/**/*.ts', + 'src/importers/**/*.ts', 'src/enums/**/*.ts', 'src/models/**/*.ts', 'src/misc/**/*.ts', From 4aaf452883defa23d8ea90a7ffbfa98bc91276d8 Mon Sep 17 00:00:00 2001 From: ShirokaiLon <6662937+ShirokaiLon@users.noreply.github.com> Date: Fri, 8 Feb 2019 13:01:08 +0000 Subject: [PATCH 0707/1626] Ignore regex URI when launching (#28) * Do not launch regex URI * Fix incorrect check * More null checks --- src/models/view/loginUriView.ts | 2 +- src/models/view/loginView.ts | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/models/view/loginUriView.ts b/src/models/view/loginUriView.ts index 84e37176c1..74c5638e85 100644 --- a/src/models/view/loginUriView.ts +++ b/src/models/view/loginUriView.ts @@ -78,7 +78,7 @@ export class LoginUriView implements View { if (this._canLaunch != null) { return this._canLaunch; } - if (this.uri != null) { + if (this.uri != null && this.match != UriMatchType.RegularExpression) { const uri = this.launchUri; for (let i = 0; i < CanLaunchWhitelist.length; i++) { if (uri.indexOf(CanLaunchWhitelist[i]) === 0) { diff --git a/src/models/view/loginView.ts b/src/models/view/loginView.ts index 8f4173ea92..66f95bb368 100644 --- a/src/models/view/loginView.ts +++ b/src/models/view/loginView.ts @@ -31,11 +31,18 @@ export class LoginView implements View { } get canLaunch(): boolean { - return this.hasUris && this.uris[0].canLaunch; + return this.hasUris && this.uris.some(uri => uri.canLaunch); } get launchUri(): string { - return this.canLaunch ? this.uris[0].launchUri : null; + if (this.hasUris) { + const uri = this.uris.find(uri => uri.canLaunch) + if (uri != null) { + return uri.launchUri; + } + } + + return null; } get hasUris(): boolean { From 98addd8ab5a226f654e78b1fe806b5fab6575b68 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 8 Feb 2019 08:03:12 -0500 Subject: [PATCH 0708/1626] formatting updates --- src/models/view/loginUriView.ts | 2 +- src/models/view/loginView.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/models/view/loginUriView.ts b/src/models/view/loginUriView.ts index 74c5638e85..cfdcc0e967 100644 --- a/src/models/view/loginUriView.ts +++ b/src/models/view/loginUriView.ts @@ -78,7 +78,7 @@ export class LoginUriView implements View { if (this._canLaunch != null) { return this._canLaunch; } - if (this.uri != null && this.match != UriMatchType.RegularExpression) { + if (this.uri != null && this.match !== UriMatchType.RegularExpression) { const uri = this.launchUri; for (let i = 0; i < CanLaunchWhitelist.length; i++) { if (uri.indexOf(CanLaunchWhitelist[i]) === 0) { diff --git a/src/models/view/loginView.ts b/src/models/view/loginView.ts index 66f95bb368..7c13a33faa 100644 --- a/src/models/view/loginView.ts +++ b/src/models/view/loginView.ts @@ -31,17 +31,16 @@ export class LoginView implements View { } get canLaunch(): boolean { - return this.hasUris && this.uris.some(uri => uri.canLaunch); + return this.hasUris && this.uris.some((u) => u.canLaunch); } get launchUri(): string { if (this.hasUris) { - const uri = this.uris.find(uri => uri.canLaunch) + const uri = this.uris.find((u) => u.canLaunch); if (uri != null) { return uri.launchUri; } } - return null; } From 647b254a71d0af105e1f6f1a1febeb15cd4181fb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 9 Feb 2019 00:19:46 -0500 Subject: [PATCH 0709/1626] billing invoices and transactions --- src/enums/transactionType.ts | 7 +++ src/models/response/billingResponse.ts | 52 +++++++++++++++++-- .../response/organizationBillingResponse.ts | 14 ++++- 3 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 src/enums/transactionType.ts diff --git a/src/enums/transactionType.ts b/src/enums/transactionType.ts new file mode 100644 index 0000000000..68cce6322f --- /dev/null +++ b/src/enums/transactionType.ts @@ -0,0 +1,7 @@ +export enum TransactionType { + Charge = 0, + Credit = 1, + PromotionalCredit = 2, + ReferralCredit = 3, + Refund = 4, +} diff --git a/src/models/response/billingResponse.ts b/src/models/response/billingResponse.ts index 4c3cd5ad01..2e32dfcb4b 100644 --- a/src/models/response/billingResponse.ts +++ b/src/models/response/billingResponse.ts @@ -1,4 +1,5 @@ import { PaymentMethodType } from '../../enums/paymentMethodType'; +import { TransactionType } from '../../enums/transactionType'; export class BillingResponse { storageName: string; @@ -6,8 +7,10 @@ export class BillingResponse { maxStorageGb: number; paymentSource: BillingSourceResponse; subscription: BillingSubscriptionResponse; - upcomingInvoice: BillingInvoiceResponse; + upcomingInvoice: BillingInvoiceInfoResponse; charges: BillingChargeResponse[] = []; + invoices: BillingInvoiceResponse[] = []; + transactions: BillingTransactionResponse[] = []; license: any; expiration: string; @@ -19,10 +22,16 @@ export class BillingResponse { this.subscription = response.Subscription == null ? null : new BillingSubscriptionResponse(response.Subscription); this.upcomingInvoice = response.UpcomingInvoice == null ? - null : new BillingInvoiceResponse(response.UpcomingInvoice); + null : new BillingInvoiceInfoResponse(response.UpcomingInvoice); if (response.Charges != null) { this.charges = response.Charges.map((c: any) => new BillingChargeResponse(c)); } + if (response.Transactions != null) { + this.transactions = response.Transactions.map((t: any) => new BillingTransactionResponse(t)); + } + if (response.Invoices != null) { + this.invoices = response.Invoices.map((i: any) => new BillingInvoiceResponse(i)); + } this.license = response.License; this.expiration = response.Expiration; } @@ -82,7 +91,7 @@ export class BillingSubscriptionItemResponse { } } -export class BillingInvoiceResponse { +export class BillingInvoiceInfoResponse { date: string; amount: number; @@ -115,3 +124,40 @@ export class BillingChargeResponse { this.invoiceId = response.InvoiceId; } } + +export class BillingInvoiceResponse extends BillingInvoiceInfoResponse { + url: string; + pdfUrl: string; + number: string; + paid: boolean; + + constructor(response: any) { + super(response); + this.url = response.Url; + this.pdfUrl = response.PdfUrl; + this.number = response.Number; + this.paid = response.Paid; + } +} + +export class BillingTransactionResponse { + createdDate: string; + amount: number; + refunded: boolean; + partiallyRefunded: boolean; + refundedAmount: number; + type: TransactionType; + paymentMethodType: PaymentMethodType; + details: string; + + constructor(response: any) { + this.createdDate = response.CreatedDate; + this.amount = response.Amount; + this.refunded = response.Refunded; + this.partiallyRefunded = response.PartiallyRefunded; + this.refundedAmount = response.RefundedAmount; + this.type = response.Type; + this.paymentMethodType = response.PaymentMethodType; + this.details = response.Details; + } +} diff --git a/src/models/response/organizationBillingResponse.ts b/src/models/response/organizationBillingResponse.ts index f68ef9e6c8..258da85b67 100644 --- a/src/models/response/organizationBillingResponse.ts +++ b/src/models/response/organizationBillingResponse.ts @@ -1,8 +1,10 @@ import { BillingChargeResponse, + BillingInvoiceInfoResponse, BillingInvoiceResponse, BillingSourceResponse, BillingSubscriptionResponse, + BillingTransactionResponse, } from './billingResponse'; import { OrganizationResponse } from './organizationResponse'; @@ -11,8 +13,10 @@ export class OrganizationBillingResponse extends OrganizationResponse { storageGb: number; paymentSource: BillingSourceResponse; subscription: BillingSubscriptionResponse; - upcomingInvoice: BillingInvoiceResponse; + upcomingInvoice: BillingInvoiceInfoResponse; charges: BillingChargeResponse[] = []; + invoices: BillingInvoiceResponse[] = []; + transactions: BillingTransactionResponse[] = []; expiration: string; constructor(response: any) { @@ -23,10 +27,16 @@ export class OrganizationBillingResponse extends OrganizationResponse { this.subscription = response.Subscription == null ? null : new BillingSubscriptionResponse(response.Subscription); this.upcomingInvoice = response.UpcomingInvoice == null ? - null : new BillingInvoiceResponse(response.UpcomingInvoice); + null : new BillingInvoiceInfoResponse(response.UpcomingInvoice); if (response.Charges != null) { this.charges = response.Charges.map((c: any) => new BillingChargeResponse(c)); } + if (response.Transactions != null) { + this.transactions = response.Transactions.map((t: any) => new BillingTransactionResponse(t)); + } + if (response.Invoices != null) { + this.invoices = response.Invoices.map((i: any) => new BillingInvoiceResponse(i)); + } this.expiration = response.Expiration; } } From 7a1e7b54743f065ee10eb27499186b629cce22b5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 12 Feb 2019 23:52:50 -0500 Subject: [PATCH 0710/1626] support for unlocking with PIN code --- src/abstractions/crypto.service.ts | 2 + src/abstractions/lock.service.ts | 1 + src/angular/components/lock.component.ts | 60 ++++++++++++++++++++---- src/services/constants.service.ts | 2 + src/services/crypto.service.ts | 10 ++++ src/services/lock.service.ts | 5 ++ 6 files changed, 72 insertions(+), 8 deletions(-) diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 0d0d9b4701..11eb21fcf1 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -26,11 +26,13 @@ export abstract class CryptoService { clearEncKey: (memoryOnly?: boolean) => Promise; clearKeyPair: (memoryOnly?: boolean) => Promise; clearOrgKeys: (memoryOnly?: boolean) => Promise; + clearPinProtectedKey: () => Promise; clearKeys: () => Promise; toggleKey: () => Promise; makeKey: (password: string, salt: string, kdf: KdfType, kdfIterations: number) => Promise; makeShareKey: () => Promise<[CipherString, SymmetricCryptoKey]>; makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, CipherString]>; + makePinKey: (pin: string, salt: string) => Promise; hashPassword: (password: string, key: SymmetricCryptoKey) => Promise; makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; remakeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; diff --git a/src/abstractions/lock.service.ts b/src/abstractions/lock.service.ts index 448a0a05b0..9c39c25351 100644 --- a/src/abstractions/lock.service.ts +++ b/src/abstractions/lock.service.ts @@ -2,4 +2,5 @@ export abstract class LockService { checkLock: () => Promise; lock: () => Promise; setLockOption: (lockOption: number) => Promise; + isPinLockSet: () => Promise; } diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 8ebc938e01..5dac6ea04a 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -3,27 +3,67 @@ import { Router } from '@angular/router'; import { CryptoService } from '../../abstractions/crypto.service'; import { I18nService } from '../../abstractions/i18n.service'; +import { LockService } from '../../abstractions/lock.service'; import { MessagingService } from '../../abstractions/messaging.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { StorageService } from '../../abstractions/storage.service'; import { UserService } from '../../abstractions/user.service'; +import { ConstantsService } from '../../services/constants.service'; + +import { CipherString } from '../../models/domain/cipherString'; +import { SymmetricCryptoKey } from '../../models/domain/symmetricCryptoKey'; + export class LockComponent implements OnInit { masterPassword: string = ''; + pin: string = ''; showPassword: boolean = false; email: string; + pinLock: boolean = false; protected successRoute: string = 'vault'; protected onSuccessfulSubmit: () => void; + private invalidPinAttempts = 0; + constructor(protected router: Router, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected messagingService: MessagingService, - protected userService: UserService, protected cryptoService: CryptoService) { } + protected userService: UserService, protected cryptoService: CryptoService, + protected storageService: StorageService, protected lockService: LockService) { } async ngOnInit() { + this.pinLock = await this.lockService.isPinLockSet(); this.email = await this.userService.getEmail(); } async submit() { + // PIN + if (this.pinLock) { + if (this.pin == null || this.pin === '') { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('pinRequired')); + return; + } + + const pinProtectedKey = await this.storageService.get(ConstantsService.pinProtectedKey); + try { + const protectedKeyCs = new CipherString(pinProtectedKey); + const pinKey = await this.cryptoService.makePinKey(this.pin, this.email); + const decKey = await this.cryptoService.decryptToBytes(protectedKeyCs, pinKey); + await this.setKeyAndContinue(new SymmetricCryptoKey(decKey)); + } catch { + this.invalidPinAttempts++; + if (this.invalidPinAttempts >= 5) { + this.messagingService.send('logout'); + return; + } + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('invalidPin')); + } + return; + } + + // Master Password if (this.masterPassword == null || this.masterPassword === '') { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('masterPassRequired')); @@ -37,13 +77,7 @@ export class LockComponent implements OnInit { const storedKeyHash = await this.cryptoService.getKeyHash(); if (storedKeyHash != null && keyHash != null && storedKeyHash === keyHash) { - await this.cryptoService.setKey(key); - this.messagingService.send('unlocked'); - if (this.onSuccessfulSubmit != null) { - this.onSuccessfulSubmit(); - } else if (this.router != null) { - this.router.navigate([this.successRoute]); - } + this.setKeyAndContinue(key); } else { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('invalidMasterPassword')); @@ -63,4 +97,14 @@ export class LockComponent implements OnInit { this.showPassword = !this.showPassword; document.getElementById('masterPassword').focus(); } + + private async setKeyAndContinue(key: SymmetricCryptoKey) { + await this.cryptoService.setKey(key); + this.messagingService.send('unlocked'); + if (this.onSuccessfulSubmit != null) { + this.onSuccessfulSubmit(); + } else if (this.router != null) { + this.router.navigate([this.successRoute]); + } + } } diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index dc6d0cf89d..41c5a1b395 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -18,6 +18,7 @@ export class ConstantsService { static readonly dontShowCardsCurrentTab: string = 'dontShowCardsCurrentTab'; static readonly dontShowIdentitiesCurrentTab: string = 'dontShowIdentitiesCurrentTab'; static readonly defaultUriMatch: string = 'defaultUriMatch'; + static readonly pinProtectedKey: string = 'pinProtectedKey'; readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; readonly disableGaKey: string = ConstantsService.disableGaKey; @@ -37,4 +38,5 @@ export class ConstantsService { readonly dontShowCardsCurrentTab: string = ConstantsService.dontShowCardsCurrentTab; readonly dontShowIdentitiesCurrentTab: string = ConstantsService.dontShowIdentitiesCurrentTab; readonly defaultUriMatch: string = ConstantsService.defaultUriMatch; + readonly pinProtectedKey: string = ConstantsService.pinProtectedKey; } diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 8c5f68b099..c9979e1d84 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -266,6 +266,10 @@ export class CryptoService implements CryptoServiceAbstraction { return this.storageService.remove(Keys.encOrgKeys); } + clearPinProtectedKey(): Promise { + return this.storageService.remove(ConstantsService.pinProtectedKey); + } + clearKeys(): Promise { return Promise.all([ this.clearKey(), @@ -273,6 +277,7 @@ export class CryptoService implements CryptoServiceAbstraction { this.clearOrgKeys(), this.clearEncKey(), this.clearKeyPair(), + this.clearPinProtectedKey(), ]); } @@ -319,6 +324,11 @@ export class CryptoService implements CryptoServiceAbstraction { return [publicB64, privateEnc]; } + async makePinKey(pin: string, salt: string): Promise { + const pinKey = await this.makeKey(pin, salt, KdfType.PBKDF2_SHA256, 100000); + return await this.stretchKey(pinKey); + } + async hashPassword(password: string, key: SymmetricCryptoKey): Promise { if (key == null) { key = await this.getKey(); diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts index 194c45d130..ff1c976ae6 100644 --- a/src/services/lock.service.ts +++ b/src/services/lock.service.ts @@ -87,4 +87,9 @@ export class LockService implements LockServiceAbstraction { await this.storageService.save(ConstantsService.lockOptionKey, lockOption); await this.cryptoService.toggleKey(); } + + async isPinLockSet(): Promise { + const pinProtectedKey = await this.storageService.get(ConstantsService.pinProtectedKey); + return pinProtectedKey != null; + } } From 53260a5be8456f5d45fe68dfcac59dc28ab4e8c2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 13 Feb 2019 00:04:31 -0500 Subject: [PATCH 0711/1626] use user kdf settings for making PIN key --- src/abstractions/crypto.service.ts | 2 +- src/angular/components/lock.component.ts | 53 +++++++++++------------- src/services/crypto.service.ts | 4 +- 3 files changed, 28 insertions(+), 31 deletions(-) diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 11eb21fcf1..889500c40f 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -32,7 +32,7 @@ export abstract class CryptoService { makeKey: (password: string, salt: string, kdf: KdfType, kdfIterations: number) => Promise; makeShareKey: () => Promise<[CipherString, SymmetricCryptoKey]>; makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, CipherString]>; - makePinKey: (pin: string, salt: string) => Promise; + makePinKey: (pin: string, salt: string, kdf: KdfType, kdfIterations: number) => Promise; hashPassword: (password: string, key: SymmetricCryptoKey) => Promise; makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; remakeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 5dac6ea04a..26d1cb9f5d 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -37,18 +37,25 @@ export class LockComponent implements OnInit { } async submit() { - // PIN - if (this.pinLock) { - if (this.pin == null || this.pin === '') { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('pinRequired')); - return; - } + if (this.pinLock && (this.pin == null || this.pin === '')) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('pinRequired')); + return; + } + if (!this.pinLock && (this.masterPassword == null || this.masterPassword === '')) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassRequired')); + return; + } + const kdf = await this.userService.getKdf(); + const kdfIterations = await this.userService.getKdfIterations(); + + if (this.pinLock) { const pinProtectedKey = await this.storageService.get(ConstantsService.pinProtectedKey); try { const protectedKeyCs = new CipherString(pinProtectedKey); - const pinKey = await this.cryptoService.makePinKey(this.pin, this.email); + const pinKey = await this.cryptoService.makePinKey(this.pin, this.email, kdf, kdfIterations); const decKey = await this.cryptoService.decryptToBytes(protectedKeyCs, pinKey); await this.setKeyAndContinue(new SymmetricCryptoKey(decKey)); } catch { @@ -60,27 +67,17 @@ export class LockComponent implements OnInit { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('invalidPin')); } - return; - } - - // Master Password - if (this.masterPassword == null || this.masterPassword === '') { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPassRequired')); - return; - } - - const kdf = await this.userService.getKdf(); - const kdfIterations = await this.userService.getKdfIterations(); - const key = await this.cryptoService.makeKey(this.masterPassword, this.email, kdf, kdfIterations); - const keyHash = await this.cryptoService.hashPassword(this.masterPassword, key); - const storedKeyHash = await this.cryptoService.getKeyHash(); - - if (storedKeyHash != null && keyHash != null && storedKeyHash === keyHash) { - this.setKeyAndContinue(key); } else { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('invalidMasterPassword')); + const key = await this.cryptoService.makeKey(this.masterPassword, this.email, kdf, kdfIterations); + const keyHash = await this.cryptoService.hashPassword(this.masterPassword, key); + const storedKeyHash = await this.cryptoService.getKeyHash(); + + if (storedKeyHash != null && keyHash != null && storedKeyHash === keyHash) { + this.setKeyAndContinue(key); + } else { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('invalidMasterPassword')); + } } } diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index c9979e1d84..865ec3bfc1 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -324,8 +324,8 @@ export class CryptoService implements CryptoServiceAbstraction { return [publicB64, privateEnc]; } - async makePinKey(pin: string, salt: string): Promise { - const pinKey = await this.makeKey(pin, salt, KdfType.PBKDF2_SHA256, 100000); + async makePinKey(pin: string, salt: string, kdf: KdfType, kdfIterations: number): Promise { + const pinKey = await this.makeKey(pin, salt, kdf, kdfIterations); return await this.stretchKey(pinKey); } From f67fac3eebc21b8935a54a28b7a21152c8513322 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 13 Feb 2019 10:05:58 -0500 Subject: [PATCH 0712/1626] focus pin if using pinLock --- src/angular/components/lock.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 26d1cb9f5d..37f2f72603 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -92,7 +92,7 @@ export class LockComponent implements OnInit { togglePassword() { this.platformUtilsService.eventTrack('Toggled Master Password on Unlock'); this.showPassword = !this.showPassword; - document.getElementById('masterPassword').focus(); + document.getElementById(this.pinLock ? 'pin' : 'masterPassword').focus(); } private async setKeyAndContinue(key: SymmetricCryptoKey) { From 76c53bc641bc2385b601921f52be105bb539ffe6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 13 Feb 2019 15:32:25 -0500 Subject: [PATCH 0713/1626] remembeat csv importer --- src/importers/rememBearCsvImporter.ts | 73 +++++++++++++++++++++++++++ src/services/import.service.ts | 4 ++ 2 files changed, 77 insertions(+) create mode 100644 src/importers/rememBearCsvImporter.ts diff --git a/src/importers/rememBearCsvImporter.ts b/src/importers/rememBearCsvImporter.ts new file mode 100644 index 0000000000..9cf559e8d2 --- /dev/null +++ b/src/importers/rememBearCsvImporter.ts @@ -0,0 +1,73 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { CipherType } from '../enums/cipherType'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CardView } from '../models/view/cardView'; + +export class RememBearCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + if (value.trash === 'true') { + return; + } + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(value.name); + cipher.notes = this.getValueOrDefault(value.notes); + if (value.type === 'LoginItem') { + cipher.login.uris = this.makeUriArray(value.website); + cipher.login.password = this.getValueOrDefault(value.password); + cipher.login.username = this.getValueOrDefault(value.username); + } else if (value.type === 'CreditCardItem') { + cipher.type = CipherType.Card; + cipher.card = new CardView(); + cipher.card.cardholderName = this.getValueOrDefault(value.cardholder); + cipher.card.number = this.getValueOrDefault(value.number); + cipher.card.brand = this.getCardBrand(cipher.card.number); + cipher.card.code = this.getValueOrDefault(value.verification); + + try { + const expMonth = this.getValueOrDefault(value.expiryMonth); + if (expMonth != null) { + const expMonthNumber = parseInt(expMonth, null); + if (expMonthNumber != null && expMonthNumber >= 1 && expMonthNumber <= 12) { + cipher.card.expMonth = expMonthNumber.toString(); + } + } + } catch { } + try { + const expYear = this.getValueOrDefault(value.expiryYear); + if (expYear != null) { + const expYearNumber = parseInt(expYear, null); + if (expYearNumber != null) { + cipher.card.expYear = expYearNumber.toString(); + } + } + } catch { } + + const pin = this.getValueOrDefault(value.pin); + if (pin != null) { + this.processKvp(cipher, 'PIN', pin); + } + const zip = this.getValueOrDefault(value.zipCode); + if (zip != null) { + this.processKvp(cipher, 'Zip Code', zip); + } + } + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} diff --git a/src/services/import.service.ts b/src/services/import.service.ts index 6f39bf52c6..7c66014e32 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -55,6 +55,7 @@ import { PasswordAgentCsvImporter } from '../importers/passwordAgentCsvImporter' import { PasswordBossJsonImporter } from '../importers/passwordBossJsonImporter'; import { PasswordDragonXmlImporter } from '../importers/passwordDragonXmlImporter'; import { PasswordSafeXmlImporter } from '../importers/passwordSafeXmlImporter'; +import { RememBearCsvImporter } from '../importers/rememBearCsvImporter'; import { RoboFormCsvImporter } from '../importers/roboformCsvImporter'; import { SafeInCloudXmlImporter } from '../importers/safeInCloudXmlImporter'; import { SaferPassCsvImporter } from '../importers/saferpassCsvImport'; @@ -111,6 +112,7 @@ export class ImportService implements ImportServiceAbstraction { { id: 'avastcsv', name: 'Avast Passwords (csv)' }, { id: 'fsecurefsk', name: 'F-Secure KEY (fsk)' }, { id: 'kasperskytxt', name: 'Kaspersky Password Manager (txt)' }, + { id: 'remembearcsv', name: 'RememBear (csv)' }, ]; constructor(private cipherService: CipherService, private folderService: FolderService, @@ -239,6 +241,8 @@ export class ImportService implements ImportServiceAbstraction { return new FSecureFskImporter(); case 'kasperskytxt': return new KasperskyTxtImporter(); + case 'remembearcsv': + return new RememBearCsvImporter(); default: return null; } From 0bdbfd79846e54e1b28a7fd116d8ffa533de4219 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 13 Feb 2019 21:36:36 -0500 Subject: [PATCH 0714/1626] soft locking with protected pin --- src/abstractions/lock.service.ts | 7 +++- src/angular/components/lock.component.ts | 33 +++++++++++++---- src/angular/services/auth-guard.service.ts | 8 ++-- src/services/constants.service.ts | 2 + src/services/lock.service.ts | 43 ++++++++++++++++++++-- src/services/notifications.service.ts | 6 +-- 6 files changed, 79 insertions(+), 20 deletions(-) diff --git a/src/abstractions/lock.service.ts b/src/abstractions/lock.service.ts index 9c39c25351..ede31db0b1 100644 --- a/src/abstractions/lock.service.ts +++ b/src/abstractions/lock.service.ts @@ -1,6 +1,9 @@ export abstract class LockService { + pinLocked: boolean; + isLocked: () => Promise; checkLock: () => Promise; - lock: () => Promise; + lock: (allowSoftLock?: boolean) => Promise; setLockOption: (lockOption: number) => Promise; - isPinLockSet: () => Promise; + isPinLockSet: () => Promise<[boolean, boolean]>; + clear: () => Promise; } diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 37f2f72603..715c43f5b0 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -25,6 +25,7 @@ export class LockComponent implements OnInit { protected onSuccessfulSubmit: () => void; private invalidPinAttempts = 0; + private pinSet: [boolean, boolean]; constructor(protected router: Router, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected messagingService: MessagingService, @@ -32,7 +33,9 @@ export class LockComponent implements OnInit { protected storageService: StorageService, protected lockService: LockService) { } async ngOnInit() { - this.pinLock = await this.lockService.isPinLockSet(); + this.pinSet = await this.lockService.isPinLockSet(); + const hasKey = await this.cryptoService.hasKey(); + this.pinLock = (this.pinSet[0] && hasKey) || this.pinSet[1]; this.email = await this.userService.getEmail(); } @@ -52,13 +55,25 @@ export class LockComponent implements OnInit { const kdfIterations = await this.userService.getKdfIterations(); if (this.pinLock) { - const pinProtectedKey = await this.storageService.get(ConstantsService.pinProtectedKey); + let failed = true; try { - const protectedKeyCs = new CipherString(pinProtectedKey); - const pinKey = await this.cryptoService.makePinKey(this.pin, this.email, kdf, kdfIterations); - const decKey = await this.cryptoService.decryptToBytes(protectedKeyCs, pinKey); - await this.setKeyAndContinue(new SymmetricCryptoKey(decKey)); - } catch { + if (this.pinSet[0]) { + const protectedPin = await this.storageService.get(ConstantsService.protectedPin); + const decPin = await this.cryptoService.decryptToUtf8(new CipherString(protectedPin)); + this.lockService.pinLocked = false; + failed = decPin !== this.pin; + this.doContinue(); + } else { + const pinProtectedKey = await this.storageService.get(ConstantsService.pinProtectedKey); + const protectedKeyCs = new CipherString(pinProtectedKey); + const pinKey = await this.cryptoService.makePinKey(this.pin, this.email, kdf, kdfIterations); + const decKey = await this.cryptoService.decryptToBytes(protectedKeyCs, pinKey); + failed = false; + await this.setKeyAndContinue(new SymmetricCryptoKey(decKey)); + } + } catch { } + + if (failed) { this.invalidPinAttempts++; if (this.invalidPinAttempts >= 5) { this.messagingService.send('logout'); @@ -97,6 +112,10 @@ export class LockComponent implements OnInit { private async setKeyAndContinue(key: SymmetricCryptoKey) { await this.cryptoService.setKey(key); + this.doContinue(); + } + + private doContinue() { this.messagingService.send('unlocked'); if (this.onSuccessfulSubmit != null) { this.onSuccessfulSubmit(); diff --git a/src/angular/services/auth-guard.service.ts b/src/angular/services/auth-guard.service.ts index 1db4e5726c..6956df71d1 100644 --- a/src/angular/services/auth-guard.service.ts +++ b/src/angular/services/auth-guard.service.ts @@ -4,13 +4,13 @@ import { Router, } from '@angular/router'; -import { CryptoService } from '../../abstractions/crypto.service'; +import { LockService } from '../../abstractions/lock.service'; import { MessagingService } from '../../abstractions/messaging.service'; import { UserService } from '../../abstractions/user.service'; @Injectable() export class AuthGuardService implements CanActivate { - constructor(private cryptoService: CryptoService, private userService: UserService, private router: Router, + constructor(private lockService: LockService, private userService: UserService, private router: Router, private messagingService: MessagingService) { } async canActivate() { @@ -20,8 +20,8 @@ export class AuthGuardService implements CanActivate { return false; } - const hasKey = await this.cryptoService.hasKey(); - if (!hasKey) { + const locked = await this.lockService.isLocked(); + if (locked) { this.router.navigate(['lock']); return false; } diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index 41c5a1b395..755e3335e8 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -19,6 +19,7 @@ export class ConstantsService { static readonly dontShowIdentitiesCurrentTab: string = 'dontShowIdentitiesCurrentTab'; static readonly defaultUriMatch: string = 'defaultUriMatch'; static readonly pinProtectedKey: string = 'pinProtectedKey'; + static readonly protectedPin: string = 'protectedPin'; readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; readonly disableGaKey: string = ConstantsService.disableGaKey; @@ -39,4 +40,5 @@ export class ConstantsService { readonly dontShowIdentitiesCurrentTab: string = ConstantsService.dontShowIdentitiesCurrentTab; readonly defaultUriMatch: string = ConstantsService.defaultUriMatch; readonly pinProtectedKey: string = ConstantsService.pinProtectedKey; + readonly protectedPin: string = ConstantsService.protectedPin; } diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts index ff1c976ae6..1576c1582c 100644 --- a/src/services/lock.service.ts +++ b/src/services/lock.service.ts @@ -11,6 +11,8 @@ import { SearchService } from '../abstractions/search.service'; import { StorageService } from '../abstractions/storage.service'; export class LockService implements LockServiceAbstraction { + pinLocked = false; + private inited = false; constructor(private cipherService: CipherService, private folderService: FolderService, @@ -32,12 +34,24 @@ export class LockService implements LockServiceAbstraction { } } + async isLocked(): Promise { + if (this.pinLocked) { + return true; + } + const hasKey = await this.cryptoService.hasKey(); + return !hasKey; + } + async checkLock(): Promise { if (this.platformUtilsService.isViewOpen()) { // Do not lock return; } + if (this.pinLocked) { + return; + } + const hasKey = await this.cryptoService.hasKey(); if (!hasKey) { // no key so no need to lock @@ -61,11 +75,19 @@ export class LockService implements LockServiceAbstraction { const diffSeconds = ((new Date()).getTime() - lastActive) / 1000; if (diffSeconds >= lockOptionSeconds) { // need to lock now - await this.lock(); + await this.lock(true); } } - async lock(): Promise { + async lock(allowSoftLock = false): Promise { + if (allowSoftLock) { + const pinSet = await this.isPinLockSet(); + if (pinSet[0]) { + await this.pinLock(); + return; + } + } + await Promise.all([ this.cryptoService.clearKey(), this.cryptoService.clearOrgKeys(true), @@ -88,8 +110,21 @@ export class LockService implements LockServiceAbstraction { await this.cryptoService.toggleKey(); } - async isPinLockSet(): Promise { + async isPinLockSet(): Promise<[boolean, boolean]> { + const protectedPin = await this.storageService.get(ConstantsService.protectedPin); const pinProtectedKey = await this.storageService.get(ConstantsService.pinProtectedKey); - return pinProtectedKey != null; + return [protectedPin != null, pinProtectedKey != null]; + } + + clear(): Promise { + return this.storageService.remove(ConstantsService.protectedPin); + } + + private async pinLock(): Promise { + this.pinLocked = true; + this.messagingService.send('locked'); + if (this.lockedCallback != null) { + await this.lockedCallback(); + } } } diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index 9ad374bf65..5a52f527e3 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -5,8 +5,8 @@ import { NotificationType } from '../enums/notificationType'; import { ApiService } from '../abstractions/api.service'; import { AppIdService } from '../abstractions/appId.service'; -import { CryptoService } from '../abstractions/crypto.service'; import { EnvironmentService } from '../abstractions/environment.service'; +import { LockService } from '../abstractions/lock.service'; import { NotificationsService as NotificationsServiceAbstraction } from '../abstractions/notifications.service'; import { SyncService } from '../abstractions/sync.service'; import { UserService } from '../abstractions/user.service'; @@ -27,7 +27,7 @@ export class NotificationsService implements NotificationsServiceAbstraction { constructor(private userService: UserService, private syncService: SyncService, private appIdService: AppIdService, private apiService: ApiService, - private cryptoService: CryptoService, private logoutCallback: () => Promise) { } + private lockService: LockService, private logoutCallback: () => Promise) { } async init(environmentService: EnvironmentService): Promise { this.inited = false; @@ -185,7 +185,7 @@ export class NotificationsService implements NotificationsServiceAbstraction { private async isAuthedAndUnlocked() { if (await this.userService.isAuthenticated()) { - return this.cryptoService.hasKey(); + return this.lockService.isLocked(); } return false; } From 43872f82ccd7453926c75240ecde196151e89ce9 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 13 Feb 2019 22:08:55 -0500 Subject: [PATCH 0715/1626] cant be pin locked without key --- src/services/lock.service.ts | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts index 1576c1582c..941e86f249 100644 --- a/src/services/lock.service.ts +++ b/src/services/lock.service.ts @@ -35,10 +35,10 @@ export class LockService implements LockServiceAbstraction { } async isLocked(): Promise { - if (this.pinLocked) { + const hasKey = await this.cryptoService.hasKey(); + if (hasKey && this.pinLocked) { return true; } - const hasKey = await this.cryptoService.hasKey(); return !hasKey; } @@ -48,13 +48,7 @@ export class LockService implements LockServiceAbstraction { return; } - if (this.pinLocked) { - return; - } - - const hasKey = await this.cryptoService.hasKey(); - if (!hasKey) { - // no key so no need to lock + if (this.isLocked()) { return; } @@ -83,7 +77,11 @@ export class LockService implements LockServiceAbstraction { if (allowSoftLock) { const pinSet = await this.isPinLockSet(); if (pinSet[0]) { - await this.pinLock(); + this.pinLocked = true; + this.messagingService.send('locked'); + if (this.lockedCallback != null) { + await this.lockedCallback(); + } return; } } @@ -119,12 +117,4 @@ export class LockService implements LockServiceAbstraction { clear(): Promise { return this.storageService.remove(ConstantsService.protectedPin); } - - private async pinLock(): Promise { - this.pinLocked = true; - this.messagingService.send('locked'); - if (this.lockedCallback != null) { - await this.lockedCallback(); - } - } } From a19a30ffed177e18d6e64801066510bc983a3e8d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 14 Feb 2019 09:07:20 -0500 Subject: [PATCH 0716/1626] add vnc launch protocol --- src/models/view/loginUriView.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/models/view/loginUriView.ts b/src/models/view/loginUriView.ts index cfdcc0e967..1d51561c11 100644 --- a/src/models/view/loginUriView.ts +++ b/src/models/view/loginUriView.ts @@ -13,6 +13,7 @@ const CanLaunchWhitelist = [ 'ftp://', 'sftp://', 'irc://', + 'vnc://', 'chrome://', ]; From c37a52be8568fe749bb6e4c190dc4c736f02fb80 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 15 Feb 2019 16:53:01 -0500 Subject: [PATCH 0717/1626] trim env urls --- src/services/environment.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/environment.service.ts b/src/services/environment.service.ts index 28f0f286b6..60a2dc8c62 100644 --- a/src/services/environment.service.ts +++ b/src/services/environment.service.ts @@ -103,6 +103,6 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { url = 'https://' + url; } - return url; + return url.trim(); } } From 3e996ae9adf309f3ee4398630299fb0ab5312f9c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sun, 17 Feb 2019 21:16:49 -0500 Subject: [PATCH 0718/1626] not locked --- src/services/notifications.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index 5a52f527e3..d88d9124a4 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -185,7 +185,8 @@ export class NotificationsService implements NotificationsServiceAbstraction { private async isAuthedAndUnlocked() { if (await this.userService.isAuthenticated()) { - return this.lockService.isLocked(); + const locked = await this.lockService.isLocked(); + return !locked; } return false; } From 8b411de034569b36788239a5948d64f5029d6437 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 18 Feb 2019 15:24:06 -0500 Subject: [PATCH 0719/1626] support for new billing and subscription endpoints --- src/abstractions/api.service.ts | 7 +- src/models/response/billingResponse.ts | 73 ++----------------- .../response/organizationBillingResponse.ts | 42 ----------- .../organizationSubscriptionResponse.ts | 24 ++++++ src/models/response/subscriptionResponse.ts | 71 ++++++++++++++++++ src/services/api.service.ts | 17 ++++- 6 files changed, 119 insertions(+), 115 deletions(-) delete mode 100644 src/models/response/organizationBillingResponse.ts create mode 100644 src/models/response/organizationSubscriptionResponse.ts create mode 100644 src/models/response/subscriptionResponse.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 3fd8e0e430..ed77c77506 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -68,8 +68,8 @@ import { import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; import { ListResponse } from '../models/response/listResponse'; -import { OrganizationBillingResponse } from '../models/response/organizationBillingResponse'; import { OrganizationResponse } from '../models/response/organizationResponse'; +import { OrganizationSubscriptionResponse } from '../models/response/organizationSubscriptionResponse'; import { OrganizationUserDetailsResponse, OrganizationUserUserDetailsResponse, @@ -77,6 +77,7 @@ import { import { PreloginResponse } from '../models/response/preloginResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SelectionReadOnlyResponse } from '../models/response/selectionReadOnlyResponse'; +import { SubscriptionResponse } from '../models/response/subscriptionResponse'; import { SyncResponse } from '../models/response/syncResponse'; import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; import { TwoFactorDuoResponse } from '../models/response/twoFactorDuoResponse'; @@ -101,6 +102,7 @@ export abstract class ApiService { getProfile: () => Promise; getUserBilling: () => Promise; + getUserSubscription: () => Promise; putProfile: (request: UpdateProfileRequest) => Promise; postPrelogin: (request: PreloginRequest) => Promise; postEmailToken: (request: EmailTokenRequest) => Promise; @@ -224,7 +226,8 @@ export abstract class ApiService { postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise; getOrganization: (id: string) => Promise; - getOrganizationBilling: (id: string) => Promise; + getOrganizationBilling: (id: string) => Promise; + getOrganizationSubscription: (id: string) => Promise; getOrganizationLicense: (id: string, installationId: string) => Promise; postOrganization: (request: OrganizationCreateRequest) => Promise; putOrganization: (id: string, request: OrganizationUpdateRequest) => Promise; diff --git a/src/models/response/billingResponse.ts b/src/models/response/billingResponse.ts index 2e32dfcb4b..48c797f3f6 100644 --- a/src/models/response/billingResponse.ts +++ b/src/models/response/billingResponse.ts @@ -2,27 +2,13 @@ import { PaymentMethodType } from '../../enums/paymentMethodType'; import { TransactionType } from '../../enums/transactionType'; export class BillingResponse { - storageName: string; - storageGb: number; - maxStorageGb: number; paymentSource: BillingSourceResponse; - subscription: BillingSubscriptionResponse; - upcomingInvoice: BillingInvoiceInfoResponse; charges: BillingChargeResponse[] = []; invoices: BillingInvoiceResponse[] = []; transactions: BillingTransactionResponse[] = []; - license: any; - expiration: string; constructor(response: any) { - this.storageName = response.StorageName; - this.storageGb = response.StorageGb; - this.maxStorageGb = response.MaxStorageGb; this.paymentSource = response.PaymentSource == null ? null : new BillingSourceResponse(response.PaymentSource); - this.subscription = response.Subscription == null ? - null : new BillingSubscriptionResponse(response.Subscription); - this.upcomingInvoice = response.UpcomingInvoice == null ? - null : new BillingInvoiceInfoResponse(response.UpcomingInvoice); if (response.Charges != null) { this.charges = response.Charges.map((c: any) => new BillingChargeResponse(c)); } @@ -32,8 +18,6 @@ export class BillingResponse { if (response.Invoices != null) { this.invoices = response.Invoices.map((i: any) => new BillingInvoiceResponse(i)); } - this.license = response.License; - this.expiration = response.Expiration; } } @@ -51,56 +35,6 @@ export class BillingSourceResponse { } } -export class BillingSubscriptionResponse { - trialStartDate: string; - trialEndDate: string; - periodStartDate: string; - periodEndDate: string; - cancelledDate: string; - cancelAtEndDate: boolean; - status: string; - cancelled: boolean; - items: BillingSubscriptionItemResponse[] = []; - - constructor(response: any) { - this.trialEndDate = response.TrialStartDate; - this.trialEndDate = response.TrialEndDate; - this.periodStartDate = response.PeriodStartDate; - this.periodEndDate = response.PeriodEndDate; - this.cancelledDate = response.CancelledDate; - this.cancelAtEndDate = response.CancelAtEndDate; - this.status = response.Status; - this.cancelled = response.Cancelled; - if (response.Items != null) { - this.items = response.Items.map((i: any) => new BillingSubscriptionItemResponse(i)); - } - } -} - -export class BillingSubscriptionItemResponse { - name: string; - amount: number; - quantity: number; - interval: string; - - constructor(response: any) { - this.name = response.Name; - this.amount = response.Amount; - this.quantity = response.Quantity; - this.interval = response.Interval; - } -} - -export class BillingInvoiceInfoResponse { - date: string; - amount: number; - - constructor(response: any) { - this.date = response.Date; - this.amount = response.Amount; - } -} - export class BillingChargeResponse { createdDate: string; amount: number; @@ -125,18 +59,21 @@ export class BillingChargeResponse { } } -export class BillingInvoiceResponse extends BillingInvoiceInfoResponse { +export class BillingInvoiceResponse { url: string; pdfUrl: string; number: string; paid: boolean; + date: string; + amount: number; constructor(response: any) { - super(response); this.url = response.Url; this.pdfUrl = response.PdfUrl; this.number = response.Number; this.paid = response.Paid; + this.date = response.Date; + this.amount = response.Amount; } } diff --git a/src/models/response/organizationBillingResponse.ts b/src/models/response/organizationBillingResponse.ts deleted file mode 100644 index 258da85b67..0000000000 --- a/src/models/response/organizationBillingResponse.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - BillingChargeResponse, - BillingInvoiceInfoResponse, - BillingInvoiceResponse, - BillingSourceResponse, - BillingSubscriptionResponse, - BillingTransactionResponse, -} from './billingResponse'; -import { OrganizationResponse } from './organizationResponse'; - -export class OrganizationBillingResponse extends OrganizationResponse { - storageName: string; - storageGb: number; - paymentSource: BillingSourceResponse; - subscription: BillingSubscriptionResponse; - upcomingInvoice: BillingInvoiceInfoResponse; - charges: BillingChargeResponse[] = []; - invoices: BillingInvoiceResponse[] = []; - transactions: BillingTransactionResponse[] = []; - expiration: string; - - constructor(response: any) { - super(response); - this.storageName = response.StorageName; - this.storageGb = response.StorageGb; - this.paymentSource = response.PaymentSource == null ? null : new BillingSourceResponse(response.PaymentSource); - this.subscription = response.Subscription == null ? - null : new BillingSubscriptionResponse(response.Subscription); - this.upcomingInvoice = response.UpcomingInvoice == null ? - null : new BillingInvoiceInfoResponse(response.UpcomingInvoice); - if (response.Charges != null) { - this.charges = response.Charges.map((c: any) => new BillingChargeResponse(c)); - } - if (response.Transactions != null) { - this.transactions = response.Transactions.map((t: any) => new BillingTransactionResponse(t)); - } - if (response.Invoices != null) { - this.invoices = response.Invoices.map((i: any) => new BillingInvoiceResponse(i)); - } - this.expiration = response.Expiration; - } -} diff --git a/src/models/response/organizationSubscriptionResponse.ts b/src/models/response/organizationSubscriptionResponse.ts new file mode 100644 index 0000000000..77cacaa68b --- /dev/null +++ b/src/models/response/organizationSubscriptionResponse.ts @@ -0,0 +1,24 @@ +import { OrganizationResponse } from './organizationResponse'; +import { + BillingSubscriptionResponse, + BillingSubscriptionUpcomingInvoiceResponse, +} from './subscriptionResponse'; + +export class OrganizationSubscriptionResponse extends OrganizationResponse { + storageName: string; + storageGb: number; + subscription: BillingSubscriptionResponse; + upcomingInvoice: BillingSubscriptionUpcomingInvoiceResponse; + expiration: string; + + constructor(response: any) { + super(response); + this.storageName = response.StorageName; + this.storageGb = response.StorageGb; + this.subscription = response.Subscription == null ? + null : new BillingSubscriptionResponse(response.Subscription); + this.upcomingInvoice = response.UpcomingInvoice == null ? + null : new BillingSubscriptionUpcomingInvoiceResponse(response.UpcomingInvoice); + this.expiration = response.Expiration; + } +} diff --git a/src/models/response/subscriptionResponse.ts b/src/models/response/subscriptionResponse.ts new file mode 100644 index 0000000000..86d3580307 --- /dev/null +++ b/src/models/response/subscriptionResponse.ts @@ -0,0 +1,71 @@ +export class SubscriptionResponse { + storageName: string; + storageGb: number; + maxStorageGb: number; + subscription: BillingSubscriptionResponse; + upcomingInvoice: BillingSubscriptionUpcomingInvoiceResponse; + license: any; + expiration: string; + + constructor(response: any) { + this.storageName = response.StorageName; + this.storageGb = response.StorageGb; + this.maxStorageGb = response.MaxStorageGb; + this.subscription = response.Subscription == null ? + null : new BillingSubscriptionResponse(response.Subscription); + this.upcomingInvoice = response.UpcomingInvoice == null ? + null : new BillingSubscriptionUpcomingInvoiceResponse(response.UpcomingInvoice); + this.license = response.License; + this.expiration = response.Expiration; + } +} + +export class BillingSubscriptionResponse { + trialStartDate: string; + trialEndDate: string; + periodStartDate: string; + periodEndDate: string; + cancelledDate: string; + cancelAtEndDate: boolean; + status: string; + cancelled: boolean; + items: BillingSubscriptionItemResponse[] = []; + + constructor(response: any) { + this.trialEndDate = response.TrialStartDate; + this.trialEndDate = response.TrialEndDate; + this.periodStartDate = response.PeriodStartDate; + this.periodEndDate = response.PeriodEndDate; + this.cancelledDate = response.CancelledDate; + this.cancelAtEndDate = response.CancelAtEndDate; + this.status = response.Status; + this.cancelled = response.Cancelled; + if (response.Items != null) { + this.items = response.Items.map((i: any) => new BillingSubscriptionItemResponse(i)); + } + } +} + +export class BillingSubscriptionItemResponse { + name: string; + amount: number; + quantity: number; + interval: string; + + constructor(response: any) { + this.name = response.Name; + this.amount = response.Amount; + this.quantity = response.Quantity; + this.interval = response.Interval; + } +} + +export class BillingSubscriptionUpcomingInvoiceResponse { + date: string; + amount: number; + + constructor(response: any) { + this.date = response.Date; + this.amount = response.Amount; + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index bb9f7faa55..692ac69dc5 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -75,8 +75,8 @@ import { import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; import { ListResponse } from '../models/response/listResponse'; -import { OrganizationBillingResponse } from '../models/response/organizationBillingResponse'; import { OrganizationResponse } from '../models/response/organizationResponse'; +import { OrganizationSubscriptionResponse } from '../models/response/organizationSubscriptionResponse'; import { OrganizationUserDetailsResponse, OrganizationUserUserDetailsResponse, @@ -84,6 +84,7 @@ import { import { PreloginResponse } from '../models/response/preloginResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SelectionReadOnlyResponse } from '../models/response/selectionReadOnlyResponse'; +import { SubscriptionResponse } from '../models/response/subscriptionResponse'; import { SyncResponse } from '../models/response/syncResponse'; import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; import { TwoFactorDuoResponse } from '../models/response/twoFactorDuoResponse'; @@ -200,6 +201,11 @@ export class ApiService implements ApiServiceAbstraction { return new BillingResponse(r); } + async getUserSubscription(): Promise { + const r = await this.send('GET', '/accounts/subscription', null, true, true); + return new SubscriptionResponse(r); + } + async putProfile(request: UpdateProfileRequest): Promise { const r = await this.send('PUT', '/accounts/profile', request, true, true); return new ProfileResponse(r); @@ -722,9 +728,14 @@ export class ApiService implements ApiServiceAbstraction { return new OrganizationResponse(r); } - async getOrganizationBilling(id: string): Promise { + async getOrganizationBilling(id: string): Promise { const r = await this.send('GET', '/organizations/' + id + '/billing', null, true, true); - return new OrganizationBillingResponse(r); + return new BillingResponse(r); + } + + async getOrganizationSubscription(id: string): Promise { + const r = await this.send('GET', '/organizations/' + id + '/subscription', null, true, true); + return new OrganizationSubscriptionResponse(r); } async getOrganizationLicense(id: string, installationId: string): Promise { From b95b35e7d9444c668691dd3ee6e78c1904321f22 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 18 Feb 2019 17:01:10 -0500 Subject: [PATCH 0720/1626] remove charges and add balance --- src/models/response/billingResponse.ts | 30 ++------------------------ 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/src/models/response/billingResponse.ts b/src/models/response/billingResponse.ts index 48c797f3f6..c1c605c435 100644 --- a/src/models/response/billingResponse.ts +++ b/src/models/response/billingResponse.ts @@ -2,16 +2,14 @@ import { PaymentMethodType } from '../../enums/paymentMethodType'; import { TransactionType } from '../../enums/transactionType'; export class BillingResponse { + balance: number; paymentSource: BillingSourceResponse; - charges: BillingChargeResponse[] = []; invoices: BillingInvoiceResponse[] = []; transactions: BillingTransactionResponse[] = []; constructor(response: any) { + this.balance = response.Balance; this.paymentSource = response.PaymentSource == null ? null : new BillingSourceResponse(response.PaymentSource); - if (response.Charges != null) { - this.charges = response.Charges.map((c: any) => new BillingChargeResponse(c)); - } if (response.Transactions != null) { this.transactions = response.Transactions.map((t: any) => new BillingTransactionResponse(t)); } @@ -35,30 +33,6 @@ export class BillingSourceResponse { } } -export class BillingChargeResponse { - createdDate: string; - amount: number; - paymentSource: BillingSourceResponse; - status: string; - failureMessage: string; - refunded: boolean; - partiallyRefunded: boolean; - refundedAmount: number; - invoiceId: string; - - constructor(response: any) { - this.createdDate = response.CreatedDate; - this.amount = response.Amount; - this.paymentSource = response.PaymentSource != null ? new BillingSourceResponse(response.PaymentSource) : null; - this.status = response.Status; - this.failureMessage = response.FailureMessage; - this.refunded = response.Refunded; - this.partiallyRefunded = response.PartiallyRefunded; - this.refundedAmount = response.RefundedAmount; - this.invoiceId = response.InvoiceId; - } -} - export class BillingInvoiceResponse { url: string; pdfUrl: string; From 9c44fc1329b9595560462ffb7cd4d32bb0e22a68 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 18 Feb 2019 23:53:31 -0500 Subject: [PATCH 0721/1626] remove country from org create req --- src/models/request/organizationCreateRequest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/models/request/organizationCreateRequest.ts b/src/models/request/organizationCreateRequest.ts index 3188afb3a1..7a7e92cf5d 100644 --- a/src/models/request/organizationCreateRequest.ts +++ b/src/models/request/organizationCreateRequest.ts @@ -11,5 +11,4 @@ export class OrganizationCreateRequest { additionalStorageGb: number; premiumAccessAddon: boolean; collectionName: string; - country: string; } From 36e65f08cad6cebdb5b90c88a68360296e8ff2ec Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 19 Feb 2019 17:05:27 -0500 Subject: [PATCH 0722/1626] payment method types --- src/enums/paymentMethodType.ts | 2 +- src/models/request/organizationCreateRequest.ts | 2 ++ src/models/request/paymentRequest.ts | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/enums/paymentMethodType.ts b/src/enums/paymentMethodType.ts index fcc50fa45e..7f91650b45 100644 --- a/src/enums/paymentMethodType.ts +++ b/src/enums/paymentMethodType.ts @@ -2,5 +2,5 @@ export enum PaymentMethodType { Card = 0, BankAccount = 1, PayPal = 2, - Bitcoin = 3, + BitPay = 3, } diff --git a/src/models/request/organizationCreateRequest.ts b/src/models/request/organizationCreateRequest.ts index 7a7e92cf5d..c7f7da6822 100644 --- a/src/models/request/organizationCreateRequest.ts +++ b/src/models/request/organizationCreateRequest.ts @@ -1,3 +1,4 @@ +import { PaymentMethodType } from '../../enums/paymentMethodType'; import { PlanType } from '../../enums/planType'; export class OrganizationCreateRequest { @@ -6,6 +7,7 @@ export class OrganizationCreateRequest { billingEmail: string; planType: PlanType; key: string; + paymentMethodType: PaymentMethodType; paymentToken: string; additionalSeats: number; additionalStorageGb: number; diff --git a/src/models/request/paymentRequest.ts b/src/models/request/paymentRequest.ts index 710fd18ffc..7ace99cd3a 100644 --- a/src/models/request/paymentRequest.ts +++ b/src/models/request/paymentRequest.ts @@ -1,3 +1,6 @@ +import { PaymentMethodType } from '../../enums/paymentMethodType'; + export class PaymentRequest { + paymentMethodType: PaymentMethodType; paymentToken: string; } From 2b931963cd8dbebdcbdd6a418ac3ef72adb73539 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 20 Feb 2019 20:15:37 -0500 Subject: [PATCH 0723/1626] credit payment method type --- src/enums/paymentMethodType.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/enums/paymentMethodType.ts b/src/enums/paymentMethodType.ts index 7f91650b45..fa5e5c169e 100644 --- a/src/enums/paymentMethodType.ts +++ b/src/enums/paymentMethodType.ts @@ -3,4 +3,5 @@ export enum PaymentMethodType { BankAccount = 1, PayPal = 2, BitPay = 3, + Credit = 4, } From 3362334d2ce2220fb0d1af322d88c9efec37763d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 21 Feb 2019 22:45:56 -0500 Subject: [PATCH 0724/1626] post bitpay invoice api --- src/abstractions/api.service.ts | 3 +++ src/models/request/bitPayInvoiceRequest.ts | 9 +++++++++ src/services/api.service.ts | 8 ++++++++ 3 files changed, 20 insertions(+) create mode 100644 src/models/request/bitPayInvoiceRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index ed77c77506..3de4ffce01 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -1,5 +1,6 @@ import { EnvironmentUrls } from '../models/domain/environmentUrls'; +import { BitPayInvoiceRequest } from '../models/request/bitPayInvoiceRequest'; import { CipherBulkDeleteRequest } from '../models/request/cipherBulkDeleteRequest'; import { CipherBulkMoveRequest } from '../models/request/cipherBulkMoveRequest'; import { CipherBulkShareRequest } from '../models/request/cipherBulkShareRequest'; @@ -253,6 +254,8 @@ export abstract class ApiService { getHibpBreach: (username: string) => Promise; + postBitPayInvoice: (request: BitPayInvoiceRequest) => Promise; + getActiveBearerToken: () => Promise; fetch: (request: Request) => Promise; } diff --git a/src/models/request/bitPayInvoiceRequest.ts b/src/models/request/bitPayInvoiceRequest.ts new file mode 100644 index 0000000000..2d1369ea6c --- /dev/null +++ b/src/models/request/bitPayInvoiceRequest.ts @@ -0,0 +1,9 @@ +export class BitPayInvoiceRequest { + userId: string; + organizationId: string; + credit: boolean; + amount: number; + returnUrl: string; + name: string; + email: string; +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 692ac69dc5..f7ecedba82 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -6,6 +6,7 @@ import { TokenService } from '../abstractions/token.service'; import { EnvironmentUrls } from '../models/domain/environmentUrls'; +import { BitPayInvoiceRequest } from '../models/request/bitPayInvoiceRequest'; import { CipherBulkDeleteRequest } from '../models/request/cipherBulkDeleteRequest'; import { CipherBulkMoveRequest } from '../models/request/cipherBulkMoveRequest'; import { CipherBulkShareRequest } from '../models/request/cipherBulkShareRequest'; @@ -837,6 +838,13 @@ export class ApiService implements ApiServiceAbstraction { return r.map((a: any) => new BreachAccountResponse(a)); } + // Misc + + async postBitPayInvoice(request: BitPayInvoiceRequest): Promise { + const r = await this.send('POST', '/bitpay-invoice', request, true, true); + return r as string; + } + // Helpers async getActiveBearerToken(): Promise { From 2b575f0c6064df6e4d8ad5ed4c76f084fcb3a898 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 22 Feb 2019 13:16:12 -0500 Subject: [PATCH 0725/1626] send lockedUrl message --- src/angular/services/auth-guard.service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/angular/services/auth-guard.service.ts b/src/angular/services/auth-guard.service.ts index 6956df71d1..9b6ebb263e 100644 --- a/src/angular/services/auth-guard.service.ts +++ b/src/angular/services/auth-guard.service.ts @@ -1,7 +1,9 @@ import { Injectable } from '@angular/core'; import { + ActivatedRouteSnapshot, CanActivate, Router, + RouterStateSnapshot, } from '@angular/router'; import { LockService } from '../../abstractions/lock.service'; @@ -13,7 +15,7 @@ export class AuthGuardService implements CanActivate { constructor(private lockService: LockService, private userService: UserService, private router: Router, private messagingService: MessagingService) { } - async canActivate() { + async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) { const isAuthed = await this.userService.isAuthenticated(); if (!isAuthed) { this.messagingService.send('logout'); @@ -22,6 +24,9 @@ export class AuthGuardService implements CanActivate { const locked = await this.lockService.isLocked(); if (locked) { + if (routerState != null) { + this.messagingService.send('lockedUrl', { url: routerState.url }); + } this.router.navigate(['lock']); return false; } From 3bdfc2baced8d2a0d959614c5076d0f4b4b87a81 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 22 Feb 2019 15:06:29 -0500 Subject: [PATCH 0726/1626] remove control characters from lastpass folder name --- src/importers/lastpassCsvImporter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/importers/lastpassCsvImporter.ts b/src/importers/lastpassCsvImporter.ts index a78b23c6c5..0c017fb856 100644 --- a/src/importers/lastpassCsvImporter.ts +++ b/src/importers/lastpassCsvImporter.ts @@ -27,7 +27,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { let folderIndex = result.folders.length; let grouping = value.grouping; if (grouping != null) { - grouping = grouping.replace(/\\/g, '/'); + grouping = grouping.replace(/\\/g, '/').replace(/[\x00-\x1F\x7F-\x9F]/g, ''); } const hasFolder = this.getValueOrDefault(grouping, '(none)') !== '(none)'; let addFolder = hasFolder; From d0a0da8ee9ec4869001be96bc362e537ae15f961 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 22 Feb 2019 15:36:03 -0500 Subject: [PATCH 0727/1626] send deletedCipher message --- src/angular/components/add-edit.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 8cd755cfa4..94f6ba3b16 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -298,6 +298,7 @@ export class AddEditComponent implements OnInit { this.platformUtilsService.eventTrack('Deleted Cipher'); this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedItem')); this.onDeletedCipher.emit(this.cipher); + this.messagingService.send('deletedCipher'); } catch { } return true; From fc2f64ee367b65e35e683ece0c91c05b5fb83bc5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 25 Feb 2019 09:19:36 -0500 Subject: [PATCH 0728/1626] added wiretransfer payment type --- src/enums/paymentMethodType.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/enums/paymentMethodType.ts b/src/enums/paymentMethodType.ts index fa5e5c169e..cd8dedf661 100644 --- a/src/enums/paymentMethodType.ts +++ b/src/enums/paymentMethodType.ts @@ -4,4 +4,5 @@ export enum PaymentMethodType { PayPal = 2, BitPay = 3, Credit = 4, + WireTransfer = 5, } From 9a4611ec5a650362abc7f5616fd7006f950c5b3d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 25 Feb 2019 15:07:19 -0500 Subject: [PATCH 0729/1626] lock reload --- src/abstractions/lock.service.ts | 2 ++ src/services/lock.service.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/abstractions/lock.service.ts b/src/abstractions/lock.service.ts index ede31db0b1..e40bc00281 100644 --- a/src/abstractions/lock.service.ts +++ b/src/abstractions/lock.service.ts @@ -6,4 +6,6 @@ export abstract class LockService { setLockOption: (lockOption: number) => Promise; isPinLockSet: () => Promise<[boolean, boolean]>; clear: () => Promise; + startLockReload: () => void; + cancelLockReload: () => void; } diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts index 941e86f249..14c13a1a08 100644 --- a/src/services/lock.service.ts +++ b/src/services/lock.service.ts @@ -14,6 +14,7 @@ export class LockService implements LockServiceAbstraction { pinLocked = false; private inited = false; + private reloadInterval: any = null; constructor(private cipherService: CipherService, private folderService: FolderService, private collectionService: CollectionService, private cryptoService: CryptoService, @@ -117,4 +118,31 @@ export class LockService implements LockServiceAbstraction { clear(): Promise { return this.storageService.remove(ConstantsService.protectedPin); } + + startLockReload(): void { + if (this.pinLocked || this.reloadInterval != null) { + return; + } + this.reloadInterval = setInterval(async () => { + let doRefresh = false; + const lastActive = await this.storageService.get(ConstantsService.lastActiveKey); + if (lastActive != null) { + const diffSeconds = (new Date()).getTime() - lastActive; + // Don't refresh if they are still active in the window + doRefresh = diffSeconds >= 5000; + } + if (doRefresh) { + clearInterval(this.reloadInterval); + this.reloadInterval = null; + this.messagingService.send('reloadProcess'); + } + }, 10000); + } + + cancelLockReload(): void { + if (this.reloadInterval != null) { + clearInterval(this.reloadInterval); + this.reloadInterval = null; + } + } } From 79c15a18414190ffce39a126ccc9078405287693 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 25 Feb 2019 16:14:54 -0500 Subject: [PATCH 0730/1626] reload callback --- src/services/lock.service.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts index 14c13a1a08..bf07401861 100644 --- a/src/services/lock.service.ts +++ b/src/services/lock.service.ts @@ -20,7 +20,7 @@ export class LockService implements LockServiceAbstraction { private collectionService: CollectionService, private cryptoService: CryptoService, private platformUtilsService: PlatformUtilsService, private storageService: StorageService, private messagingService: MessagingService, private searchService: SearchService, - private lockedCallback: () => Promise) { + private lockedCallback: () => Promise = null, private reloadCallback: () => Promise = null) { } init(checkOnInterval: boolean) { @@ -49,7 +49,7 @@ export class LockService implements LockServiceAbstraction { return; } - if (this.isLocked()) { + if (await this.isLocked()) { return; } @@ -135,6 +135,9 @@ export class LockService implements LockServiceAbstraction { clearInterval(this.reloadInterval); this.reloadInterval = null; this.messagingService.send('reloadProcess'); + if (this.reloadCallback != null) { + await this.reloadCallback(); + } } }, 10000); } From dd2be223518ca010249c6e47bc0b3e858738b181 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 26 Feb 2019 16:45:26 -0500 Subject: [PATCH 0731/1626] trim slashes --- src/services/collection.service.ts | 4 ++-- src/services/folder.service.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index ef7d602c8e..842f5614cb 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -107,8 +107,8 @@ export class CollectionService implements CollectionServiceAbstraction { const collectionCopy = new CollectionView(); collectionCopy.id = c.id; collectionCopy.organizationId = c.organizationId; - ServiceUtils.nestedTraverse(nodes, 0, c.name.split(NestingDelimiter), collectionCopy, - null, NestingDelimiter); + ServiceUtils.nestedTraverse(nodes, 0, c.name.replace(/^\/+|\/+$/g, '').split(NestingDelimiter), + collectionCopy, null, NestingDelimiter); }); return nodes; } diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index 70fc7f1e71..ba64f0e53a 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -105,8 +105,8 @@ export class FolderService implements FolderServiceAbstraction { const folderCopy = new FolderView(); folderCopy.id = f.id; folderCopy.revisionDate = f.revisionDate; - ServiceUtils.nestedTraverse(nodes, 0, f.name.split(NestingDelimiter), folderCopy, - null, NestingDelimiter); + ServiceUtils.nestedTraverse(nodes, 0, f.name.replace(/^\/+|\/+$/g, '').split(NestingDelimiter), + folderCopy, null, NestingDelimiter); }); return nodes; } From 21d011554c1c7d5e497241965bc775cfc1001d16 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 26 Feb 2019 22:35:53 -0500 Subject: [PATCH 0732/1626] readFromClipboard util --- src/abstractions/platformUtils.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index 14131a13c4..38204ab899 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -28,4 +28,5 @@ export abstract class PlatformUtilsService { isDev: () => boolean; isSelfHost: () => boolean; copyToClipboard: (text: string, options?: any) => void; + readFromClipboard: (options?: any) => Promise; } From 68f7557e44766ae1d9596e2c06877a51f34cb7a0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 26 Feb 2019 22:40:53 -0500 Subject: [PATCH 0733/1626] implement readFromClipboard for electron utils --- src/electron/services/electronPlatformUtils.service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index 536de73617..ac660afc65 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -188,4 +188,9 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { const type = options ? options.type : null; clipboard.writeText(text, type); } + + readFromClipboard(options?: any): Promise { + const type = options ? options.type : null; + return Promise.resolve(clipboard.readText(type)); + } } From 808437ab06c79793616a0370b7919d846a56ee34 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 27 Feb 2019 09:21:58 -0500 Subject: [PATCH 0734/1626] system service for proc reload and clear clipboard --- src/abstractions/lock.service.ts | 2 - src/abstractions/system.service.ts | 5 +++ src/services/lock.service.ts | 33 +-------------- src/services/system.service.ts | 66 ++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 34 deletions(-) create mode 100644 src/abstractions/system.service.ts create mode 100644 src/services/system.service.ts diff --git a/src/abstractions/lock.service.ts b/src/abstractions/lock.service.ts index e40bc00281..ede31db0b1 100644 --- a/src/abstractions/lock.service.ts +++ b/src/abstractions/lock.service.ts @@ -6,6 +6,4 @@ export abstract class LockService { setLockOption: (lockOption: number) => Promise; isPinLockSet: () => Promise<[boolean, boolean]>; clear: () => Promise; - startLockReload: () => void; - cancelLockReload: () => void; } diff --git a/src/abstractions/system.service.ts b/src/abstractions/system.service.ts new file mode 100644 index 0000000000..cf4f3e3133 --- /dev/null +++ b/src/abstractions/system.service.ts @@ -0,0 +1,5 @@ +export abstract class SystemService { + startProcessReload: () => void; + cancelProcessReload: () => void; + clearClipboard: (clipboardValue: string, timeoutMs?: number) => void; +} diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts index bf07401861..230fd33d10 100644 --- a/src/services/lock.service.ts +++ b/src/services/lock.service.ts @@ -14,13 +14,12 @@ export class LockService implements LockServiceAbstraction { pinLocked = false; private inited = false; - private reloadInterval: any = null; constructor(private cipherService: CipherService, private folderService: FolderService, private collectionService: CollectionService, private cryptoService: CryptoService, private platformUtilsService: PlatformUtilsService, private storageService: StorageService, private messagingService: MessagingService, private searchService: SearchService, - private lockedCallback: () => Promise = null, private reloadCallback: () => Promise = null) { + private lockedCallback: () => Promise = null) { } init(checkOnInterval: boolean) { @@ -118,34 +117,4 @@ export class LockService implements LockServiceAbstraction { clear(): Promise { return this.storageService.remove(ConstantsService.protectedPin); } - - startLockReload(): void { - if (this.pinLocked || this.reloadInterval != null) { - return; - } - this.reloadInterval = setInterval(async () => { - let doRefresh = false; - const lastActive = await this.storageService.get(ConstantsService.lastActiveKey); - if (lastActive != null) { - const diffSeconds = (new Date()).getTime() - lastActive; - // Don't refresh if they are still active in the window - doRefresh = diffSeconds >= 5000; - } - if (doRefresh) { - clearInterval(this.reloadInterval); - this.reloadInterval = null; - this.messagingService.send('reloadProcess'); - if (this.reloadCallback != null) { - await this.reloadCallback(); - } - } - }, 10000); - } - - cancelLockReload(): void { - if (this.reloadInterval != null) { - clearInterval(this.reloadInterval); - this.reloadInterval = null; - } - } } diff --git a/src/services/system.service.ts b/src/services/system.service.ts new file mode 100644 index 0000000000..73e1dbdbc7 --- /dev/null +++ b/src/services/system.service.ts @@ -0,0 +1,66 @@ +import { LockService } from '../abstractions/lock.service'; +import { MessagingService } from '../abstractions/messaging.service'; +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; +import { StorageService } from '../abstractions/storage.service'; +import { SystemService as SystemServiceAbstraction } from '../abstractions/system.service'; + +import { ConstantsService } from './constants.service'; + +import { Utils } from '../misc/utils'; + +export class SystemService implements SystemServiceAbstraction { + private reloadInterval: any = null; + private clearClipboardTimeout: any = null; + + constructor(private storageService: StorageService, private lockService: LockService, + private messagingService: MessagingService, private platformUtilsService: PlatformUtilsService, + private reloadCallback: () => Promise = null) { + } + + startProcessReload(): void { + if (this.lockService.pinLocked || this.reloadInterval != null) { + return; + } + this.cancelProcessReload(); + this.reloadInterval = setInterval(async () => { + let doRefresh = false; + const lastActive = await this.storageService.get(ConstantsService.lastActiveKey); + if (lastActive != null) { + const diffSeconds = (new Date()).getTime() - lastActive; + // Don't refresh if they are still active in the window + doRefresh = diffSeconds >= 5000; + } + if (doRefresh) { + clearInterval(this.reloadInterval); + this.reloadInterval = null; + this.messagingService.send('reloadProcess'); + if (this.reloadCallback != null) { + await this.reloadCallback(); + } + } + }, 10000); + } + + cancelProcessReload(): void { + if (this.reloadInterval != null) { + clearInterval(this.reloadInterval); + this.reloadInterval = null; + } + } + + clearClipboard(clipboardValue: string, timeoutMs = 5000): void { + if (this.clearClipboardTimeout != null) { + clearTimeout(this.clearClipboardTimeout); + this.clearClipboardTimeout = null; + } + if (Utils.isNullOrWhitespace(clipboardValue)) { + return; + } + this.clearClipboardTimeout = setTimeout(async () => { + const clipboardValueNow = await this.platformUtilsService.readFromClipboard(); + if (clipboardValue === clipboardValueNow) { + this.platformUtilsService.copyToClipboard(''); + } + }, timeoutMs); + } +} From 8aa2f0fb183ad8f30a9fb5396b63e6fff5b87e12 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 27 Feb 2019 09:22:14 -0500 Subject: [PATCH 0735/1626] vlipboard clear timeout is 30 sec --- src/services/system.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/system.service.ts b/src/services/system.service.ts index 73e1dbdbc7..0bbbd5b805 100644 --- a/src/services/system.service.ts +++ b/src/services/system.service.ts @@ -48,7 +48,7 @@ export class SystemService implements SystemServiceAbstraction { } } - clearClipboard(clipboardValue: string, timeoutMs = 5000): void { + clearClipboard(clipboardValue: string, timeoutMs = 30000): void { if (this.clearClipboardTimeout != null) { clearTimeout(this.clearClipboardTimeout); this.clearClipboardTimeout = null; From d4fab1c6976f6b47fff89ef89404d6fcc7583021 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 27 Feb 2019 11:06:55 -0500 Subject: [PATCH 0736/1626] clear clipboard setting --- src/services/constants.service.ts | 2 ++ src/services/system.service.ts | 20 ++++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index 755e3335e8..4ec0fc0e08 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -20,6 +20,7 @@ export class ConstantsService { static readonly defaultUriMatch: string = 'defaultUriMatch'; static readonly pinProtectedKey: string = 'pinProtectedKey'; static readonly protectedPin: string = 'protectedPin'; + static readonly clearClipboardKey: string = 'clearClipboardKey'; readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; readonly disableGaKey: string = ConstantsService.disableGaKey; @@ -41,4 +42,5 @@ export class ConstantsService { readonly defaultUriMatch: string = ConstantsService.defaultUriMatch; readonly pinProtectedKey: string = ConstantsService.pinProtectedKey; readonly protectedPin: string = ConstantsService.protectedPin; + readonly clearClipboardKey: string = ConstantsService.clearClipboardKey; } diff --git a/src/services/system.service.ts b/src/services/system.service.ts index 0bbbd5b805..310dfacb4d 100644 --- a/src/services/system.service.ts +++ b/src/services/system.service.ts @@ -48,7 +48,7 @@ export class SystemService implements SystemServiceAbstraction { } } - clearClipboard(clipboardValue: string, timeoutMs = 30000): void { + clearClipboard(clipboardValue: string, timeoutMs: number = null): void { if (this.clearClipboardTimeout != null) { clearTimeout(this.clearClipboardTimeout); this.clearClipboardTimeout = null; @@ -56,11 +56,19 @@ export class SystemService implements SystemServiceAbstraction { if (Utils.isNullOrWhitespace(clipboardValue)) { return; } - this.clearClipboardTimeout = setTimeout(async () => { - const clipboardValueNow = await this.platformUtilsService.readFromClipboard(); - if (clipboardValue === clipboardValueNow) { - this.platformUtilsService.copyToClipboard(''); + this.storageService.get(ConstantsService.clearClipboardKey).then((clearSeconds) => { + if (clearSeconds == null) { + return; } - }, timeoutMs); + if (timeoutMs == null) { + timeoutMs = clearSeconds * 1000; + } + this.clearClipboardTimeout = setTimeout(async () => { + const clipboardValueNow = await this.platformUtilsService.readFromClipboard(); + if (clipboardValue === clipboardValueNow) { + this.platformUtilsService.copyToClipboard(''); + } + }, timeoutMs); + }); } } From b9267c521ddffce35932a0cc2e9726d30b80c60d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 27 Feb 2019 11:26:35 -0500 Subject: [PATCH 0737/1626] copied value to clipboard message --- src/electron/services/electronPlatformUtils.service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index ac660afc65..b2b01750e0 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -186,7 +186,12 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { copyToClipboard(text: string, options?: any): void { const type = options ? options.type : null; + const clearMs: number = options && options.clearMs ? options.clearMs : null; clipboard.writeText(text, type); + this.messagingService.send('copiedToClipboard', { + clipboardValue: text, + clearMs: clearMs, + }); } readFromClipboard(options?: any): Promise { From 62e9c75357b211db6de769d8592224fc846638aa Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 27 Feb 2019 11:56:17 -0500 Subject: [PATCH 0738/1626] clearPendingClipboard function --- src/abstractions/system.service.ts | 1 + src/services/system.service.ts | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/abstractions/system.service.ts b/src/abstractions/system.service.ts index cf4f3e3133..c8948bc11b 100644 --- a/src/abstractions/system.service.ts +++ b/src/abstractions/system.service.ts @@ -2,4 +2,5 @@ export abstract class SystemService { startProcessReload: () => void; cancelProcessReload: () => void; clearClipboard: (clipboardValue: string, timeoutMs?: number) => void; + clearPendingClipboard: () => Promise; } diff --git a/src/services/system.service.ts b/src/services/system.service.ts index 310dfacb4d..1e492c077f 100644 --- a/src/services/system.service.ts +++ b/src/services/system.service.ts @@ -11,6 +11,7 @@ import { Utils } from '../misc/utils'; export class SystemService implements SystemServiceAbstraction { private reloadInterval: any = null; private clearClipboardTimeout: any = null; + private clearClipboardTimeoutFunction: () => Promise = null; constructor(private storageService: StorageService, private lockService: LockService, private messagingService: MessagingService, private platformUtilsService: PlatformUtilsService, @@ -63,12 +64,22 @@ export class SystemService implements SystemServiceAbstraction { if (timeoutMs == null) { timeoutMs = clearSeconds * 1000; } - this.clearClipboardTimeout = setTimeout(async () => { + this.clearClipboardTimeoutFunction = async () => { const clipboardValueNow = await this.platformUtilsService.readFromClipboard(); if (clipboardValue === clipboardValueNow) { this.platformUtilsService.copyToClipboard(''); } + }; + this.clearClipboardTimeout = setTimeout(async () => { + await this.clearPendingClipboard(); }, timeoutMs); }); } + + async clearPendingClipboard() { + if (this.clearClipboardTimeoutFunction != null) { + await this.clearClipboardTimeoutFunction(); + this.clearClipboardTimeoutFunction = null; + } + } } From 48164a31d93dea61af867c7d6be06344d417431c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 1 Mar 2019 00:13:37 -0500 Subject: [PATCH 0739/1626] support camel or pascal case in API responses --- src/models/api/cardApi.ts | 22 ++-- src/models/api/fieldApi.ts | 16 ++- src/models/api/identityApi.ts | 46 ++++--- src/models/api/loginApi.ts | 26 ++-- src/models/api/loginUriApi.ts | 15 ++- src/models/api/secureNoteApi.ts | 12 +- src/models/request/cipherRequest.ts | 121 +++++++++--------- src/models/response/attachmentResponse.ts | 17 ++- src/models/response/baseResponse.ts | 36 ++++++ src/models/response/billingResponse.ts | 65 ++++++---- src/models/response/breachAccountResponse.ts | 29 +++-- src/models/response/cipherResponse.ts | 76 +++++------ src/models/response/collectionResponse.ts | 17 ++- src/models/response/deviceResponse.ts | 15 ++- src/models/response/domainsResponse.ts | 17 +-- src/models/response/errorResponse.ts | 14 +- src/models/response/eventResponse.ts | 27 ++-- src/models/response/folderResponse.ts | 11 +- src/models/response/globalDomainResponse.ts | 11 +- src/models/response/groupResponse.ts | 19 +-- src/models/response/identityTokenResponse.ts | 11 +- .../response/identityTwoFactorResponse.ts | 16 ++- src/models/response/keysResponse.ts | 9 +- src/models/response/listResponse.ts | 10 +- src/models/response/notificationResponse.ts | 40 +++--- src/models/response/organizationResponse.ts | 43 ++++--- .../organizationSubscriptionResponse.ts | 15 ++- .../response/organizationUserResponse.ts | 26 ++-- .../response/passwordHistoryResponse.ts | 9 +- src/models/response/preloginResponse.ts | 9 +- .../response/profileOrganizationResponse.ts | 37 +++--- src/models/response/profileResponse.ts | 33 ++--- .../response/selectionReadOnlyResponse.ts | 9 +- src/models/response/subscriptionResponse.ts | 66 +++++----- src/models/response/syncResponse.ts | 36 +++--- .../twoFactorAuthenticatorResponse.ts | 9 +- src/models/response/twoFactorDuoResponse.ts | 13 +- src/models/response/twoFactorEmailResponse.ts | 9 +- .../response/twoFactorProviderResponse.ts | 9 +- .../response/twoFactorRescoverResponse.ts | 7 +- src/models/response/twoFactorU2fResponse.ts | 30 +++-- .../response/twoFactorYubiKeyResponse.ts | 19 +-- src/models/response/userKeyResponse.ts | 9 +- 43 files changed, 637 insertions(+), 449 deletions(-) create mode 100644 src/models/response/baseResponse.ts diff --git a/src/models/api/cardApi.ts b/src/models/api/cardApi.ts index f91c44ef16..b837bb90f4 100644 --- a/src/models/api/cardApi.ts +++ b/src/models/api/cardApi.ts @@ -1,4 +1,6 @@ -export class CardApi { +import { BaseResponse } from '../response/baseResponse'; + +export class CardApi extends BaseResponse { cardholderName: string; brand: string; number: string; @@ -6,12 +8,16 @@ export class CardApi { 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; + constructor(data: any = null) { + super(data); + if (data == null) { + return; + } + this.cardholderName = this.getResponseProperty('CardholderName'); + this.brand = this.getResponseProperty('Brand'); + this.number = this.getResponseProperty('Number'); + this.expMonth = this.getResponseProperty('ExpMonth'); + this.expYear = this.getResponseProperty('ExpYear'); + this.code = this.getResponseProperty('Code'); } } diff --git a/src/models/api/fieldApi.ts b/src/models/api/fieldApi.ts index 59a8a06aaa..b7a024ccaa 100644 --- a/src/models/api/fieldApi.ts +++ b/src/models/api/fieldApi.ts @@ -1,13 +1,19 @@ +import { BaseResponse } from '../response/baseResponse'; + import { FieldType } from '../../enums/fieldType'; -export class FieldApi { +export class FieldApi extends BaseResponse { name: string; value: string; type: FieldType; - constructor(response: any) { - this.type = response.Type; - this.name = response.Name; - this.value = response.Value; + constructor(data: any = null) { + super(data); + if (data == null) { + return; + } + this.type = this.getResponseProperty('Type'); + this.name = this.getResponseProperty('Name'); + this.value = this.getResponseProperty('Value'); } } diff --git a/src/models/api/identityApi.ts b/src/models/api/identityApi.ts index e498eb71fe..e271e5e3fa 100644 --- a/src/models/api/identityApi.ts +++ b/src/models/api/identityApi.ts @@ -1,4 +1,6 @@ -export class IdentityApi { +import { BaseResponse } from '../response/baseResponse'; + +export class IdentityApi extends BaseResponse { title: string; firstName: string; middleName: string; @@ -18,24 +20,28 @@ export class IdentityApi { 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; + constructor(data: any = null) { + super(data); + if (data == null) { + return; + } + this.title = this.getResponseProperty('Title'); + this.firstName = this.getResponseProperty('FirstName'); + this.middleName = this.getResponseProperty('MiddleName'); + this.lastName = this.getResponseProperty('LastName'); + this.address1 = this.getResponseProperty('Address1'); + this.address2 = this.getResponseProperty('Address2'); + this.address3 = this.getResponseProperty('Address3'); + this.city = this.getResponseProperty('City'); + this.state = this.getResponseProperty('State'); + this.postalCode = this.getResponseProperty('PostalCode'); + this.country = this.getResponseProperty('Country'); + this.company = this.getResponseProperty('Company'); + this.email = this.getResponseProperty('Email'); + this.phone = this.getResponseProperty('Phone'); + this.ssn = this.getResponseProperty('SSN'); + this.username = this.getResponseProperty('Username'); + this.passportNumber = this.getResponseProperty('PassportNumber'); + this.licenseNumber = this.getResponseProperty('LicenseNumber'); } } diff --git a/src/models/api/loginApi.ts b/src/models/api/loginApi.ts index 2f9f5579af..c71a4cec25 100644 --- a/src/models/api/loginApi.ts +++ b/src/models/api/loginApi.ts @@ -1,23 +1,27 @@ +import { BaseResponse } from '../response/baseResponse'; + import { LoginUriApi } from './loginUriApi'; -export class LoginApi { +export class LoginApi extends BaseResponse { uris: LoginUriApi[]; username: string; password: string; passwordRevisionDate: string; totp: string; - constructor(data: any) { - this.username = data.Username; - this.password = data.Password; - this.passwordRevisionDate = data.PasswordRevisionDate; - this.totp = data.Totp; + constructor(data: any = null) { + super(data); + if (data == null) { + return; + } + this.username = this.getResponseProperty('Username'); + this.password = this.getResponseProperty('Password'); + this.passwordRevisionDate = this.getResponseProperty('PasswordRevisionDate'); + this.totp = this.getResponseProperty('Totp'); - if (data.Uris) { - this.uris = []; - data.Uris.forEach((u: any) => { - this.uris.push(new LoginUriApi(u)); - }); + const uris = this.getResponseProperty('Uris'); + if (uris != null) { + this.uris = uris.map((u: any) => new LoginUriApi(u)); } } } diff --git a/src/models/api/loginUriApi.ts b/src/models/api/loginUriApi.ts index b61c673421..6ea8d4e2c5 100644 --- a/src/models/api/loginUriApi.ts +++ b/src/models/api/loginUriApi.ts @@ -1,11 +1,18 @@ +import { BaseResponse } from '../response/baseResponse'; + import { UriMatchType } from '../../enums/uriMatchType'; -export class LoginUriApi { +export class LoginUriApi extends BaseResponse { uri: string; match: UriMatchType = null; - constructor(data: any) { - this.uri = data.Uri; - this.match = data.Match != null ? data.Match : null; + constructor(data: any = null) { + super(data); + if (data == null) { + return; + } + this.uri = this.getResponseProperty('Uri'); + const match = this.getResponseProperty('Match'); + this.match = match != null ? match : null; } } diff --git a/src/models/api/secureNoteApi.ts b/src/models/api/secureNoteApi.ts index 4a31aa27b2..2fbdd94b0c 100644 --- a/src/models/api/secureNoteApi.ts +++ b/src/models/api/secureNoteApi.ts @@ -1,9 +1,15 @@ +import { BaseResponse } from '../response/baseResponse'; + import { SecureNoteType } from '../../enums/secureNoteType'; -export class SecureNoteApi { +export class SecureNoteApi extends BaseResponse { type: SecureNoteType; - constructor(data: any) { - this.type = data.Type; + constructor(data: any = null) { + super(data); + if (data == null) { + return; + } + this.type = this.getResponseProperty('Type'); } } diff --git a/src/models/request/cipherRequest.ts b/src/models/request/cipherRequest.ts index 46717dd6a8..9333e753f5 100644 --- a/src/models/request/cipherRequest.ts +++ b/src/models/request/cipherRequest.ts @@ -6,6 +6,7 @@ import { CardApi } from '../api/cardApi'; import { FieldApi } from '../api/fieldApi'; import { IdentityApi } from '../api/identityApi'; import { LoginApi } from '../api/loginApi'; +import { LoginUriApi } from '../api/loginUriApi'; import { SecureNoteApi } from '../api/secureNoteApi'; import { AttachmentRequest } from './attachmentRequest'; @@ -38,79 +39,85 @@ export class CipherRequest { switch (this.type) { case CipherType.Login: - this.login = { - uris: null, - username: cipher.login.username ? cipher.login.username.encryptedString : null, - password: cipher.login.password ? cipher.login.password.encryptedString : null, - passwordRevisionDate: cipher.login.passwordRevisionDate != null ? - cipher.login.passwordRevisionDate.toISOString() : null, - totp: cipher.login.totp ? cipher.login.totp.encryptedString : null, - }; + this.login = new LoginApi(); + this.login.uris = null; + this.login.username = cipher.login.username ? cipher.login.username.encryptedString : null; + this.login.password = cipher.login.password ? cipher.login.password.encryptedString : null; + this.login.passwordRevisionDate = cipher.login.passwordRevisionDate != null ? + cipher.login.passwordRevisionDate.toISOString() : null; + this.login.totp = cipher.login.totp ? cipher.login.totp.encryptedString : null; - if (cipher.login.uris) { - this.login.uris = []; - cipher.login.uris.forEach((u) => { - this.login.uris.push({ - uri: u.uri ? u.uri.encryptedString : null, - match: u.match != null ? u.match : null, - }); + if (cipher.login.uris != null) { + this.login.uris = cipher.login.uris.map((u) => { + const uri = new LoginUriApi(); + uri.uri = u.uri != null ? u.uri.encryptedString : null; + uri.match = u.match != null ? u.match : null; + return uri; }); } break; case CipherType.SecureNote: - this.secureNote = { - type: cipher.secureNote.type, - }; + this.secureNote = new SecureNoteApi(); + 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, - }; + this.card = new CardApi(); + this.card.cardholderName = cipher.card.cardholderName != null ? + cipher.card.cardholderName.encryptedString : null; + this.card.brand = cipher.card.brand != null ? cipher.card.brand.encryptedString : null; + this.card.number = cipher.card.number != null ? cipher.card.number.encryptedString : null; + this.card.expMonth = cipher.card.expMonth != null ? cipher.card.expMonth.encryptedString : null; + this.card.expYear = cipher.card.expYear != null ? cipher.card.expYear.encryptedString : null; + this.card.code = cipher.card.code != null ? 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, - }; + this.identity = new IdentityApi(); + this.identity.title = cipher.identity.title != null ? cipher.identity.title.encryptedString : null; + this.identity.firstName = cipher.identity.firstName != null ? + cipher.identity.firstName.encryptedString : null; + this.identity.middleName = cipher.identity.middleName != null ? + cipher.identity.middleName.encryptedString : null; + this.identity.lastName = cipher.identity.lastName != null ? + cipher.identity.lastName.encryptedString : null; + this.identity.address1 = cipher.identity.address1 != null ? + cipher.identity.address1.encryptedString : null; + this.identity.address2 = cipher.identity.address2 != null ? + cipher.identity.address2.encryptedString : null; + this.identity.address3 = cipher.identity.address3 != null ? + cipher.identity.address3.encryptedString : null; + this.identity.city = cipher.identity.city != null ? cipher.identity.city.encryptedString : null; + this.identity.state = cipher.identity.state != null ? cipher.identity.state.encryptedString : null; + this.identity.postalCode = cipher.identity.postalCode != null ? + cipher.identity.postalCode.encryptedString : null; + this.identity.country = cipher.identity.country != null ? + cipher.identity.country.encryptedString : null; + this.identity.company = cipher.identity.company != null ? + cipher.identity.company.encryptedString : null; + this.identity.email = cipher.identity.email != null ? cipher.identity.email.encryptedString : null; + this.identity.phone = cipher.identity.phone != null ? cipher.identity.phone.encryptedString : null; + this.identity.ssn = cipher.identity.ssn != null ? cipher.identity.ssn.encryptedString : null; + this.identity.username = cipher.identity.username != null ? + cipher.identity.username.encryptedString : null; + this.identity.passportNumber = cipher.identity.passportNumber != null ? + cipher.identity.passportNumber.encryptedString : null; + this.identity.licenseNumber = cipher.identity.licenseNumber != null ? + cipher.identity.licenseNumber.encryptedString : null; break; default: break; } - if (cipher.fields) { - this.fields = []; - cipher.fields.forEach((field) => { - this.fields.push({ - type: field.type, - name: field.name ? field.name.encryptedString : null, - value: field.value ? field.value.encryptedString : null, - }); + if (cipher.fields != null) { + this.fields = cipher.fields.map((f) => { + const field = new FieldApi(); + field.type = f.type; + field.name = f.name ? f.name.encryptedString : null; + field.value = f.value ? f.value.encryptedString : null; + return field; }); } - if (cipher.passwordHistory) { + if (cipher.passwordHistory != null) { this.passwordHistory = []; cipher.passwordHistory.forEach((ph) => { this.passwordHistory.push({ @@ -120,7 +127,7 @@ export class CipherRequest { }); } - if (cipher.attachments) { + if (cipher.attachments != null) { this.attachments = {}; this.attachments2 = {}; cipher.attachments.forEach((attachment) => { diff --git a/src/models/response/attachmentResponse.ts b/src/models/response/attachmentResponse.ts index fd45c709b6..241fba02c2 100644 --- a/src/models/response/attachmentResponse.ts +++ b/src/models/response/attachmentResponse.ts @@ -1,4 +1,6 @@ -export class AttachmentResponse { +import { BaseResponse } from './baseResponse'; + +export class AttachmentResponse extends BaseResponse { id: string; url: string; fileName: string; @@ -7,11 +9,12 @@ export class AttachmentResponse { sizeName: string; constructor(response: any) { - this.id = response.Id; - this.url = response.Url; - this.fileName = response.FileName; - this.key = response.Key; - this.size = response.Size; - this.sizeName = response.SizeName; + super(response); + this.id = this.getResponseProperty('Id'); + this.url = this.getResponseProperty('Url'); + this.fileName = this.getResponseProperty('FileName'); + this.key = this.getResponseProperty('Key'); + this.size = this.getResponseProperty('Size'); + this.sizeName = this.getResponseProperty('SizeName'); } } diff --git a/src/models/response/baseResponse.ts b/src/models/response/baseResponse.ts new file mode 100644 index 0000000000..b4478bff3d --- /dev/null +++ b/src/models/response/baseResponse.ts @@ -0,0 +1,36 @@ +export abstract class BaseResponse { + protected response: any; + + constructor(response: any) { + this.response = response; + } + + protected getResponseProperty(propertyName: string, response: any = null, exactName = false): any { + if (propertyName == null || propertyName === '') { + throw new Error('propertyName must not be null/empty.'); + } + if (response == null) { + response = this.response; + } + if (!exactName && response[propertyName] === undefined) { + let otherCasePropertyName: string = null; + if (propertyName.charAt(0) === propertyName.charAt(0).toUpperCase()) { + otherCasePropertyName = propertyName.charAt(0).toLowerCase(); + } else { + otherCasePropertyName = propertyName.charAt(0).toUpperCase(); + } + if (propertyName.length > 1) { + otherCasePropertyName += propertyName.slice(1); + } + + propertyName = otherCasePropertyName; + if (response[propertyName] === undefined) { + propertyName = propertyName.toLowerCase(); + } + if (response[propertyName] === undefined) { + propertyName = propertyName.toUpperCase(); + } + } + return response[propertyName]; + } +} diff --git a/src/models/response/billingResponse.ts b/src/models/response/billingResponse.ts index c1c605c435..36954db664 100644 --- a/src/models/response/billingResponse.ts +++ b/src/models/response/billingResponse.ts @@ -1,39 +1,46 @@ +import { BaseResponse } from './baseResponse'; + import { PaymentMethodType } from '../../enums/paymentMethodType'; import { TransactionType } from '../../enums/transactionType'; -export class BillingResponse { +export class BillingResponse extends BaseResponse { balance: number; paymentSource: BillingSourceResponse; invoices: BillingInvoiceResponse[] = []; transactions: BillingTransactionResponse[] = []; constructor(response: any) { - this.balance = response.Balance; - this.paymentSource = response.PaymentSource == null ? null : new BillingSourceResponse(response.PaymentSource); - if (response.Transactions != null) { - this.transactions = response.Transactions.map((t: any) => new BillingTransactionResponse(t)); + super(response); + this.balance = this.getResponseProperty('Balance'); + const paymentSource = this.getResponseProperty('PaymentSource'); + const transactions = this.getResponseProperty('Transactions'); + const invoices = this.getResponseProperty('Invoices'); + this.paymentSource = paymentSource == null ? null : new BillingSourceResponse(paymentSource); + if (transactions != null) { + this.transactions = transactions.map((t: any) => new BillingTransactionResponse(t)); } - if (response.Invoices != null) { - this.invoices = response.Invoices.map((i: any) => new BillingInvoiceResponse(i)); + if (invoices != null) { + this.invoices = invoices.map((i: any) => new BillingInvoiceResponse(i)); } } } -export class BillingSourceResponse { +export class BillingSourceResponse extends BaseResponse { type: PaymentMethodType; cardBrand: string; description: string; needsVerification: boolean; constructor(response: any) { - this.type = response.Type; - this.cardBrand = response.CardBrand; - this.description = response.Description; - this.needsVerification = response.NeedsVerification; + super(response); + this.type = this.getResponseProperty('Type'); + this.cardBrand = this.getResponseProperty('CardBrand'); + this.description = this.getResponseProperty('Description'); + this.needsVerification = this.getResponseProperty('NeedsVerification'); } } -export class BillingInvoiceResponse { +export class BillingInvoiceResponse extends BaseResponse { url: string; pdfUrl: string; number: string; @@ -42,16 +49,17 @@ export class BillingInvoiceResponse { amount: number; constructor(response: any) { - this.url = response.Url; - this.pdfUrl = response.PdfUrl; - this.number = response.Number; - this.paid = response.Paid; - this.date = response.Date; - this.amount = response.Amount; + super(response); + this.url = this.getResponseProperty('Url'); + this.pdfUrl = this.getResponseProperty('PdfUrl'); + this.number = this.getResponseProperty('Number'); + this.paid = this.getResponseProperty('Paid'); + this.date = this.getResponseProperty('Date'); + this.amount = this.getResponseProperty('Amount'); } } -export class BillingTransactionResponse { +export class BillingTransactionResponse extends BaseResponse { createdDate: string; amount: number; refunded: boolean; @@ -62,13 +70,14 @@ export class BillingTransactionResponse { details: string; constructor(response: any) { - this.createdDate = response.CreatedDate; - this.amount = response.Amount; - this.refunded = response.Refunded; - this.partiallyRefunded = response.PartiallyRefunded; - this.refundedAmount = response.RefundedAmount; - this.type = response.Type; - this.paymentMethodType = response.PaymentMethodType; - this.details = response.Details; + super(response); + this.createdDate = this.getResponseProperty('CreatedDate'); + this.amount = this.getResponseProperty('Amount'); + this.refunded = this.getResponseProperty('Refunded'); + this.partiallyRefunded = this.getResponseProperty('PartiallyRefunded'); + this.refundedAmount = this.getResponseProperty('RefundedAmount'); + this.type = this.getResponseProperty('Type'); + this.paymentMethodType = this.getResponseProperty('PaymentMethodType'); + this.details = this.getResponseProperty('Details'); } } diff --git a/src/models/response/breachAccountResponse.ts b/src/models/response/breachAccountResponse.ts index 842d8fa860..bfa4f70c28 100644 --- a/src/models/response/breachAccountResponse.ts +++ b/src/models/response/breachAccountResponse.ts @@ -1,4 +1,6 @@ -export class BreachAccountResponse { +import { BaseResponse } from './baseResponse'; + +export class BreachAccountResponse extends BaseResponse { addedDate: string; breachDate: string; dataClasses: string[]; @@ -13,17 +15,18 @@ export class BreachAccountResponse { title: string; constructor(response: any) { - this.addedDate = response.AddedDate; - this.breachDate = response.BreachDate; - this.dataClasses = response.DataClasses; - this.description = response.Description; - this.domain = response.Domain; - this.isActive = response.IsActive; - this.isVerified = response.IsVerified; - this.logoPath = response.LogoPath; - this.modifiedDate = response.ModifiedDate; - this.name = response.Name; - this.pwnCount = response.PwnCount; - this.title = response.Title; + super(response); + this.addedDate = this.getResponseProperty('AddedDate'); + this.breachDate = this.getResponseProperty('BreachDate'); + this.dataClasses = this.getResponseProperty('DataClasses'); + this.description = this.getResponseProperty('Description'); + this.domain = this.getResponseProperty('Domain'); + this.isActive = this.getResponseProperty('IsActive'); + this.isVerified = this.getResponseProperty('IsVerified'); + this.logoPath = this.getResponseProperty('LogoPath'); + this.modifiedDate = this.getResponseProperty('ModifiedDate'); + this.name = this.getResponseProperty('Name'); + this.pwnCount = this.getResponseProperty('PwnCount'); + this.title = this.getResponseProperty('Title'); } } diff --git a/src/models/response/cipherResponse.ts b/src/models/response/cipherResponse.ts index 87ceec6338..54584123d4 100644 --- a/src/models/response/cipherResponse.ts +++ b/src/models/response/cipherResponse.ts @@ -1,4 +1,5 @@ import { AttachmentResponse } from './attachmentResponse'; +import { BaseResponse } from './baseResponse'; import { PasswordHistoryResponse } from './passwordHistoryResponse'; import { CardApi } from '../api/cardApi'; @@ -7,7 +8,7 @@ import { IdentityApi } from '../api/identityApi'; import { LoginApi } from '../api/loginApi'; import { SecureNoteApi } from '../api/secureNoteApi'; -export class CipherResponse { +export class CipherResponse extends BaseResponse { id: string; organizationId: string; folderId: string; @@ -28,59 +29,52 @@ export class CipherResponse { collectionIds: string[]; constructor(response: any) { - this.id = response.Id; - this.organizationId = response.OrganizationId; - this.folderId = response.FolderId || null; - this.type = response.Type; - this.name = response.Name; - this.notes = response.Notes; - this.favorite = response.Favorite || false; - this.edit = response.Edit || true; - this.organizationUseTotp = response.OrganizationUseTotp; - this.revisionDate = response.RevisionDate; + super(response); + this.id = this.getResponseProperty('Id'); + this.organizationId = this.getResponseProperty('OrganizationId'); + this.folderId = this.getResponseProperty('FolderId') || null; + this.type = this.getResponseProperty('Type'); + this.name = this.getResponseProperty('Name'); + this.notes = this.getResponseProperty('Notes'); + this.favorite = this.getResponseProperty('Favorite') || false; + this.edit = this.getResponseProperty('Edit') || true; + this.organizationUseTotp = this.getResponseProperty('OrganizationUseTotp'); + this.revisionDate = this.getResponseProperty('RevisionDate'); + this.collectionIds = this.getResponseProperty('CollectionIds'); - if (response.Login != null) { - this.login = new LoginApi(response.Login); + const login = this.getResponseProperty('Login'); + if (login != null) { + this.login = new LoginApi(login); } - if (response.Card != null) { - this.card = new CardApi(response.Card); + const card = this.getResponseProperty('Card'); + if (card != null) { + this.card = new CardApi(card); } - if (response.Identity != null) { - this.identity = new IdentityApi(response.Identity); + const identity = this.getResponseProperty('Identity'); + if (identity != null) { + this.identity = new IdentityApi(identity); } - if (response.SecureNote != null) { - this.secureNote = new SecureNoteApi(response.SecureNote); + const secureNote = this.getResponseProperty('SecureNote'); + if (secureNote != null) { + this.secureNote = new SecureNoteApi(secureNote); } - if (response.Fields != null) { - this.fields = []; - response.Fields.forEach((field: any) => { - this.fields.push(new FieldApi(field)); - }); + const fields = this.getResponseProperty('Fields'); + if (fields != null) { + this.fields = fields.map((f: any) => new FieldApi(f)); } - if (response.Attachments != null) { - this.attachments = []; - response.Attachments.forEach((attachment: any) => { - this.attachments.push(new AttachmentResponse(attachment)); - }); + const attachments = this.getResponseProperty('Attachments'); + if (attachments != null) { + this.attachments = attachments.map((a: any) => new AttachmentResponse(a)); } - if (response.PasswordHistory != null) { - this.passwordHistory = []; - response.PasswordHistory.forEach((ph: any) => { - this.passwordHistory.push(new PasswordHistoryResponse(ph)); - }); - } - - if (response.CollectionIds) { - this.collectionIds = []; - response.CollectionIds.forEach((id: string) => { - this.collectionIds.push(id); - }); + const passwordHistory = this.getResponseProperty('PasswordHistory'); + if (passwordHistory != null) { + this.passwordHistory = passwordHistory.map((h: any) => new PasswordHistoryResponse(h)); } } } diff --git a/src/models/response/collectionResponse.ts b/src/models/response/collectionResponse.ts index 14e839cf35..458945334f 100644 --- a/src/models/response/collectionResponse.ts +++ b/src/models/response/collectionResponse.ts @@ -1,14 +1,16 @@ +import { BaseResponse } from './baseResponse'; import { SelectionReadOnlyResponse } from './selectionReadOnlyResponse'; -export class CollectionResponse { +export class CollectionResponse extends BaseResponse { id: string; organizationId: string; name: string; constructor(response: any) { - this.id = response.Id; - this.organizationId = response.OrganizationId; - this.name = response.Name; + super(response); + this.id = this.getResponseProperty('Id'); + this.organizationId = this.getResponseProperty('OrganizationId'); + this.name = this.getResponseProperty('Name'); } } @@ -17,7 +19,7 @@ export class CollectionDetailsResponse extends CollectionResponse { constructor(response: any) { super(response); - this.readOnly = response.ReadOnly || false; + this.readOnly = this.getResponseProperty('ReadOnly') || false; } } @@ -26,8 +28,9 @@ export class CollectionGroupDetailsResponse extends CollectionResponse { constructor(response: any) { super(response); - if (response.Groups != null) { - this.groups = response.Groups.map((g: any) => new SelectionReadOnlyResponse(g)); + const groups = this.getResponseProperty('Groups'); + if (groups != null) { + this.groups = groups.map((g: any) => new SelectionReadOnlyResponse(g)); } } } diff --git a/src/models/response/deviceResponse.ts b/src/models/response/deviceResponse.ts index 23d0135b76..30f4e400f5 100644 --- a/src/models/response/deviceResponse.ts +++ b/src/models/response/deviceResponse.ts @@ -1,6 +1,8 @@ +import { BaseResponse } from './baseResponse'; + import { DeviceType } from '../../enums/deviceType'; -export class DeviceResponse { +export class DeviceResponse extends BaseResponse { id: string; name: number; identifier: string; @@ -8,10 +10,11 @@ export class DeviceResponse { 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; + super(response); + this.id = this.getResponseProperty('Id'); + this.name = this.getResponseProperty('Name'); + this.identifier = this.getResponseProperty('Identifier'); + this.type = this.getResponseProperty('Type'); + this.creationDate = this.getResponseProperty('CreationDate'); } } diff --git a/src/models/response/domainsResponse.ts b/src/models/response/domainsResponse.ts index 11ecfe1fbc..3e7727e93c 100644 --- a/src/models/response/domainsResponse.ts +++ b/src/models/response/domainsResponse.ts @@ -1,17 +1,18 @@ +import { BaseResponse } from './baseResponse'; import { GlobalDomainResponse } from './globalDomainResponse'; -export class DomainsResponse { +export class DomainsResponse extends BaseResponse { 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)); - }); + super(response); + this.equivalentDomains = this.getResponseProperty('EquivalentDomains'); + const globalEquivalentDomains = this.getResponseProperty('GlobalEquivalentDomains'); + if (globalEquivalentDomains != null) { + this.globalEquivalentDomains = globalEquivalentDomains.map((d: any) => new GlobalDomainResponse(d)); + } else { + this.globalEquivalentDomains = []; } } } diff --git a/src/models/response/errorResponse.ts b/src/models/response/errorResponse.ts index 87afde4837..d28f6a2e02 100644 --- a/src/models/response/errorResponse.ts +++ b/src/models/response/errorResponse.ts @@ -1,19 +1,23 @@ -export class ErrorResponse { +import { BaseResponse } from './baseResponse'; + +export class ErrorResponse extends BaseResponse { message: string; validationErrors: { [key: string]: string[]; }; statusCode: number; constructor(response: any, status: number, identityResponse?: boolean) { + super(response); let errorModel = null; - if (identityResponse && response && response.ErrorModel) { - errorModel = response.ErrorModel; + const responseErrorModel = this.getResponseProperty('ErrorModel'); + if (responseErrorModel && identityResponse && response) { + errorModel = responseErrorModel; } else if (response) { errorModel = response; } if (errorModel) { - this.message = errorModel.Message; - this.validationErrors = errorModel.ValidationErrors; + this.message = this.getResponseProperty('Message', errorModel); + this.validationErrors = this.getResponseProperty('ValidationErrors', errorModel); } else { if (status === 429) { this.message = 'Rate limit exceeded. Try again later.'; diff --git a/src/models/response/eventResponse.ts b/src/models/response/eventResponse.ts index 658d05baa8..e3cfa93323 100644 --- a/src/models/response/eventResponse.ts +++ b/src/models/response/eventResponse.ts @@ -1,7 +1,9 @@ +import { BaseResponse } from './baseResponse'; + import { DeviceType } from '../../enums/deviceType'; import { EventType } from '../../enums/eventType'; -export class EventResponse { +export class EventResponse extends BaseResponse { type: EventType; userId: string; organizationId: string; @@ -15,16 +17,17 @@ export class EventResponse { ipAddress: string; constructor(response: any) { - this.type = response.Type; - this.userId = response.UserId; - this.organizationId = response.OrganizationId; - this.cipherId = response.CipherId; - this.collectionId = response.CollectionId; - this.groupId = response.GroupId; - this.organizationUserId = response.OrganizationUserId; - this.actingUserId = response.ActingUserId; - this.date = response.Date; - this.deviceType = response.DeviceType; - this.ipAddress = response.IpAddress; + super(response); + this.type = this.getResponseProperty('Type'); + this.userId = this.getResponseProperty('UserId'); + this.organizationId = this.getResponseProperty('OrganizationId'); + this.cipherId = this.getResponseProperty('CipherId'); + this.collectionId = this.getResponseProperty('CollectionId'); + this.groupId = this.getResponseProperty('GroupId'); + this.organizationUserId = this.getResponseProperty('OrganizationUserId'); + this.actingUserId = this.getResponseProperty('ActingUserId'); + this.date = this.getResponseProperty('Date'); + this.deviceType = this.getResponseProperty('DeviceType'); + this.ipAddress = this.getResponseProperty('IpAddress'); } } diff --git a/src/models/response/folderResponse.ts b/src/models/response/folderResponse.ts index 1e3b4570cc..00b3d3d8dd 100644 --- a/src/models/response/folderResponse.ts +++ b/src/models/response/folderResponse.ts @@ -1,11 +1,14 @@ -export class FolderResponse { +import { BaseResponse } from './baseResponse'; + +export class FolderResponse extends BaseResponse { id: string; name: string; revisionDate: string; constructor(response: any) { - this.id = response.Id; - this.name = response.Name; - this.revisionDate = response.RevisionDate; + super(response); + this.id = this.getResponseProperty('Id'); + this.name = this.getResponseProperty('Name'); + this.revisionDate = this.getResponseProperty('RevisionDate'); } } diff --git a/src/models/response/globalDomainResponse.ts b/src/models/response/globalDomainResponse.ts index 75f5e4ce61..5f37c7e625 100644 --- a/src/models/response/globalDomainResponse.ts +++ b/src/models/response/globalDomainResponse.ts @@ -1,11 +1,14 @@ -export class GlobalDomainResponse { +import { BaseResponse } from './baseResponse'; + +export class GlobalDomainResponse extends BaseResponse { type: number; domains: string[]; excluded: number[]; constructor(response: any) { - this.type = response.Type; - this.domains = response.Domains; - this.excluded = response.Excluded; + super(response); + this.type = this.getResponseProperty('Type'); + this.domains = this.getResponseProperty('Domains'); + this.excluded = this.getResponseProperty('Excluded'); } } diff --git a/src/models/response/groupResponse.ts b/src/models/response/groupResponse.ts index 960e077a2a..2c3cf119b9 100644 --- a/src/models/response/groupResponse.ts +++ b/src/models/response/groupResponse.ts @@ -1,6 +1,7 @@ +import { BaseResponse } from './baseResponse'; import { SelectionReadOnlyResponse } from './selectionReadOnlyResponse'; -export class GroupResponse { +export class GroupResponse extends BaseResponse { id: string; organizationId: string; name: string; @@ -8,11 +9,12 @@ export class GroupResponse { externalId: string; constructor(response: any) { - this.id = response.Id; - this.organizationId = response.OrganizationId; - this.name = response.Name; - this.accessAll = response.AccessAll; - this.externalId = response.ExternalId; + super(response); + this.id = this.getResponseProperty('Id'); + this.organizationId = this.getResponseProperty('OrganizationId'); + this.name = this.getResponseProperty('Name'); + this.accessAll = this.getResponseProperty('AccessAll'); + this.externalId = this.getResponseProperty('ExternalId'); } } @@ -21,8 +23,9 @@ export class GroupDetailsResponse extends GroupResponse { constructor(response: any) { super(response); - if (response.Collections != null) { - this.collections = response.Collections.map((c: any) => new SelectionReadOnlyResponse(c)); + const collections = this.getResponseProperty('Collections'); + if (collections != null) { + this.collections = collections.map((c: any) => new SelectionReadOnlyResponse(c)); } } } diff --git a/src/models/response/identityTokenResponse.ts b/src/models/response/identityTokenResponse.ts index 2252c76c91..20adaff718 100644 --- a/src/models/response/identityTokenResponse.ts +++ b/src/models/response/identityTokenResponse.ts @@ -1,4 +1,6 @@ -export class IdentityTokenResponse { +import { BaseResponse } from './baseResponse'; + +export class IdentityTokenResponse extends BaseResponse { accessToken: string; expiresIn: number; refreshToken: string; @@ -9,13 +11,14 @@ export class IdentityTokenResponse { twoFactorToken: string; constructor(response: any) { + super(response); 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; + this.privateKey = this.getResponseProperty('PrivateKey'); + this.key = this.getResponseProperty('Key'); + this.twoFactorToken = this.getResponseProperty('TwoFactorToken'); } } diff --git a/src/models/response/identityTwoFactorResponse.ts b/src/models/response/identityTwoFactorResponse.ts index 2adeeaa053..ea92a5885c 100644 --- a/src/models/response/identityTwoFactorResponse.ts +++ b/src/models/response/identityTwoFactorResponse.ts @@ -1,15 +1,19 @@ +import { BaseResponse } from './baseResponse'; + import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; -export class IdentityTwoFactorResponse { +export class IdentityTwoFactorResponse extends BaseResponse { twoFactorProviders: TwoFactorProviderType[]; twoFactorProviders2 = new Map(); constructor(response: any) { - this.twoFactorProviders = response.TwoFactorProviders; - if (response.TwoFactorProviders2 != null) { - for (const prop in response.TwoFactorProviders2) { - if (response.TwoFactorProviders2.hasOwnProperty(prop)) { - this.twoFactorProviders2.set(parseInt(prop, null), response.TwoFactorProviders2[prop]); + super(response); + this.twoFactorProviders = this.getResponseProperty('TwoFactorProviders'); + const twoFactorProviders2 = this.getResponseProperty('TwoFactorProviders2'); + if (twoFactorProviders2 != null) { + for (const prop in twoFactorProviders2) { + if (twoFactorProviders2.hasOwnProperty(prop)) { + this.twoFactorProviders2.set(parseInt(prop, null), twoFactorProviders2[prop]); } } } diff --git a/src/models/response/keysResponse.ts b/src/models/response/keysResponse.ts index a357f97c44..405edcdc15 100644 --- a/src/models/response/keysResponse.ts +++ b/src/models/response/keysResponse.ts @@ -1,9 +1,12 @@ -export class KeysResponse { +import { BaseResponse } from './baseResponse'; + +export class KeysResponse extends BaseResponse { privateKey: string; publicKey: string; constructor(response: any) { - this.privateKey = response.PrivateKey; - this.publicKey = response.PublicKey; + super(response); + this.privateKey = this.getResponseProperty('PrivateKey'); + this.publicKey = this.getResponseProperty('PublicKey'); } } diff --git a/src/models/response/listResponse.ts b/src/models/response/listResponse.ts index b6cfdf5feb..3b15a3f571 100644 --- a/src/models/response/listResponse.ts +++ b/src/models/response/listResponse.ts @@ -1,9 +1,13 @@ -export class ListResponse { +import { BaseResponse } from './baseResponse'; + +export class ListResponse extends BaseResponse { data: T[]; continuationToken: string; constructor(response: any, t: new (dataResponse: any) => T) { - this.data = response.Data == null ? [] : response.Data.map((dr: any) => new t(dr)); - this.continuationToken = response.ContinuationToken; + super(response); + const data = this.getResponseProperty('Data'); + this.data = data == null ? [] : data.map((dr: any) => new t(dr)); + this.continuationToken = this.getResponseProperty('ContinuationToken'); } } diff --git a/src/models/response/notificationResponse.ts b/src/models/response/notificationResponse.ts index b768a0c6c0..9187be71fd 100644 --- a/src/models/response/notificationResponse.ts +++ b/src/models/response/notificationResponse.ts @@ -1,15 +1,18 @@ +import { BaseResponse } from './baseResponse'; + import { NotificationType } from '../../enums/notificationType'; -export class NotificationResponse { +export class NotificationResponse extends BaseResponse { contextId: string; type: NotificationType; payload: any; constructor(response: any) { - this.contextId = response.contextId || response.ContextId; - this.type = response.type != null ? response.type : response.Type; + super(response); + this.contextId = this.getResponseProperty('ContextId'); + this.type = this.getResponseProperty('Type'); - const payload = response.payload || response.Payload; + const payload = this.getResponseProperty('Payload'); switch (this.type) { case NotificationType.SyncCipherCreate: case NotificationType.SyncCipherDelete: @@ -35,7 +38,7 @@ export class NotificationResponse { } } -export class SyncCipherNotification { +export class SyncCipherNotification extends BaseResponse { id: string; userId: string; organizationId: string; @@ -43,32 +46,35 @@ export class SyncCipherNotification { revisionDate: Date; constructor(response: any) { - this.id = response.id || response.Id; - this.userId = response.userId || response.UserId; - this.organizationId = response.organizationId || response.OrganizationId; - this.collectionIds = response.collectionIds || response.CollectionIds; - this.revisionDate = new Date(response.revisionDate || response.RevisionDate); + super(response); + this.id = this.getResponseProperty('Id'); + this.userId = this.getResponseProperty('UserId'); + this.organizationId = this.getResponseProperty('OrganizationId'); + this.collectionIds = this.getResponseProperty('CollectionIds'); + this.revisionDate = new Date(this.getResponseProperty('RevisionDate')); } } -export class SyncFolderNotification { +export class SyncFolderNotification extends BaseResponse { id: string; userId: string; revisionDate: Date; constructor(response: any) { - this.id = response.id || response.Id; - this.userId = response.userId || response.UserId; - this.revisionDate = new Date(response.revisionDate || response.RevisionDate); + super(response); + this.id = this.getResponseProperty('Id'); + this.userId = this.getResponseProperty('UserId'); + this.revisionDate = new Date(this.getResponseProperty('RevisionDate')); } } -export class UserNotification { +export class UserNotification extends BaseResponse { userId: string; date: Date; constructor(response: any) { - this.userId = response.userId || response.UserId; - this.date = new Date(response.date || response.Date); + super(response); + this.userId = this.getResponseProperty('UserId'); + this.date = new Date(this.getResponseProperty('Date')); } } diff --git a/src/models/response/organizationResponse.ts b/src/models/response/organizationResponse.ts index ea62f2c98a..51009533c4 100644 --- a/src/models/response/organizationResponse.ts +++ b/src/models/response/organizationResponse.ts @@ -1,6 +1,8 @@ +import { BaseResponse } from './baseResponse'; + import { PlanType } from '../../enums/planType'; -export class OrganizationResponse { +export class OrganizationResponse extends BaseResponse { id: string; name: string; businessName: string; @@ -22,24 +24,25 @@ export class OrganizationResponse { use2fa: boolean; constructor(response: any) { - this.id = response.Id; - this.name = response.Name; - this.businessName = response.BusinessName; - this.businessAddress1 = response.BusinessAddress1; - this.businessAddress2 = response.BusinessAddress2; - this.businessAddress3 = response.BusinessAddress3; - this.businessCountry = response.BusinessCountry; - this.businessTaxNumber = response.BusinessTaxNumber; - this.billingEmail = response.BillingEmail; - this.plan = response.Plan; - this.planType = response.PlanType; - this.seats = response.Seats; - this.maxCollections = response.MaxCollections; - this.maxStorageGb = response.MaxStorageGb; - this.useGroups = response.UseGroups; - this.useDirectory = response.UseDirectory; - this.useEvents = response.UseEvents; - this.useTotp = response.UseTotp; - this.use2fa = response.Use2fa; + super(response); + this.id = this.getResponseProperty('Id'); + this.name = this.getResponseProperty('Name'); + this.businessName = this.getResponseProperty('BusinessName'); + this.businessAddress1 = this.getResponseProperty('BusinessAddress1'); + this.businessAddress2 = this.getResponseProperty('BusinessAddress2'); + this.businessAddress3 = this.getResponseProperty('BusinessAddress3'); + this.businessCountry = this.getResponseProperty('BusinessCountry'); + this.businessTaxNumber = this.getResponseProperty('BusinessTaxNumber'); + this.billingEmail = this.getResponseProperty('BillingEmail'); + this.plan = this.getResponseProperty('Plan'); + this.planType = this.getResponseProperty('PlanType'); + this.seats = this.getResponseProperty('Seats'); + this.maxCollections = this.getResponseProperty('MaxCollections'); + this.maxStorageGb = this.getResponseProperty('MaxStorageGb'); + this.useGroups = this.getResponseProperty('UseGroups'); + this.useDirectory = this.getResponseProperty('UseDirectory'); + this.useEvents = this.getResponseProperty('UseEvents'); + this.useTotp = this.getResponseProperty('UseTotp'); + this.use2fa = this.getResponseProperty('Use2fa'); } } diff --git a/src/models/response/organizationSubscriptionResponse.ts b/src/models/response/organizationSubscriptionResponse.ts index 77cacaa68b..9fbd16d52d 100644 --- a/src/models/response/organizationSubscriptionResponse.ts +++ b/src/models/response/organizationSubscriptionResponse.ts @@ -13,12 +13,13 @@ export class OrganizationSubscriptionResponse extends OrganizationResponse { constructor(response: any) { super(response); - this.storageName = response.StorageName; - this.storageGb = response.StorageGb; - this.subscription = response.Subscription == null ? - null : new BillingSubscriptionResponse(response.Subscription); - this.upcomingInvoice = response.UpcomingInvoice == null ? - null : new BillingSubscriptionUpcomingInvoiceResponse(response.UpcomingInvoice); - this.expiration = response.Expiration; + this.storageName = this.getResponseProperty('StorageName'); + this.storageGb = this.getResponseProperty('StorageGb'); + const subscription = this.getResponseProperty('Subscription'); + this.subscription = subscription == null ? null : new BillingSubscriptionResponse(subscription); + const upcomingInvoice = this.getResponseProperty('UpcomingInvoice'); + this.upcomingInvoice = upcomingInvoice == null ? null : + new BillingSubscriptionUpcomingInvoiceResponse(upcomingInvoice); + this.expiration = this.getResponseProperty('Expiration'); } } diff --git a/src/models/response/organizationUserResponse.ts b/src/models/response/organizationUserResponse.ts index 144bcf9539..333baa838a 100644 --- a/src/models/response/organizationUserResponse.ts +++ b/src/models/response/organizationUserResponse.ts @@ -1,8 +1,10 @@ import { OrganizationUserStatusType } from '../../enums/organizationUserStatusType'; import { OrganizationUserType } from '../../enums/organizationUserType'; + +import { BaseResponse } from './baseResponse'; import { SelectionReadOnlyResponse } from './selectionReadOnlyResponse'; -export class OrganizationUserResponse { +export class OrganizationUserResponse extends BaseResponse { id: string; userId: string; type: OrganizationUserType; @@ -10,11 +12,12 @@ export class OrganizationUserResponse { accessAll: boolean; constructor(response: any) { - this.id = response.Id; - this.userId = response.UserId; - this.type = response.Type; - this.status = response.Status; - this.accessAll = response.AccessAll; + super(response); + this.id = this.getResponseProperty('Id'); + this.userId = this.getResponseProperty('UserId'); + this.type = this.getResponseProperty('Type'); + this.status = this.getResponseProperty('Status'); + this.accessAll = this.getResponseProperty('AccessAll'); } } @@ -25,9 +28,9 @@ export class OrganizationUserUserDetailsResponse extends OrganizationUserRespons constructor(response: any) { super(response); - this.name = response.Name; - this.email = response.Email; - this.twoFactorEnabled = response.TwoFactorEnabled; + this.name = this.getResponseProperty('Name'); + this.email = this.getResponseProperty('Email'); + this.twoFactorEnabled = this.getResponseProperty('TwoFactorEnabled'); } } @@ -36,8 +39,9 @@ export class OrganizationUserDetailsResponse extends OrganizationUserResponse { constructor(response: any) { super(response); - if (response.Collections != null) { - this.collections = response.Collections.map((c: any) => new SelectionReadOnlyResponse(c)); + const collections = this.getResponseProperty('Collections'); + if (collections != null) { + this.collections = collections.map((c: any) => new SelectionReadOnlyResponse(c)); } } } diff --git a/src/models/response/passwordHistoryResponse.ts b/src/models/response/passwordHistoryResponse.ts index 03c210de1c..805a566877 100644 --- a/src/models/response/passwordHistoryResponse.ts +++ b/src/models/response/passwordHistoryResponse.ts @@ -1,9 +1,12 @@ -export class PasswordHistoryResponse { +import { BaseResponse } from './baseResponse'; + +export class PasswordHistoryResponse extends BaseResponse { password: string; lastUsedDate: string; constructor(response: any) { - this.password = response.Password; - this.lastUsedDate = response.LastUsedDate; + super(response); + this.password = this.getResponseProperty('Password'); + this.lastUsedDate = this.getResponseProperty('LastUsedDate'); } } diff --git a/src/models/response/preloginResponse.ts b/src/models/response/preloginResponse.ts index 8a893b5f52..fcbd4cb20a 100644 --- a/src/models/response/preloginResponse.ts +++ b/src/models/response/preloginResponse.ts @@ -1,11 +1,14 @@ +import { BaseResponse } from './baseResponse'; + import { KdfType } from '../../enums/kdfType'; -export class PreloginResponse { +export class PreloginResponse extends BaseResponse { kdf: KdfType; kdfIterations: number; constructor(response: any) { - this.kdf = response.Kdf; - this.kdfIterations = response.KdfIterations; + super(response); + this.kdf = this.getResponseProperty('Kdf'); + this.kdfIterations = this.getResponseProperty('KdfIterations'); } } diff --git a/src/models/response/profileOrganizationResponse.ts b/src/models/response/profileOrganizationResponse.ts index 72bbc48387..3a7beac530 100644 --- a/src/models/response/profileOrganizationResponse.ts +++ b/src/models/response/profileOrganizationResponse.ts @@ -1,7 +1,9 @@ +import { BaseResponse } from './baseResponse'; + import { OrganizationUserStatusType } from '../../enums/organizationUserStatusType'; import { OrganizationUserType } from '../../enums/organizationUserType'; -export class ProfileOrganizationResponse { +export class ProfileOrganizationResponse extends BaseResponse { id: string; name: string; useGroups: boolean; @@ -20,21 +22,22 @@ export class ProfileOrganizationResponse { enabled: boolean; constructor(response: any) { - this.id = response.Id; - this.name = response.Name; - this.useGroups = response.UseGroups; - this.useDirectory = response.UseDirectory; - this.useEvents = response.UseEvents; - this.useTotp = response.UseTotp; - this.use2fa = response.Use2fa; - this.selfHost = response.SelfHost; - this.usersGetPremium = response.UsersGetPremium; - this.seats = response.Seats; - this.maxCollections = response.MaxCollections; - this.maxStorageGb = response.MaxStorageGb; - this.key = response.Key; - this.status = response.Status; - this.type = response.Type; - this.enabled = response.Enabled; + super(response); + this.id = this.getResponseProperty('Id'); + this.name = this.getResponseProperty('Name'); + this.useGroups = this.getResponseProperty('UseGroups'); + this.useDirectory = this.getResponseProperty('UseDirectory'); + this.useEvents = this.getResponseProperty('UseEvents'); + this.useTotp = this.getResponseProperty('UseTotp'); + this.use2fa = this.getResponseProperty('Use2fa'); + this.selfHost = this.getResponseProperty('SelfHost'); + this.usersGetPremium = this.getResponseProperty('UsersGetPremium'); + this.seats = this.getResponseProperty('Seats'); + this.maxCollections = this.getResponseProperty('MaxCollections'); + this.maxStorageGb = this.getResponseProperty('MaxStorageGb'); + this.key = this.getResponseProperty('Key'); + this.status = this.getResponseProperty('Status'); + this.type = this.getResponseProperty('Type'); + this.enabled = this.getResponseProperty('Enabled'); } } diff --git a/src/models/response/profileResponse.ts b/src/models/response/profileResponse.ts index c9d2d3d221..95897d9489 100644 --- a/src/models/response/profileResponse.ts +++ b/src/models/response/profileResponse.ts @@ -1,6 +1,7 @@ +import { BaseResponse } from './baseResponse'; import { ProfileOrganizationResponse } from './profileOrganizationResponse'; -export class ProfileResponse { +export class ProfileResponse extends BaseResponse { id: string; name: string; email: string; @@ -15,22 +16,22 @@ export class ProfileResponse { 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; + super(response); + this.id = this.getResponseProperty('Id'); + this.name = this.getResponseProperty('Name'); + this.email = this.getResponseProperty('Email'); + this.emailVerified = this.getResponseProperty('EmailVerified'); + this.masterPasswordHint = this.getResponseProperty('MasterPasswordHint'); + this.premium = this.getResponseProperty('Premium'); + this.culture = this.getResponseProperty('Culture'); + this.twoFactorEnabled = this.getResponseProperty('TwoFactorEnabled'); + this.key = this.getResponseProperty('Key'); + this.privateKey = this.getResponseProperty('PrivateKey'); + this.securityStamp = this.getResponseProperty('SecurityStamp'); - if (response.Organizations) { - response.Organizations.forEach((org: any) => { - this.organizations.push(new ProfileOrganizationResponse(org)); - }); + const organizations = this.getResponseProperty('Organizations'); + if (organizations != null) { + this.organizations = organizations.map((o: any) => new ProfileOrganizationResponse(o)); } } } diff --git a/src/models/response/selectionReadOnlyResponse.ts b/src/models/response/selectionReadOnlyResponse.ts index c5f2c2dffd..5bdfc6e417 100644 --- a/src/models/response/selectionReadOnlyResponse.ts +++ b/src/models/response/selectionReadOnlyResponse.ts @@ -1,9 +1,12 @@ -export class SelectionReadOnlyResponse { +import { BaseResponse } from './baseResponse'; + +export class SelectionReadOnlyResponse extends BaseResponse { id: string; readOnly: boolean; constructor(response: any) { - this.id = response.Id; - this.readOnly = response.ReadOnly; + super(response); + this.id = this.getResponseProperty('Id'); + this.readOnly = this.getResponseProperty('ReadOnly'); } } diff --git a/src/models/response/subscriptionResponse.ts b/src/models/response/subscriptionResponse.ts index 86d3580307..13f1b30aaa 100644 --- a/src/models/response/subscriptionResponse.ts +++ b/src/models/response/subscriptionResponse.ts @@ -1,4 +1,6 @@ -export class SubscriptionResponse { +import { BaseResponse } from './baseResponse'; + +export class SubscriptionResponse extends BaseResponse { storageName: string; storageGb: number; maxStorageGb: number; @@ -8,19 +10,21 @@ export class SubscriptionResponse { expiration: string; constructor(response: any) { - this.storageName = response.StorageName; - this.storageGb = response.StorageGb; - this.maxStorageGb = response.MaxStorageGb; - this.subscription = response.Subscription == null ? - null : new BillingSubscriptionResponse(response.Subscription); - this.upcomingInvoice = response.UpcomingInvoice == null ? - null : new BillingSubscriptionUpcomingInvoiceResponse(response.UpcomingInvoice); - this.license = response.License; - this.expiration = response.Expiration; + super(response); + this.storageName = this.getResponseProperty('StorageName'); + this.storageGb = this.getResponseProperty('StorageGb'); + this.maxStorageGb = this.getResponseProperty('MaxStorageGb'); + this.license = this.getResponseProperty('License'); + this.expiration = this.getResponseProperty('Expiration'); + const subscription = this.getResponseProperty('Subscription'); + const upcomingInvoice = this.getResponseProperty('UpcomingInvoice'); + this.subscription = subscription == null ? null : new BillingSubscriptionResponse(subscription); + this.upcomingInvoice = upcomingInvoice == null ? null : + new BillingSubscriptionUpcomingInvoiceResponse(upcomingInvoice); } } -export class BillingSubscriptionResponse { +export class BillingSubscriptionResponse extends BaseResponse { trialStartDate: string; trialEndDate: string; periodStartDate: string; @@ -32,40 +36,44 @@ export class BillingSubscriptionResponse { items: BillingSubscriptionItemResponse[] = []; constructor(response: any) { - this.trialEndDate = response.TrialStartDate; - this.trialEndDate = response.TrialEndDate; - this.periodStartDate = response.PeriodStartDate; - this.periodEndDate = response.PeriodEndDate; - this.cancelledDate = response.CancelledDate; - this.cancelAtEndDate = response.CancelAtEndDate; - this.status = response.Status; - this.cancelled = response.Cancelled; - if (response.Items != null) { - this.items = response.Items.map((i: any) => new BillingSubscriptionItemResponse(i)); + super(response); + this.trialEndDate = this.getResponseProperty('TrialStartDate'); + this.trialEndDate = this.getResponseProperty('TrialEndDate'); + this.periodStartDate = this.getResponseProperty('PeriodStartDate'); + this.periodEndDate = this.getResponseProperty('PeriodEndDate'); + this.cancelledDate = this.getResponseProperty('CancelledDate'); + this.cancelAtEndDate = this.getResponseProperty('CancelAtEndDate'); + this.status = this.getResponseProperty('Status'); + this.cancelled = this.getResponseProperty('Cancelled'); + const items = this.getResponseProperty('Items'); + if (items != null) { + this.items = items.map((i: any) => new BillingSubscriptionItemResponse(i)); } } } -export class BillingSubscriptionItemResponse { +export class BillingSubscriptionItemResponse extends BaseResponse { name: string; amount: number; quantity: number; interval: string; constructor(response: any) { - this.name = response.Name; - this.amount = response.Amount; - this.quantity = response.Quantity; - this.interval = response.Interval; + super(response); + this.name = this.getResponseProperty('Name'); + this.amount = this.getResponseProperty('Amount'); + this.quantity = this.getResponseProperty('Quantity'); + this.interval = this.getResponseProperty('Interval'); } } -export class BillingSubscriptionUpcomingInvoiceResponse { +export class BillingSubscriptionUpcomingInvoiceResponse extends BaseResponse { date: string; amount: number; constructor(response: any) { - this.date = response.Date; - this.amount = response.Amount; + super(response); + this.date = this.getResponseProperty('Date'); + this.amount = this.getResponseProperty('Amount'); } } diff --git a/src/models/response/syncResponse.ts b/src/models/response/syncResponse.ts index 3d0b741099..fb84dad89a 100644 --- a/src/models/response/syncResponse.ts +++ b/src/models/response/syncResponse.ts @@ -1,10 +1,11 @@ +import { BaseResponse } from './baseResponse'; import { CipherResponse } from './cipherResponse'; import { CollectionDetailsResponse } from './collectionResponse'; import { DomainsResponse } from './domainsResponse'; import { FolderResponse } from './folderResponse'; import { ProfileResponse } from './profileResponse'; -export class SyncResponse { +export class SyncResponse extends BaseResponse { profile?: ProfileResponse; folders: FolderResponse[] = []; collections: CollectionDetailsResponse[] = []; @@ -12,30 +13,31 @@ export class SyncResponse { domains?: DomainsResponse; constructor(response: any) { - if (response.Profile) { - this.profile = new ProfileResponse(response.Profile); + super(response); + + const profile = this.getResponseProperty('Profile'); + if (profile != null) { + this.profile = new ProfileResponse(profile); } - if (response.Folders) { - response.Folders.forEach((folder: any) => { - this.folders.push(new FolderResponse(folder)); - }); + const folders = this.getResponseProperty('Folders'); + if (folders != null) { + this.folders = folders.map((f: any) => new FolderResponse(f)); } - if (response.Collections) { - response.Collections.forEach((collection: any) => { - this.collections.push(new CollectionDetailsResponse(collection)); - }); + const collections = this.getResponseProperty('Collections'); + if (collections != null) { + this.collections = collections.map((c: any) => new CollectionDetailsResponse(c)); } - if (response.Ciphers) { - response.Ciphers.forEach((cipher: any) => { - this.ciphers.push(new CipherResponse(cipher)); - }); + const ciphers = this.getResponseProperty('Ciphers'); + if (ciphers != null) { + this.ciphers = ciphers.map((c: any) => new CipherResponse(c)); } - if (response.Domains) { - this.domains = new DomainsResponse(response.Domains); + const domains = this.getResponseProperty('Domains'); + if (domains != null) { + this.domains = new DomainsResponse(domains); } } } diff --git a/src/models/response/twoFactorAuthenticatorResponse.ts b/src/models/response/twoFactorAuthenticatorResponse.ts index f84509bd93..b08ecb1998 100644 --- a/src/models/response/twoFactorAuthenticatorResponse.ts +++ b/src/models/response/twoFactorAuthenticatorResponse.ts @@ -1,9 +1,12 @@ -export class TwoFactorAuthenticatorResponse { +import { BaseResponse } from './baseResponse'; + +export class TwoFactorAuthenticatorResponse extends BaseResponse { enabled: boolean; key: string; constructor(response: any) { - this.enabled = response.Enabled; - this.key = response.Key; + super(response); + this.enabled = this.getResponseProperty('Enabled'); + this.key = this.getResponseProperty('Key'); } } diff --git a/src/models/response/twoFactorDuoResponse.ts b/src/models/response/twoFactorDuoResponse.ts index a72b32b364..087fce48ae 100644 --- a/src/models/response/twoFactorDuoResponse.ts +++ b/src/models/response/twoFactorDuoResponse.ts @@ -1,13 +1,16 @@ -export class TwoFactorDuoResponse { +import { BaseResponse } from './baseResponse'; + +export class TwoFactorDuoResponse extends BaseResponse { enabled: boolean; host: string; secretKey: string; integrationKey: string; constructor(response: any) { - this.enabled = response.Enabled; - this.host = response.Host; - this.secretKey = response.SecretKey; - this.integrationKey = response.IntegrationKey; + super(response); + this.enabled = this.getResponseProperty('Enabled'); + this.host = this.getResponseProperty('Host'); + this.secretKey = this.getResponseProperty('SecretKey'); + this.integrationKey = this.getResponseProperty('IntegrationKey'); } } diff --git a/src/models/response/twoFactorEmailResponse.ts b/src/models/response/twoFactorEmailResponse.ts index eb8840a6ec..54f4a551d4 100644 --- a/src/models/response/twoFactorEmailResponse.ts +++ b/src/models/response/twoFactorEmailResponse.ts @@ -1,9 +1,12 @@ -export class TwoFactorEmailResponse { +import { BaseResponse } from './baseResponse'; + +export class TwoFactorEmailResponse extends BaseResponse { enabled: boolean; email: string; constructor(response: any) { - this.enabled = response.Enabled; - this.email = response.Email; + super(response); + this.enabled = this.getResponseProperty('Enabled'); + this.email = this.getResponseProperty('Email'); } } diff --git a/src/models/response/twoFactorProviderResponse.ts b/src/models/response/twoFactorProviderResponse.ts index ff834f9a25..570b1d6eb9 100644 --- a/src/models/response/twoFactorProviderResponse.ts +++ b/src/models/response/twoFactorProviderResponse.ts @@ -1,11 +1,14 @@ +import { BaseResponse } from './baseResponse'; + import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; -export class TwoFactorProviderResponse { +export class TwoFactorProviderResponse extends BaseResponse { enabled: boolean; type: TwoFactorProviderType; constructor(response: any) { - this.enabled = response.Enabled; - this.type = response.Type; + super(response); + this.enabled = this.getResponseProperty('Enabled'); + this.type = this.getResponseProperty('Type'); } } diff --git a/src/models/response/twoFactorRescoverResponse.ts b/src/models/response/twoFactorRescoverResponse.ts index 0f73778083..62bfdfe69b 100644 --- a/src/models/response/twoFactorRescoverResponse.ts +++ b/src/models/response/twoFactorRescoverResponse.ts @@ -1,7 +1,10 @@ -export class TwoFactorRecoverResponse { +import { BaseResponse } from './baseResponse'; + +export class TwoFactorRecoverResponse extends BaseResponse { code: string; constructor(response: any) { - this.code = response.Code; + super(response); + this.code = this.getResponseProperty('Code'); } } diff --git a/src/models/response/twoFactorU2fResponse.ts b/src/models/response/twoFactorU2fResponse.ts index 62ae93d68c..059cea312b 100644 --- a/src/models/response/twoFactorU2fResponse.ts +++ b/src/models/response/twoFactorU2fResponse.ts @@ -1,35 +1,41 @@ -export class TwoFactorU2fResponse { +import { BaseResponse } from './baseResponse'; + +export class TwoFactorU2fResponse extends BaseResponse { enabled: boolean; keys: KeyResponse[]; constructor(response: any) { - this.enabled = response.Enabled; - this.keys = response.Keys == null ? null : response.Keys.map((k: any) => new KeyResponse(k)); + super(response); + this.enabled = this.getResponseProperty('Enabled'); + const keys = this.getResponseProperty('Keys'); + this.keys = keys == null ? null : keys.map((k: any) => new KeyResponse(k)); } } -export class KeyResponse { +export class KeyResponse extends BaseResponse { name: string; id: number; compromised: boolean; constructor(response: any) { - this.name = response.Name; - this.id = response.Id; - this.compromised = response.Compromised; + super(response); + this.name = this.getResponseProperty('Name'); + this.id = this.getResponseProperty('Id'); + this.compromised = this.getResponseProperty('Compromised'); } } -export class ChallengeResponse { +export class ChallengeResponse extends BaseResponse { userId: string; appId: string; challenge: string; version: string; constructor(response: any) { - this.userId = response.UserId; - this.appId = response.AppId; - this.challenge = response.Challenge; - this.version = response.Version; + super(response); + this.userId = this.getResponseProperty('UserId'); + this.appId = this.getResponseProperty('AppId'); + this.challenge = this.getResponseProperty('Challenge'); + this.version = this.getResponseProperty('Version'); } } diff --git a/src/models/response/twoFactorYubiKeyResponse.ts b/src/models/response/twoFactorYubiKeyResponse.ts index 454d4b9919..c717210141 100644 --- a/src/models/response/twoFactorYubiKeyResponse.ts +++ b/src/models/response/twoFactorYubiKeyResponse.ts @@ -1,4 +1,6 @@ -export class TwoFactorYubiKeyResponse { +import { BaseResponse } from './baseResponse'; + +export class TwoFactorYubiKeyResponse extends BaseResponse { enabled: boolean; key1: string; key2: string; @@ -8,12 +10,13 @@ export class TwoFactorYubiKeyResponse { nfc: boolean; constructor(response: any) { - this.enabled = response.Enabled; - this.key1 = response.Key1; - this.key2 = response.Key2; - this.key3 = response.Key3; - this.key4 = response.Key4; - this.key5 = response.Key5; - this.nfc = response.Nfc; + super(response); + this.enabled = this.getResponseProperty('Enable'); + this.key1 = this.getResponseProperty('Key1'); + this.key2 = this.getResponseProperty('Key2'); + this.key3 = this.getResponseProperty('Key3'); + this.key4 = this.getResponseProperty('Key4'); + this.key5 = this.getResponseProperty('Key5'); + this.nfc = this.getResponseProperty('Nfc'); } } diff --git a/src/models/response/userKeyResponse.ts b/src/models/response/userKeyResponse.ts index 45fbe22b52..66af36393b 100644 --- a/src/models/response/userKeyResponse.ts +++ b/src/models/response/userKeyResponse.ts @@ -1,9 +1,12 @@ -export class UserKeyResponse { +import { BaseResponse } from './baseResponse'; + +export class UserKeyResponse extends BaseResponse { userId: string; publicKey: string; constructor(response: any) { - this.userId = response.UserId; - this.publicKey = response.PublicKey; + super(response); + this.userId = this.getResponseProperty('UserId'); + this.publicKey = this.getResponseProperty('PublicKey'); } } From 5535eb1002e17f25a6da67263f02f153c93564ef Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 2 Mar 2019 00:04:33 -0500 Subject: [PATCH 0740/1626] useApi added to models --- src/models/response/organizationResponse.ts | 2 ++ src/models/response/profileOrganizationResponse.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/models/response/organizationResponse.ts b/src/models/response/organizationResponse.ts index 51009533c4..436e09e3fa 100644 --- a/src/models/response/organizationResponse.ts +++ b/src/models/response/organizationResponse.ts @@ -22,6 +22,7 @@ export class OrganizationResponse extends BaseResponse { useEvents: boolean; useTotp: boolean; use2fa: boolean; + useApi: boolean; constructor(response: any) { super(response); @@ -44,5 +45,6 @@ export class OrganizationResponse extends BaseResponse { this.useEvents = this.getResponseProperty('UseEvents'); this.useTotp = this.getResponseProperty('UseTotp'); this.use2fa = this.getResponseProperty('Use2fa'); + this.useApi = this.getResponseProperty('UseApi'); } } diff --git a/src/models/response/profileOrganizationResponse.ts b/src/models/response/profileOrganizationResponse.ts index 3a7beac530..55eb3a6890 100644 --- a/src/models/response/profileOrganizationResponse.ts +++ b/src/models/response/profileOrganizationResponse.ts @@ -11,6 +11,7 @@ export class ProfileOrganizationResponse extends BaseResponse { useEvents: boolean; useTotp: boolean; use2fa: boolean; + useApi: boolean; selfHost: boolean; usersGetPremium: boolean; seats: number; @@ -30,6 +31,7 @@ export class ProfileOrganizationResponse extends BaseResponse { this.useEvents = this.getResponseProperty('UseEvents'); this.useTotp = this.getResponseProperty('UseTotp'); this.use2fa = this.getResponseProperty('Use2fa'); + this.useApi = this.getResponseProperty('UseApi'); this.selfHost = this.getResponseProperty('SelfHost'); this.usersGetPremium = this.getResponseProperty('UsersGetPremium'); this.seats = this.getResponseProperty('Seats'); From 9cc84f76d7f6f6b63497758f0e325c634be3f8be Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 2 Mar 2019 12:22:51 -0500 Subject: [PATCH 0741/1626] add useApi flags --- src/models/data/organizationData.ts | 2 ++ src/models/domain/organization.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/models/data/organizationData.ts b/src/models/data/organizationData.ts index 75212bcf06..d351a7024a 100644 --- a/src/models/data/organizationData.ts +++ b/src/models/data/organizationData.ts @@ -14,6 +14,7 @@ export class OrganizationData { useEvents: boolean; useTotp: boolean; use2fa: boolean; + useApi: boolean; selfHost: boolean; usersGetPremium: boolean; seats: number; @@ -31,6 +32,7 @@ export class OrganizationData { this.useEvents = response.useEvents; this.useTotp = response.useTotp; this.use2fa = response.use2fa; + this.useApi = response.useApi; this.selfHost = response.selfHost; this.usersGetPremium = response.usersGetPremium; this.seats = response.seats; diff --git a/src/models/domain/organization.ts b/src/models/domain/organization.ts index 961f8cc023..346a08778d 100644 --- a/src/models/domain/organization.ts +++ b/src/models/domain/organization.ts @@ -14,6 +14,7 @@ export class Organization { useEvents: boolean; useTotp: boolean; use2fa: boolean; + useApi: boolean; selfHost: boolean; usersGetPremium: boolean; seats: number; @@ -35,6 +36,7 @@ export class Organization { this.useEvents = obj.useEvents; this.useTotp = obj.useTotp; this.use2fa = obj.use2fa; + this.useApi = obj.useApi; this.selfHost = obj.selfHost; this.usersGetPremium = obj.usersGetPremium; this.seats = obj.seats; From 39f3a0788dd393d7769bfbba629b6dec31b98399 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 5 Mar 2019 16:22:00 -0500 Subject: [PATCH 0742/1626] dont continue when failed --- src/angular/components/lock.component.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 715c43f5b0..419124f038 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -62,7 +62,9 @@ export class LockComponent implements OnInit { const decPin = await this.cryptoService.decryptToUtf8(new CipherString(protectedPin)); this.lockService.pinLocked = false; failed = decPin !== this.pin; - this.doContinue(); + if (!failed) { + this.doContinue(); + } } else { const pinProtectedKey = await this.storageService.get(ConstantsService.pinProtectedKey); const protectedKeyCs = new CipherString(pinProtectedKey); @@ -71,7 +73,9 @@ export class LockComponent implements OnInit { failed = false; await this.setKeyAndContinue(new SymmetricCryptoKey(decKey)); } - } catch { } + } catch { + failed = true; + } if (failed) { this.invalidPinAttempts++; From cc27f98aaee3f2fc59dec0c596c7c5b9ad53cf5e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 5 Mar 2019 16:36:11 -0500 Subject: [PATCH 0743/1626] makeKeyFromPin in background context --- src/abstractions/crypto.service.ts | 1 + src/angular/components/lock.component.ts | 7 ++----- src/services/crypto.service.ts | 12 ++++++++++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 889500c40f..3ee45f1bc1 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -30,6 +30,7 @@ export abstract class CryptoService { clearKeys: () => Promise; toggleKey: () => Promise; makeKey: (password: string, salt: string, kdf: KdfType, kdfIterations: number) => Promise; + makeKeyFromPin: (pin: string, salt: string, kdf: KdfType, kdfIterations: number) => Promise; makeShareKey: () => Promise<[CipherString, SymmetricCryptoKey]>; makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, CipherString]>; makePinKey: (pin: string, salt: string, kdf: KdfType, kdfIterations: number) => Promise; diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 419124f038..31493ac872 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -66,12 +66,9 @@ export class LockComponent implements OnInit { this.doContinue(); } } else { - const pinProtectedKey = await this.storageService.get(ConstantsService.pinProtectedKey); - const protectedKeyCs = new CipherString(pinProtectedKey); - const pinKey = await this.cryptoService.makePinKey(this.pin, this.email, kdf, kdfIterations); - const decKey = await this.cryptoService.decryptToBytes(protectedKeyCs, pinKey); + const key = await this.cryptoService.makeKeyFromPin(this.pin, this.email, kdf, kdfIterations); failed = false; - await this.setKeyAndContinue(new SymmetricCryptoKey(decKey)); + await this.setKeyAndContinue(key); } } catch { failed = true; diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 865ec3bfc1..b2da1ce532 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -310,6 +310,18 @@ export class CryptoService implements CryptoServiceAbstraction { return new SymmetricCryptoKey(key); } + async makeKeyFromPin(pin: string, salt: string, kdf: KdfType, kdfIterations: number): + Promise { + const pinProtectedKey = await this.storageService.get(ConstantsService.pinProtectedKey); + if (pinProtectedKey == null) { + throw new Error('No PIN protected key found.'); + } + const protectedKeyCs = new CipherString(pinProtectedKey); + const pinKey = await this.makePinKey(pin, salt, kdf, kdfIterations); + const decKey = await this.decryptToBytes(protectedKeyCs, pinKey); + return new SymmetricCryptoKey(decKey); + } + async makeShareKey(): Promise<[CipherString, SymmetricCryptoKey]> { const shareKey = await this.cryptoFunctionService.randomBytes(64); const publicKey = await this.getPublicKey(); From 965e35604c584f7dcf786f04933c9b2fcf8a4ba0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 5 Mar 2019 17:12:51 -0500 Subject: [PATCH 0744/1626] remove finally() since it is not supported in older browsers --- src/misc/sequentialize.ts | 9 ++++++++- src/misc/throttle.ts | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/misc/sequentialize.ts b/src/misc/sequentialize.ts index ab64074472..ff8c4856b0 100644 --- a/src/misc/sequentialize.ts +++ b/src/misc/sequentialize.ts @@ -32,11 +32,18 @@ export function sequentialize(cacheKey: (args: any[]) => string) { return response; } - response = originalMethod.apply(this, args).finally(() => { + const onFinally = () => { cache.delete(argsCacheKey); if (cache.size === 0) { caches.delete(this); } + }; + response = originalMethod.apply(this, args).then((val: any) => { + onFinally(); + return val; + }).catch((err: any) => { + onFinally(); + throw err; }); cache.set(argsCacheKey, response); diff --git a/src/misc/throttle.ts b/src/misc/throttle.ts index 3a295ba69d..cc60c6aaf6 100644 --- a/src/misc/throttle.ts +++ b/src/misc/throttle.ts @@ -32,7 +32,7 @@ export function throttle(limit: number, throttleKey: (args: any[]) => string) { return new Promise((resolve, reject) => { const exec = () => { - originalMethod.apply(this, args).finally(() => { + const onFinally = () => { queue.splice(queue.indexOf(exec), 1); if (queue.length >= limit) { queue[limit - 1](); @@ -42,6 +42,13 @@ export function throttle(limit: number, throttleKey: (args: any[]) => string) { allThrottles.delete(this); } } + }; + originalMethod.apply(this, args).then((val: any) => { + onFinally(); + return val; + }).catch((err: any) => { + onFinally(); + throw err; }).then(resolve, reject); }; queue.push(exec); From 199884e6ae8f8b2c876ed361a3ba37c1f496e356 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 6 Mar 2019 14:31:32 -0500 Subject: [PATCH 0745/1626] init functions --- .../components/attachments.component.ts | 48 ++++++++++--------- .../components/folder-add-edit.component.ts | 24 ++++++---- .../components/password-history.component.ts | 10 ++-- 3 files changed, 47 insertions(+), 35 deletions(-) diff --git a/src/angular/components/attachments.component.ts b/src/angular/components/attachments.component.ts index af925f23ff..58704900ac 100644 --- a/src/angular/components/attachments.component.ts +++ b/src/angular/components/attachments.component.ts @@ -35,28 +35,7 @@ export class AttachmentsComponent implements OnInit { protected platformUtilsService: PlatformUtilsService, protected win: Window) { } async ngOnInit() { - this.cipherDomain = await this.loadCipher(); - this.cipher = await this.cipherDomain.decrypt(); - - this.hasUpdatedKey = await this.cryptoService.hasEncKey(); - const canAccessPremium = await this.userService.canAccessPremium(); - this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null; - - if (!this.canAccessAttachments) { - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t('premiumRequiredDesc'), this.i18nService.t('premiumRequired'), - this.i18nService.t('learnMore'), this.i18nService.t('cancel')); - if (confirmed) { - this.platformUtilsService.launchUri('https://vault.bitwarden.com/#/?premium=purchase'); - } - } else if (!this.hasUpdatedKey) { - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t('updateKey'), this.i18nService.t('featureUnavailable'), - this.i18nService.t('learnMore'), this.i18nService.t('cancel'), 'warning'); - if (confirmed) { - this.platformUtilsService.launchUri('https://help.bitwarden.com/article/update-encryption-key/'); - } - } + await this.init(); } async submit() { @@ -156,6 +135,31 @@ export class AttachmentsComponent implements OnInit { a.downloading = false; } + protected async init() { + this.cipherDomain = await this.loadCipher(); + this.cipher = await this.cipherDomain.decrypt(); + + this.hasUpdatedKey = await this.cryptoService.hasEncKey(); + const canAccessPremium = await this.userService.canAccessPremium(); + this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null; + + if (!this.canAccessAttachments) { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('premiumRequiredDesc'), this.i18nService.t('premiumRequired'), + this.i18nService.t('learnMore'), this.i18nService.t('cancel')); + if (confirmed) { + this.platformUtilsService.launchUri('https://vault.bitwarden.com/#/?premium=purchase'); + } + } else if (!this.hasUpdatedKey) { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('updateKey'), this.i18nService.t('featureUnavailable'), + this.i18nService.t('learnMore'), this.i18nService.t('cancel'), 'warning'); + if (confirmed) { + this.platformUtilsService.launchUri('https://help.bitwarden.com/article/update-encryption-key/'); + } + } + } + protected async reuploadCipherAttachment(attachment: AttachmentView, admin: boolean) { const a = (attachment as any); if (attachment.key != null || a.downloading || this.reuploadPromises[attachment.id] != null) { diff --git a/src/angular/components/folder-add-edit.component.ts b/src/angular/components/folder-add-edit.component.ts index 5e6314431a..2515459c47 100644 --- a/src/angular/components/folder-add-edit.component.ts +++ b/src/angular/components/folder-add-edit.component.ts @@ -26,16 +26,7 @@ export class FolderAddEditComponent implements OnInit { protected platformUtilsService: PlatformUtilsService) { } async ngOnInit() { - this.editMode = this.folderId != null; - - if (this.editMode) { - this.editMode = true; - this.title = this.i18nService.t('editFolder'); - const folder = await this.folderService.get(this.folderId); - this.folder = await folder.decrypt(); - } else { - this.title = this.i18nService.t('addFolder'); - } + await this.init(); } async submit(): Promise { @@ -77,4 +68,17 @@ export class FolderAddEditComponent implements OnInit { return true; } + + protected async init() { + this.editMode = this.folderId != null; + + if (this.editMode) { + this.editMode = true; + this.title = this.i18nService.t('editFolder'); + const folder = await this.folderService.get(this.folderId); + this.folder = await folder.decrypt(); + } else { + this.title = this.i18nService.t('addFolder'); + } + } } diff --git a/src/angular/components/password-history.component.ts b/src/angular/components/password-history.component.ts index d567d73d6c..f227e4ba54 100644 --- a/src/angular/components/password-history.component.ts +++ b/src/angular/components/password-history.component.ts @@ -14,9 +14,7 @@ export class PasswordHistoryComponent implements OnInit { protected i18nService: I18nService, private win: Window) { } async ngOnInit() { - const cipher = await this.cipherService.get(this.cipherId); - const decCipher = await cipher.decrypt(); - this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory; + await this.init(); } copy(password: string) { @@ -26,4 +24,10 @@ export class PasswordHistoryComponent implements OnInit { this.platformUtilsService.showToast('info', null, this.i18nService.t('valueCopied', this.i18nService.t('password'))); } + + protected async init() { + const cipher = await this.cipherService.get(this.cipherId); + const decCipher = await cipher.decrypt(); + this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory; + } } From 82f8f2b85ecc101608def0827f23ad2c7ee74ea4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 6 Mar 2019 15:46:40 -0500 Subject: [PATCH 0746/1626] set pinLocked based on failed check --- src/angular/components/lock.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 31493ac872..8c88f6363f 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -60,8 +60,8 @@ export class LockComponent implements OnInit { if (this.pinSet[0]) { const protectedPin = await this.storageService.get(ConstantsService.protectedPin); const decPin = await this.cryptoService.decryptToUtf8(new CipherString(protectedPin)); - this.lockService.pinLocked = false; failed = decPin !== this.pin; + this.lockService.pinLocked = failed; if (!failed) { this.doContinue(); } From e9cbf7b7dfdcf47435bf093d1befcf7fbfd6b948 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 6 Mar 2019 21:37:40 -0500 Subject: [PATCH 0747/1626] null checking error response model --- src/models/response/baseResponse.ts | 4 +++- src/models/response/errorResponse.ts | 12 +++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/models/response/baseResponse.ts b/src/models/response/baseResponse.ts index b4478bff3d..d773a7cfb6 100644 --- a/src/models/response/baseResponse.ts +++ b/src/models/response/baseResponse.ts @@ -9,8 +9,10 @@ export abstract class BaseResponse { if (propertyName == null || propertyName === '') { throw new Error('propertyName must not be null/empty.'); } - if (response == null) { + if (response == null && this.response != null) { response = this.response; + } else { + return null; } if (!exactName && response[propertyName] === undefined) { let otherCasePropertyName: string = null; diff --git a/src/models/response/errorResponse.ts b/src/models/response/errorResponse.ts index d28f6a2e02..fcce9f93d4 100644 --- a/src/models/response/errorResponse.ts +++ b/src/models/response/errorResponse.ts @@ -8,11 +8,13 @@ export class ErrorResponse extends BaseResponse { constructor(response: any, status: number, identityResponse?: boolean) { super(response); let errorModel = null; - const responseErrorModel = this.getResponseProperty('ErrorModel'); - if (responseErrorModel && identityResponse && response) { - errorModel = responseErrorModel; - } else if (response) { - errorModel = response; + if (response != null) { + const responseErrorModel = this.getResponseProperty('ErrorModel'); + if (responseErrorModel && identityResponse) { + errorModel = responseErrorModel; + } else { + errorModel = response; + } } if (errorModel) { From 0fa88b44b81730679fedf88a083b4b4b1f5c40ac Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 7 Mar 2019 07:53:48 -0500 Subject: [PATCH 0748/1626] fix null check on response --- src/models/response/baseResponse.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/models/response/baseResponse.ts b/src/models/response/baseResponse.ts index d773a7cfb6..645689f8f8 100644 --- a/src/models/response/baseResponse.ts +++ b/src/models/response/baseResponse.ts @@ -11,7 +11,8 @@ export abstract class BaseResponse { } if (response == null && this.response != null) { response = this.response; - } else { + } + if (response == null) { return null; } if (!exactName && response[propertyName] === undefined) { From 3b3b71d84192cc195f4626d6294b34d788641215 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 7 Mar 2019 10:58:27 -0500 Subject: [PATCH 0749/1626] apis for org api keys --- src/abstractions/api.service.ts | 3 +++ src/models/response/apiKeyResponse.ts | 10 ++++++++++ src/services/api.service.ts | 11 +++++++++++ 3 files changed, 24 insertions(+) create mode 100644 src/models/response/apiKeyResponse.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 3de4ffce01..8b20fc0fd5 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -52,6 +52,7 @@ import { VerifyBankRequest } from '../models/request/verifyBankRequest'; import { VerifyDeleteRecoverRequest } from '../models/request/verifyDeleteRecoverRequest'; import { VerifyEmailRequest } from '../models/request/verifyEmailRequest'; +import { ApiKeyResponse } from '../models/response/apiKeyResponse'; import { BillingResponse } from '../models/response/billingResponse'; import { BreachAccountResponse } from '../models/response/breachAccountResponse'; import { CipherResponse } from '../models/response/cipherResponse'; @@ -235,6 +236,8 @@ export abstract class ApiService { postLeaveOrganization: (id: string) => Promise; postOrganizationLicense: (data: FormData) => Promise; postOrganizationLicenseUpdate: (id: string, data: FormData) => Promise; + postOrganizationApiKey: (id: string, request: PasswordVerificationRequest) => Promise; + postOrganizationRotateApiKey: (id: string, request: PasswordVerificationRequest) => Promise; postOrganizationSeat: (id: string, request: SeatRequest) => Promise; postOrganizationStorage: (id: string, request: StorageRequest) => Promise; postOrganizationPayment: (id: string, request: PaymentRequest) => Promise; diff --git a/src/models/response/apiKeyResponse.ts b/src/models/response/apiKeyResponse.ts new file mode 100644 index 0000000000..b530b2dcc3 --- /dev/null +++ b/src/models/response/apiKeyResponse.ts @@ -0,0 +1,10 @@ +import { BaseResponse } from './baseResponse'; + +export class ApiKeyResponse extends BaseResponse { + apiKey: string; + + constructor(response: any) { + super(response); + this.apiKey = this.getResponseProperty('ApiKey'); + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index f7ecedba82..36622409a9 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -58,6 +58,7 @@ import { VerifyBankRequest } from '../models/request/verifyBankRequest'; import { VerifyDeleteRecoverRequest } from '../models/request/verifyDeleteRecoverRequest'; import { VerifyEmailRequest } from '../models/request/verifyEmailRequest'; +import { ApiKeyResponse } from '../models/response/apiKeyResponse'; import { BillingResponse } from '../models/response/billingResponse'; import { BreachAccountResponse } from '../models/response/breachAccountResponse'; import { CipherResponse } from '../models/response/cipherResponse'; @@ -767,6 +768,16 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/organizations/' + id + '/license', data, true, false); } + async postOrganizationApiKey(id: string, request: PasswordVerificationRequest): Promise { + const r = await this.send('POST', '/organizations/' + id + '/api-key', request, true, true); + return new ApiKeyResponse(r); + } + + async postOrganizationRotateApiKey(id: string, request: PasswordVerificationRequest): Promise { + const r = await this.send('POST', '/organizations/' + id + '/rotate-api-key', request, true, true); + return new ApiKeyResponse(r); + } + postOrganizationSeat(id: string, request: SeatRequest): Promise { return this.send('POST', '/organizations/' + id + '/seat', request, true, false); } From 79fea92b8152c70ed59188d6acd4948dd81ed235 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 7 Mar 2019 15:17:58 -0500 Subject: [PATCH 0750/1626] collection externalId --- src/models/data/collectionData.ts | 2 ++ src/models/domain/collection.ts | 4 +++- src/models/export/collection.ts | 4 ++++ src/models/request/collectionRequest.ts | 2 ++ src/models/response/collectionResponse.ts | 2 ++ src/models/view/collectionView.ts | 2 ++ 6 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/models/data/collectionData.ts b/src/models/data/collectionData.ts index 5d49ae2389..cc3ef64b97 100644 --- a/src/models/data/collectionData.ts +++ b/src/models/data/collectionData.ts @@ -4,12 +4,14 @@ export class CollectionData { id: string; organizationId: string; name: string; + externalId: string; readOnly: boolean; constructor(response: CollectionDetailsResponse) { this.id = response.id; this.organizationId = response.organizationId; this.name = response.name; + this.externalId = response.externalId; this.readOnly = response.readOnly; } } diff --git a/src/models/domain/collection.ts b/src/models/domain/collection.ts index 5d308f66b3..59bbf24d07 100644 --- a/src/models/domain/collection.ts +++ b/src/models/domain/collection.ts @@ -9,6 +9,7 @@ export class Collection extends Domain { id: string; organizationId: string; name: CipherString; + externalId: string; readOnly: boolean; constructor(obj?: CollectionData, alreadyEncrypted: boolean = false) { @@ -21,8 +22,9 @@ export class Collection extends Domain { id: null, organizationId: null, name: null, + externalId: null, readOnly: null, - }, alreadyEncrypted, ['id', 'organizationId', 'readOnly']); + }, alreadyEncrypted, ['id', 'organizationId', 'externalId', 'readOnly']); } decrypt(): Promise { diff --git a/src/models/export/collection.ts b/src/models/export/collection.ts index 85080e39f7..d4d9c7c3b1 100644 --- a/src/models/export/collection.ts +++ b/src/models/export/collection.ts @@ -5,11 +5,13 @@ export class Collection { const req = new Collection(); req.organizationId = '00000000-0000-0000-0000-000000000000'; req.name = 'Collection name'; + req.externalId = null; return req; } static toView(req: Collection, view = new CollectionView()) { view.name = req.name; + view.externalId = req.externalId; if (view.organizationId == null) { view.organizationId = req.organizationId; } @@ -18,10 +20,12 @@ export class Collection { organizationId: string; name: string; + externalId: string; // Use build method instead of ctor so that we can control order of JSON stringify for pretty print build(o: CollectionView) { this.organizationId = o.organizationId; this.name = o.name; + this.externalId = o.externalId; } } diff --git a/src/models/request/collectionRequest.ts b/src/models/request/collectionRequest.ts index 716b0acab5..20f869567d 100644 --- a/src/models/request/collectionRequest.ts +++ b/src/models/request/collectionRequest.ts @@ -4,6 +4,7 @@ import { SelectionReadOnlyRequest } from './selectionReadOnlyRequest'; export class CollectionRequest { name: string; + externalId: string; groups: SelectionReadOnlyRequest[] = []; constructor(collection?: Collection) { @@ -11,5 +12,6 @@ export class CollectionRequest { return; } this.name = collection.name ? collection.name.encryptedString : null; + this.externalId = collection.externalId; } } diff --git a/src/models/response/collectionResponse.ts b/src/models/response/collectionResponse.ts index 458945334f..eb9441c4d7 100644 --- a/src/models/response/collectionResponse.ts +++ b/src/models/response/collectionResponse.ts @@ -5,12 +5,14 @@ export class CollectionResponse extends BaseResponse { id: string; organizationId: string; name: string; + externalId: string; constructor(response: any) { super(response); this.id = this.getResponseProperty('Id'); this.organizationId = this.getResponseProperty('OrganizationId'); this.name = this.getResponseProperty('Name'); + this.externalId = this.getResponseProperty('ExternalId'); } } diff --git a/src/models/view/collectionView.ts b/src/models/view/collectionView.ts index 7f4e52ed90..760992ef89 100644 --- a/src/models/view/collectionView.ts +++ b/src/models/view/collectionView.ts @@ -7,6 +7,7 @@ export class CollectionView implements View, ITreeNodeObject { id: string = null; organizationId: string = null; name: string = null; + externalId: string = null; readOnly: boolean = null; constructor(c?: Collection) { @@ -17,5 +18,6 @@ export class CollectionView implements View, ITreeNodeObject { this.id = c.id; this.organizationId = c.organizationId; this.readOnly = c.readOnly; + this.externalId = c.externalId; } } From 650ab56353dc263da6522e3a96dc846af43635a8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 7 Mar 2019 23:54:52 -0500 Subject: [PATCH 0751/1626] fix enabled --- src/models/response/twoFactorYubiKeyResponse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/response/twoFactorYubiKeyResponse.ts b/src/models/response/twoFactorYubiKeyResponse.ts index c717210141..92d8cffb30 100644 --- a/src/models/response/twoFactorYubiKeyResponse.ts +++ b/src/models/response/twoFactorYubiKeyResponse.ts @@ -11,7 +11,7 @@ export class TwoFactorYubiKeyResponse extends BaseResponse { constructor(response: any) { super(response); - this.enabled = this.getResponseProperty('Enable'); + this.enabled = this.getResponseProperty('Enabled'); this.key1 = this.getResponseProperty('Key1'); this.key2 = this.getResponseProperty('Key2'); this.key3 = this.getResponseProperty('Key3'); From 49e06e77c4913867fc468f7d9e0b2b1529c1d181 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 11 Mar 2019 22:36:29 -0400 Subject: [PATCH 0752/1626] electron storage implementation --- package-lock.json | 93 ++++++++++++++++--- package.json | 1 + .../services/electronStorage.service.ts | 38 ++++++++ 3 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 src/electron/services/electronStorage.service.ts diff --git a/package-lock.json b/package-lock.json index ee4d33fe8d..ad035477f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1560,6 +1560,18 @@ "tree-kill": "^1.1.0" } }, + "conf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-1.4.0.tgz", + "integrity": "sha512-bzlVWS2THbMetHqXKB8ypsXN4DQ/1qopGwNJi1eYbpwesJcd86FBjFciCQX/YwAhp9bM7NVnPFqZ5LpV7gP0Dg==", + "requires": { + "dot-prop": "^4.1.0", + "env-paths": "^1.0.0", + "make-dir": "^1.0.0", + "pkg-up": "^2.0.0", + "write-file-atomic": "^2.3.0" + } + }, "configstore": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", @@ -2018,7 +2030,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", - "dev": true, "requires": { "is-obj": "^1.0.0" } @@ -2120,6 +2131,14 @@ "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.17.tgz", "integrity": "sha512-v+Af5W5z99ehhaLOfE9eTSXUwjzh2wFlQjz51dvkZ6ZIrET6OB/zAZPvsuwT6tm3t5x+M1r+Ed3U3xtPZYAyuQ==" }, + "electron-store": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-1.3.0.tgz", + "integrity": "sha512-r1Pdl5MwpiCxgbsl0qnwv/GABO5+J/JTO16+KyqL+bOITIk9o3cq3Sw69uO9NgPkpfcKeEwxtJFbtbiBlGTiDA==", + "requires": { + "conf": "^1.3.0" + } + }, "electron-updater": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.0.6.tgz", @@ -2232,8 +2251,7 @@ "env-paths": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz", - "integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=", - "dev": true + "integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=" }, "error-ex": { "version": "1.3.2", @@ -3421,9 +3439,9 @@ } }, "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" }, "handlebars": { "version": "4.0.11", @@ -3693,8 +3711,7 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" }, "indent-string": { "version": "2.1.0", @@ -3899,8 +3916,7 @@ "is-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" }, "is-path-inside": { "version": "1.0.1", @@ -4616,6 +4632,22 @@ } } }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + } + } + }, "lodash": { "version": "4.17.10", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", @@ -4744,7 +4776,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, "requires": { "pify": "^3.0.0" } @@ -5728,6 +5759,27 @@ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", "dev": true }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, "package-json": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", @@ -5934,6 +5986,24 @@ "pinkie": "^2.0.0" } }, + "pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", + "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=", + "requires": { + "find-up": "^2.1.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + } + } + }, "plugin-error": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", @@ -8088,7 +8158,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", - "dev": true, "requires": { "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", diff --git a/package.json b/package.json index 0b62df2db0..3d89b5696c 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "core-js": "2.6.2", "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", "electron-log": "2.2.17", + "electron-store": "1.3.0", "electron-updater": "4.0.6", "form-data": "2.3.2", "jsdom": "13.2.0", diff --git a/src/electron/services/electronStorage.service.ts b/src/electron/services/electronStorage.service.ts new file mode 100644 index 0000000000..3cc21b5eea --- /dev/null +++ b/src/electron/services/electronStorage.service.ts @@ -0,0 +1,38 @@ +import * as fs from 'fs'; + +import { StorageService } from '../../abstractions/storage.service'; + +import { NodeUtils } from '../../misc/nodeUtils'; + +// tslint:disable-next-line +const Store = require('electron-store'); + +export class ElectronStorageService implements StorageService { + private store: any; + + constructor(dir: string, defaults = {}) { + if (!fs.existsSync(dir)) { + NodeUtils.mkdirpSync(dir, '700'); + } + const storeConfig: any = { + defaults: defaults, + name: 'data', + }; + this.store = new Store(storeConfig); + } + + get(key: string): Promise { + const val = this.store.get(key) as T; + return Promise.resolve(val != null ? val : null); + } + + save(key: string, obj: any): Promise { + this.store.set(key, obj); + return Promise.resolve(); + } + + remove(key: string): Promise { + this.store.delete(key); + return Promise.resolve(); + } +} From 13a160fb795a69bad1edbb3fc5fd5c7c15396e03 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 15 Mar 2019 22:33:19 -0400 Subject: [PATCH 0753/1626] move shared CLI items to jslib --- package-lock.json | 24 +++- package.json | 2 + src/cli/commands/update.command.ts | 83 ++++++++++++ src/cli/models/response.ts | 43 ++++++ src/cli/models/response/baseResponse.ts | 3 + src/cli/models/response/listResponse.ts | 11 ++ src/cli/models/response/messageResponse.ts | 15 +++ src/cli/models/response/stringResponse.ts | 11 ++ src/cli/services/cliPlatformUtils.service.ts | 132 +++++++++++++++++++ src/cli/services/consoleLog.service.ts | 53 ++++++++ src/services/noopMessaging.service.ts | 7 + 11 files changed, 380 insertions(+), 4 deletions(-) create mode 100644 src/cli/commands/update.command.ts create mode 100644 src/cli/models/response.ts create mode 100644 src/cli/models/response/baseResponse.ts create mode 100644 src/cli/models/response/listResponse.ts create mode 100644 src/cli/models/response/messageResponse.ts create mode 100644 src/cli/models/response/stringResponse.ts create mode 100644 src/cli/services/cliPlatformUtils.service.ts create mode 100644 src/cli/services/consoleLog.service.ts create mode 100644 src/services/noopMessaging.service.ts diff --git a/package-lock.json b/package-lock.json index ad035477f0..40a167e3e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -97,6 +97,15 @@ "msgpack5": "^4.0.2" } }, + "@types/commander": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.2.tgz", + "integrity": "sha512-0QEFiR8ljcHp9bAbWxecjVRuAMr16ivPiGOw6KFQBVrVd0RQIcM3xKdRisH2EDWgVWujiYtHwhSkSUoAAGzH7Q==", + "dev": true, + "requires": { + "commander": "*" + } + }, "@types/form-data": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", @@ -1497,10 +1506,9 @@ } }, "commander": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", - "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=", - "dev": true + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.18.0.tgz", + "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==" }, "compare-versions": { "version": "3.3.1", @@ -1558,6 +1566,14 @@ "spawn-command": "^0.0.2-1", "supports-color": "^3.2.3", "tree-kill": "^1.1.0" + }, + "dependencies": { + "commander": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", + "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=", + "dev": true + } } }, "conf": { diff --git a/package.json b/package.json index 3d89b5696c..a5f7def7d1 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "test:node:watch": "concurrently -k -n TSC,Node -c yellow,cyan \"npm run build:watch\" \"nodemon -w ./dist --delay 500ms --exec jasmine\"" }, "devDependencies": { + "@types/commander": "^2.12.2", "@types/form-data": "^2.2.1", "@types/jasmine": "^2.8.8", "@types/lowdb": "^1.0.5", @@ -72,6 +73,7 @@ "@aspnet/signalr": "1.0.4", "@aspnet/signalr-protocol-msgpack": "1.0.4", "big-integer": "1.6.36", + "commander": "2.18.0", "core-js": "2.6.2", "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", "electron-log": "2.2.17", diff --git a/src/cli/commands/update.command.ts b/src/cli/commands/update.command.ts new file mode 100644 index 0000000000..600a6eb2bf --- /dev/null +++ b/src/cli/commands/update.command.ts @@ -0,0 +1,83 @@ +import * as program from 'commander'; +import * as fetch from 'node-fetch'; + +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +import { Response } from '../models/response'; +import { MessageResponse } from '../models/response/messageResponse'; + +export class UpdateCommand { + inPkg: boolean = false; + + constructor(private platformUtilsService: PlatformUtilsService, private repoName: string, + private executableName: string) { + this.inPkg = !!(process as any).pkg; + } + + async run(cmd: program.Command): Promise { + const currentVersion = this.platformUtilsService.getApplicationVersion(); + + const response = await fetch.default('https://api.github.com/repos/bitwarden/' + + this.repoName + '/releases/latest'); + if (response.status === 200) { + const responseJson = await response.json(); + const res = new MessageResponse(null, null); + + const tagName: string = responseJson.tag_name; + if (tagName === ('v' + currentVersion)) { + res.title = 'No update available.'; + res.noColor = true; + return Response.success(res); + } + + let downloadUrl: string = null; + if (responseJson.assets != null) { + for (const a of responseJson.assets) { + const download: string = a.browser_download_url; + if (download == null) { + continue; + } + + if (download.indexOf('.zip') === -1) { + continue; + } + + if (process.platform === 'win32' && download.indexOf(this.executableName + '-windows') > -1) { + downloadUrl = download; + break; + } else if (process.platform === 'darwin' && download.indexOf(this.executableName + '-macos') > -1) { + downloadUrl = download; + break; + } else if (process.platform === 'linux' && download.indexOf(this.executableName + '-linux') > -1) { + downloadUrl = download; + break; + } + } + } + + res.title = 'A new version is available: ' + tagName; + if (downloadUrl == null) { + downloadUrl = 'https://github.com/bitwarden/' + this.repoName + '/releases'; + } else { + res.raw = downloadUrl; + } + res.message = ''; + if (responseJson.body != null && responseJson.body !== '') { + res.message = responseJson.body + '\n\n'; + } + + res.message += 'You can download this update at ' + downloadUrl; + + if (this.inPkg) { + res.message += '\n\nIf you installed this CLI through a package manager ' + + 'you should probably update using its update command instead.'; + } else { + res.message += '\n\nIf you installed this CLI through NPM ' + + 'you should update using `npm install -g @bitwarden/' + this.repoName + '`'; + } + return Response.success(res); + } else { + return Response.error('Error contacting update API: ' + response.status); + } + } +} diff --git a/src/cli/models/response.ts b/src/cli/models/response.ts new file mode 100644 index 0000000000..d361c3a2b6 --- /dev/null +++ b/src/cli/models/response.ts @@ -0,0 +1,43 @@ +import { BaseResponse } from './response/baseResponse'; + +export class Response { + static error(error: any): Response { + const res = new Response(); + res.success = false; + if (typeof (error) === 'string') { + res.message = error; + } else { + res.message = error.message != null ? error.message : error.toString(); + } + return res; + } + + static notFound(): Response { + return Response.error('Not found.'); + } + + static badRequest(message: string): Response { + return Response.error(message); + } + + static multipleResults(ids: string[]): Response { + let msg = 'More than one result was found. Try getting a specific object by `id` instead. ' + + 'The following objects were found:'; + ids.forEach((id) => { + msg += '\n' + id; + }); + return Response.error(msg); + } + + static success(data?: BaseResponse): Response { + const res = new Response(); + res.success = true; + res.data = data; + return res; + } + + success: boolean; + message: string; + errorCode: number; + data: BaseResponse; +} diff --git a/src/cli/models/response/baseResponse.ts b/src/cli/models/response/baseResponse.ts new file mode 100644 index 0000000000..9d8beca059 --- /dev/null +++ b/src/cli/models/response/baseResponse.ts @@ -0,0 +1,3 @@ +export interface BaseResponse { + object: string; +} diff --git a/src/cli/models/response/listResponse.ts b/src/cli/models/response/listResponse.ts new file mode 100644 index 0000000000..7995bd4fe8 --- /dev/null +++ b/src/cli/models/response/listResponse.ts @@ -0,0 +1,11 @@ +import { BaseResponse } from './baseResponse'; + +export class ListResponse implements BaseResponse { + object: string; + data: BaseResponse[]; + + constructor(data: BaseResponse[]) { + this.object = 'list'; + this.data = data; + } +} diff --git a/src/cli/models/response/messageResponse.ts b/src/cli/models/response/messageResponse.ts new file mode 100644 index 0000000000..448e3db795 --- /dev/null +++ b/src/cli/models/response/messageResponse.ts @@ -0,0 +1,15 @@ +import { BaseResponse } from './baseResponse'; + +export class MessageResponse implements BaseResponse { + object: string; + title: string; + message: string; + raw: string; + noColor = false; + + constructor(title: string, message: string) { + this.object = 'message'; + this.title = title; + this.message = message; + } +} diff --git a/src/cli/models/response/stringResponse.ts b/src/cli/models/response/stringResponse.ts new file mode 100644 index 0000000000..b9a0f04412 --- /dev/null +++ b/src/cli/models/response/stringResponse.ts @@ -0,0 +1,11 @@ +import { BaseResponse } from './baseResponse'; + +export class StringResponse implements BaseResponse { + object: string; + data: string; + + constructor(data: string) { + this.object = 'string'; + this.data = data; + } +} diff --git a/src/cli/services/cliPlatformUtils.service.ts b/src/cli/services/cliPlatformUtils.service.ts new file mode 100644 index 0000000000..ad4f69b6fc --- /dev/null +++ b/src/cli/services/cliPlatformUtils.service.ts @@ -0,0 +1,132 @@ + +import { DeviceType } from '../../enums/deviceType'; + +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +export class CliPlatformUtilsService implements PlatformUtilsService { + identityClientId: string; + + private deviceCache: DeviceType = null; + + constructor(identityClientId: string, private packageJson: any) { + this.identityClientId = identityClientId; + } + + getDevice(): DeviceType { + if (!this.deviceCache) { + switch (process.platform) { + case 'win32': + this.deviceCache = DeviceType.WindowsDesktop; + break; + case 'darwin': + this.deviceCache = DeviceType.MacOsDesktop; + break; + case 'linux': + default: + this.deviceCache = DeviceType.LinuxDesktop; + break; + } + } + + return this.deviceCache; + } + + getDeviceString(): string { + const device = DeviceType[this.getDevice()].toLowerCase(); + return device.replace('desktop', ''); + } + + isFirefox() { + return false; + } + + isChrome() { + return false; + } + + isEdge() { + return false; + } + + isOpera() { + return false; + } + + isVivaldi() { + return false; + } + + isSafari() { + return false; + } + + isIE() { + return false; + } + + isMacAppStore() { + return false; + } + + analyticsId() { + return null as string; + } + + isViewOpen() { + return false; + } + + lockTimeout(): number { + return null; + } + + launchUri(uri: string, options?: any): void { + throw new Error('Not implemented.'); + } + + saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void { + throw new Error('Not implemented.'); + } + + getApplicationVersion(): string { + return this.packageJson.version; + } + + supportsU2f(win: Window) { + return false; + } + + supportsDuo(): boolean { + return false; + } + + showToast(type: 'error' | 'success' | 'warning' | 'info', title: string, text: string | string[], + options?: any): void { + throw new Error('Not implemented.'); + } + + showDialog(text: string, title?: string, confirmText?: string, cancelText?: string, type?: string): + Promise { + throw new Error('Not implemented.'); + } + + eventTrack(action: string, label?: string, options?: any) { + throw new Error('Not implemented.'); + } + + isDev(): boolean { + return process.env.BWCLI_ENV === 'development'; + } + + isSelfHost(): boolean { + return false; + } + + copyToClipboard(text: string, options?: any): void { + throw new Error('Not implemented.'); + } + + readFromClipboard(options?: any): Promise { + throw new Error('Not implemented.'); + } +} diff --git a/src/cli/services/consoleLog.service.ts b/src/cli/services/consoleLog.service.ts new file mode 100644 index 0000000000..5e1904790d --- /dev/null +++ b/src/cli/services/consoleLog.service.ts @@ -0,0 +1,53 @@ +import { LogLevelType } from '../../enums/logLevelType'; + +import { LogService as LogServiceAbstraction } from '../../abstractions/log.service'; + +export class ConsoleLogService implements LogServiceAbstraction { + constructor(private isDev: boolean, private filter: (level: LogLevelType) => boolean = null) { } + + debug(message: string) { + if (!this.isDev) { + return; + } + this.write(LogLevelType.Debug, message); + } + + info(message: string) { + this.write(LogLevelType.Info, message); + } + + warning(message: string) { + this.write(LogLevelType.Warning, message); + } + + error(message: string) { + this.write(LogLevelType.Error, message); + } + + write(level: LogLevelType, message: string) { + if (this.filter != null && this.filter(level)) { + return; + } + + switch (level) { + case LogLevelType.Debug: + // tslint:disable-next-line + console.log(message); + break; + case LogLevelType.Info: + // tslint:disable-next-line + console.log(message); + break; + case LogLevelType.Warning: + // tslint:disable-next-line + console.warn(message); + break; + case LogLevelType.Error: + // tslint:disable-next-line + console.error(message); + break; + default: + break; + } + } +} diff --git a/src/services/noopMessaging.service.ts b/src/services/noopMessaging.service.ts new file mode 100644 index 0000000000..c1a1443ca7 --- /dev/null +++ b/src/services/noopMessaging.service.ts @@ -0,0 +1,7 @@ +import { MessagingService } from '../abstractions/messaging.service'; + +export class NoopMessagingService implements MessagingService { + send(subscriber: string, arg: any = {}) { + // Do nothing... + } +} From b5b4222b325a5fab7fabfd53f23de1876ffccec8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 16 Mar 2019 11:26:31 -0400 Subject: [PATCH 0754/1626] base cli program --- package-lock.json | 102 +++++++++++++++++++++++++-------------- package.json | 1 + src/cli/baseProgram.ts | 107 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 36 deletions(-) create mode 100644 src/cli/baseProgram.ts diff --git a/package-lock.json b/package-lock.json index 40a167e3e6..d58da16712 100644 --- a/package-lock.json +++ b/package-lock.json @@ -341,10 +341,12 @@ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", - "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", - "dev": true + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } }, "ansi-wrap": { "version": "0.1.0", @@ -1275,38 +1277,27 @@ } }, "chalk": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", - "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", - "dev": true, + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", "requires": { - "ansi-styles": "^1.1.0", - "escape-string-regexp": "^1.0.0", - "has-ansi": "^0.1.0", - "strip-ansi": "^0.3.0", - "supports-color": "^0.2.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" }, "dependencies": { - "ansi-regex": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", - "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", - "dev": true - }, - "strip-ansi": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", - "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", - "dev": true, - "requires": { - "ansi-regex": "^0.2.1" - } + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "supports-color": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", - "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", - "dev": true + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } } } }, @@ -1445,7 +1436,6 @@ "version": "1.9.2", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", - "dev": true, "requires": { "color-name": "1.1.1" } @@ -1453,8 +1443,7 @@ "color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", - "dev": true + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=" }, "colors": { "version": "1.1.2", @@ -1568,11 +1557,53 @@ "tree-kill": "^1.1.0" }, "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", + "dev": true + }, + "ansi-styles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", + "dev": true + }, + "chalk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", + "dev": true, + "requires": { + "ansi-styles": "^1.1.0", + "escape-string-regexp": "^1.0.0", + "has-ansi": "^0.1.0", + "strip-ansi": "^0.3.0", + "supports-color": "^0.2.0" + }, + "dependencies": { + "supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "dev": true + } + } + }, "commander": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=", "dev": true + }, + "strip-ansi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.1" + } } } }, @@ -2296,8 +2327,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { "version": "1.8.1", diff --git a/package.json b/package.json index a5f7def7d1..06253ce01c 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "@aspnet/signalr": "1.0.4", "@aspnet/signalr-protocol-msgpack": "1.0.4", "big-integer": "1.6.36", + "chalk": "2.4.1", "commander": "2.18.0", "core-js": "2.6.2", "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", diff --git a/src/cli/baseProgram.ts b/src/cli/baseProgram.ts new file mode 100644 index 0000000000..bf33a92b1b --- /dev/null +++ b/src/cli/baseProgram.ts @@ -0,0 +1,107 @@ +import * as chk from 'chalk'; + +import { Response } from './models/response'; +import { ListResponse } from './models/response/listResponse'; +import { MessageResponse } from './models/response/messageResponse'; +import { StringResponse } from './models/response/stringResponse'; + +import { UserService } from '../abstractions/user.service'; + +const chalk = chk.default; + +export abstract class BaseProgram { + constructor(private userService: UserService, private writeLn: (s: string, finalLine: boolean) => void) { } + + protected processResponse(response: Response, exitImmediately = false, dataProcessor: () => string = null) { + if (!response.success) { + if (process.env.BW_QUIET !== 'true') { + if (process.env.BW_RESPONSE === 'true') { + this.writeLn(this.getJson(response), true); + } else { + this.writeLn(chalk.redBright(response.message), true); + } + } + if (exitImmediately) { + process.exit(1); + } else { + process.exitCode = 1; + } + return; + } + + if (process.env.BW_RESPONSE === 'true') { + this.writeLn(this.getJson(response), true); + } else if (response.data != null) { + let out: string = dataProcessor != null ? dataProcessor() : null; + if (out == null) { + if (response.data.object === 'string') { + const data = (response.data as StringResponse).data; + if (data != null) { + out = data; + } + } else if (response.data.object === 'list') { + out = this.getJson((response.data as ListResponse).data); + } else if (response.data.object === 'message') { + out = this.getMessage(response); + } else { + out = this.getJson(response.data); + } + } + + if (out != null && process.env.BW_QUIET !== 'true') { + this.writeLn(out, true); + } + } + if (exitImmediately) { + process.exit(0); + } else { + process.exitCode = 0; + } + } + + protected getJson(obj: any): string { + if (process.env.BW_PRETTY === 'true') { + return JSON.stringify(obj, null, ' '); + } else { + return JSON.stringify(obj); + } + } + + protected getMessage(response: Response) { + const message = (response.data as MessageResponse); + if (process.env.BW_RAW === 'true' && message.raw != null) { + return message.raw; + } + + let out: string = ''; + if (message.title != null) { + if (message.noColor) { + out = message.title; + } else { + out = chalk.greenBright(message.title); + } + } + if (message.message != null) { + if (message.title != null) { + out += '\n'; + } + out += message.message; + } + return out.trim() === '' ? null : out; + } + + protected async exitIfAuthed() { + const authed = await this.userService.isAuthenticated(); + if (authed) { + const email = await this.userService.getEmail(); + this.processResponse(Response.error('You are already logged in as ' + email + '.'), true); + } + } + + protected async exitIfNotAuthed() { + const authed = await this.userService.isAuthenticated(); + if (!authed) { + this.processResponse(Response.error('You are not logged in.'), true); + } + } +} From d4c2b20a2594fcac1fdabf312b7289657b4af0c8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 18 Mar 2019 10:33:29 -0400 Subject: [PATCH 0755/1626] shared login and logout commands --- package-lock.json | 287 ++++++++++++++++++++++++++++- package.json | 2 + src/cli/commands/login.command.ts | 150 +++++++++++++++ src/cli/commands/logout.command.ts | 19 ++ src/cli/commands/update.command.ts | 19 +- 5 files changed, 464 insertions(+), 13 deletions(-) create mode 100644 src/cli/commands/login.command.ts create mode 100644 src/cli/commands/logout.command.ts diff --git a/package-lock.json b/package-lock.json index d58da16712..a0850947d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -115,6 +115,16 @@ "@types/node": "*" } }, + "@types/inquirer": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-0.0.43.tgz", + "integrity": "sha512-xgyfKZVMFqE8aIKy1xfFVsX2MxyXUNgjgmbF6dRbR3sL+ZM5K4ka/9L4mmTwX8eTeVYtduyXu0gUVwVJa1HbNw==", + "dev": true, + "requires": { + "@types/rx": "*", + "@types/through": "*" + } + }, "@types/jasmine": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.8.tgz", @@ -175,6 +185,141 @@ "@types/node": "*" } }, + "@types/rx": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/rx/-/rx-4.1.1.tgz", + "integrity": "sha1-WY/JSla67ZdfGUV04PVy/Y5iekg=", + "dev": true, + "requires": { + "@types/rx-core": "*", + "@types/rx-core-binding": "*", + "@types/rx-lite": "*", + "@types/rx-lite-aggregates": "*", + "@types/rx-lite-async": "*", + "@types/rx-lite-backpressure": "*", + "@types/rx-lite-coincidence": "*", + "@types/rx-lite-experimental": "*", + "@types/rx-lite-joinpatterns": "*", + "@types/rx-lite-testing": "*", + "@types/rx-lite-time": "*", + "@types/rx-lite-virtualtime": "*" + } + }, + "@types/rx-core": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/rx-core/-/rx-core-4.0.3.tgz", + "integrity": "sha1-CzNUsSOM7b4rdPYybxOdvHpZHWA=", + "dev": true + }, + "@types/rx-core-binding": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/rx-core-binding/-/rx-core-binding-4.0.4.tgz", + "integrity": "sha512-5pkfxnC4w810LqBPUwP5bg7SFR/USwhMSaAeZQQbEHeBp57pjKXRlXmqpMrLJB4y1oglR/c2502853uN0I+DAQ==", + "dev": true, + "requires": { + "@types/rx-core": "*" + } + }, + "@types/rx-lite": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/rx-lite/-/rx-lite-4.0.6.tgz", + "integrity": "sha512-oYiDrFIcor9zDm0VDUca1UbROiMYBxMLMaM6qzz4ADAfOmA9r1dYEcAFH+2fsPI5BCCjPvV9pWC3X3flbrvs7w==", + "dev": true, + "requires": { + "@types/rx-core": "*", + "@types/rx-core-binding": "*" + } + }, + "@types/rx-lite-aggregates": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/rx-lite-aggregates/-/rx-lite-aggregates-4.0.3.tgz", + "integrity": "sha512-MAGDAHy8cRatm94FDduhJF+iNS5//jrZ/PIfm+QYw9OCeDgbymFHChM8YVIvN2zArwsRftKgE33QfRWvQk4DPg==", + "dev": true, + "requires": { + "@types/rx-lite": "*" + } + }, + "@types/rx-lite-async": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/rx-lite-async/-/rx-lite-async-4.0.2.tgz", + "integrity": "sha512-vTEv5o8l6702ZwfAM5aOeVDfUwBSDOs+ARoGmWAKQ6LOInQ8J4/zjM7ov12fuTpktUKdMQjkeCp07Vd73mPkxw==", + "dev": true, + "requires": { + "@types/rx-lite": "*" + } + }, + "@types/rx-lite-backpressure": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/rx-lite-backpressure/-/rx-lite-backpressure-4.0.3.tgz", + "integrity": "sha512-Y6aIeQCtNban5XSAF4B8dffhIKu6aAy/TXFlScHzSxh6ivfQBQw6UjxyEJxIOt3IT49YkS+siuayM2H/Q0cmgA==", + "dev": true, + "requires": { + "@types/rx-lite": "*" + } + }, + "@types/rx-lite-coincidence": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/rx-lite-coincidence/-/rx-lite-coincidence-4.0.3.tgz", + "integrity": "sha512-1VNJqzE9gALUyMGypDXZZXzR0Tt7LC9DdAZQ3Ou/Q0MubNU35agVUNXKGHKpNTba+fr8GdIdkC26bRDqtCQBeQ==", + "dev": true, + "requires": { + "@types/rx-lite": "*" + } + }, + "@types/rx-lite-experimental": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/rx-lite-experimental/-/rx-lite-experimental-4.0.1.tgz", + "integrity": "sha1-xTL1y98/LBXaFt7Ykw0bKYQCPL0=", + "dev": true, + "requires": { + "@types/rx-lite": "*" + } + }, + "@types/rx-lite-joinpatterns": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/rx-lite-joinpatterns/-/rx-lite-joinpatterns-4.0.1.tgz", + "integrity": "sha1-9w/jcFGKhDLykVjMkv+1a05K/D4=", + "dev": true, + "requires": { + "@types/rx-lite": "*" + } + }, + "@types/rx-lite-testing": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/rx-lite-testing/-/rx-lite-testing-4.0.1.tgz", + "integrity": "sha1-IbGdEfTf1v/vWp0WSOnIh5v+Iek=", + "dev": true, + "requires": { + "@types/rx-lite-virtualtime": "*" + } + }, + "@types/rx-lite-time": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/rx-lite-time/-/rx-lite-time-4.0.3.tgz", + "integrity": "sha512-ukO5sPKDRwCGWRZRqPlaAU0SKVxmWwSjiOrLhoQDoWxZWg6vyB9XLEZViKOzIO6LnTIQBlk4UylYV0rnhJLxQw==", + "dev": true, + "requires": { + "@types/rx-lite": "*" + } + }, + "@types/rx-lite-virtualtime": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/rx-lite-virtualtime/-/rx-lite-virtualtime-4.0.3.tgz", + "integrity": "sha512-3uC6sGmjpOKatZSVHI2xB1+dedgml669ZRvqxy+WqmGJDVusOdyxcKfyzjW0P3/GrCiN4nmRkLVMhPwHCc5QLg==", + "dev": true, + "requires": { + "@types/rx-lite": "*" + } + }, + "@types/through": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.29.tgz", + "integrity": "sha512-9a7C5VHh+1BKblaYiq+7Tfc+EOmjMdZaD1MYtkQjSoxgB69tBjW98ry6SKsi4zEIWztLOMRuL87A3bdT/Fc/4w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/tldjs": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@types/tldjs/-/tldjs-2.3.0.tgz", @@ -326,6 +471,11 @@ "ansi-wrap": "0.1.0" } }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" + }, "ansi-red": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", @@ -1301,6 +1451,11 @@ } } }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + }, "chokidar": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", @@ -1390,6 +1545,19 @@ "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", "dev": true }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" + }, "cliui": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", @@ -2551,6 +2719,26 @@ } } }, + "external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -2674,6 +2862,14 @@ "pend": "~1.2.0" } }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, "fileset": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", @@ -3811,6 +4007,55 @@ } } }, + "inquirer": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.0.tgz", + "integrity": "sha512-QIEQG4YyQ2UYZGDC4srMZ7BjHOmNk1lR2JQj5UknBapklm6WHA+VVH7N+sUdX3A7NeCfGF8o4X1S3Ao7nAcIeg==", + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.0", + "figures": "^2.0.0", + "lodash": "^4.17.10", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.1.0", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -4964,6 +5209,11 @@ "mime-db": "~1.35.0" } }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -5059,6 +5309,11 @@ } } }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" + }, "nan": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", @@ -5744,6 +5999,14 @@ "wrappy": "1" } }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "requires": { + "mimic-fn": "^1.0.0" + } + }, "optimist": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", @@ -5796,8 +6059,7 @@ "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "p-finally": { "version": "1.0.0", @@ -6541,6 +6803,15 @@ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", "dev": true }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -6582,6 +6853,14 @@ "inherits": "^2.0.1" } }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "requires": { + "is-promise": "^2.1.0" + } + }, "rx": { "version": "2.3.24", "resolved": "https://registry.npmjs.org/rx/-/rx-2.3.24.tgz", @@ -7328,8 +7607,7 @@ "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "through2": { "version": "0.2.3", @@ -7403,7 +7681,6 @@ "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, "requires": { "os-tmpdir": "~1.0.2" } diff --git a/package.json b/package.json index 06253ce01c..909e8a4d2f 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "devDependencies": { "@types/commander": "^2.12.2", "@types/form-data": "^2.2.1", + "@types/inquirer": "^0.0.43", "@types/jasmine": "^2.8.8", "@types/lowdb": "^1.0.5", "@types/lunr": "^2.1.6", @@ -81,6 +82,7 @@ "electron-store": "1.3.0", "electron-updater": "4.0.6", "form-data": "2.3.2", + "inquirer": "6.2.0", "jsdom": "13.2.0", "keytar": "4.2.1", "lowdb": "1.0.0", diff --git a/src/cli/commands/login.command.ts b/src/cli/commands/login.command.ts new file mode 100644 index 0000000000..91d2e294fd --- /dev/null +++ b/src/cli/commands/login.command.ts @@ -0,0 +1,150 @@ +import * as program from 'commander'; +import * as inquirer from 'inquirer'; + +import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; + +import { AuthResult } from '../../models/domain/authResult'; +import { TwoFactorEmailRequest } from '../../models/request/twoFactorEmailRequest'; + +import { ApiService } from '../../abstractions/api.service'; +import { AuthService } from '../../abstractions/auth.service'; +import { I18nService } from '../../abstractions/i18n.service'; + +import { Response } from '../models/response'; + +import { MessageResponse } from '../models/response/messageResponse'; + +export class LoginCommand { + protected validatedParams: () => Promise; + protected success: () => Promise; + + constructor(protected authService: AuthService, protected apiService: ApiService, + protected i18nService: I18nService) { } + + async run(email: string, password: string, cmd: program.Command) { + if (email == null || email === '') { + const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ + type: 'input', + name: 'email', + message: 'Email address:', + }); + email = answer.email; + } + if (email == null || email.trim() === '') { + return Response.badRequest('Email address is required.'); + } + if (email.indexOf('@') === -1) { + return Response.badRequest('Email address is invalid.'); + } + + if (password == null || password === '') { + const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ + type: 'password', + name: 'password', + message: 'Master password:', + }); + password = answer.password; + } + if (password == null || password === '') { + return Response.badRequest('Master password is required.'); + } + + let twoFactorToken: string = cmd.code; + let twoFactorMethod: TwoFactorProviderType = null; + try { + if (cmd.method != null) { + twoFactorMethod = parseInt(cmd.method, null); + } + } catch (e) { + return Response.error('Invalid two-step login method.'); + } + + try { + if (this.validatedParams != null) { + await this.validatedParams(); + } + + let response: AuthResult = null; + if (twoFactorToken != null && twoFactorMethod != null) { + response = await this.authService.logInComplete(email, password, twoFactorMethod, + twoFactorToken, false); + } else { + response = await this.authService.logIn(email, password); + if (response.twoFactor) { + let selectedProvider: any = null; + const twoFactorProviders = this.authService.getSupportedTwoFactorProviders(null); + if (twoFactorProviders.length === 0) { + return Response.badRequest('No providers available for this client.'); + } + + if (twoFactorMethod != null) { + try { + selectedProvider = twoFactorProviders.filter((p) => p.type === twoFactorMethod)[0]; + } catch (e) { + return Response.error('Invalid two-step login method.'); + } + } + + if (selectedProvider == null) { + if (twoFactorProviders.length === 1) { + selectedProvider = twoFactorProviders[0]; + } else { + const options = twoFactorProviders.map((p) => p.name); + options.push(new inquirer.Separator()); + options.push('Cancel'); + const answer: inquirer.Answers = + await inquirer.createPromptModule({ output: process.stderr })({ + type: 'list', + name: 'method', + message: 'Two-step login method:', + choices: options, + }); + const i = options.indexOf(answer.method); + if (i === (options.length - 1)) { + return Response.error('Login failed.'); + } + selectedProvider = twoFactorProviders[i]; + } + } + + if (twoFactorToken == null && response.twoFactorProviders.size > 1 && + selectedProvider.type === TwoFactorProviderType.Email) { + const emailReq = new TwoFactorEmailRequest(this.authService.email, + this.authService.masterPasswordHash); + await this.apiService.postTwoFactorEmail(emailReq); + } + + if (twoFactorToken == null) { + const answer: inquirer.Answers = + await inquirer.createPromptModule({ output: process.stderr })({ + type: 'input', + name: 'token', + message: 'Two-step login code:', + }); + twoFactorToken = answer.token; + if (twoFactorToken == null || twoFactorToken === '') { + return Response.badRequest('Code is required.'); + } + } + + response = await this.authService.logInTwoFactor(selectedProvider.type, + twoFactorToken, false); + } + } + + if (response.twoFactor) { + return Response.error('Login failed.'); + } + + if (this.success != null) { + const res = await this.success(); + return Response.success(res); + } else { + const res = new MessageResponse('You are logged in!', null); + return Response.success(res); + } + } catch (e) { + return Response.error(e); + } + } +} diff --git a/src/cli/commands/logout.command.ts b/src/cli/commands/logout.command.ts new file mode 100644 index 0000000000..34413191e5 --- /dev/null +++ b/src/cli/commands/logout.command.ts @@ -0,0 +1,19 @@ +import * as program from 'commander'; + +import { AuthService } from '../../abstractions/auth.service'; +import { I18nService } from '../../abstractions/i18n.service'; + +import { Response } from '../models/response'; +import { MessageResponse } from '../models/response/messageResponse'; + +export class LogoutCommand { + constructor(private authService: AuthService, private i18nService: I18nService, + private logoutCallback: () => Promise) { } + + async run(cmd: program.Command) { + await this.logoutCallback(); + this.authService.logOut(() => { /* Do nothing */ }); + const res = new MessageResponse('You have logged out.', null); + return Response.success(res); + } +} diff --git a/src/cli/commands/update.command.ts b/src/cli/commands/update.command.ts index 600a6eb2bf..93b539ee82 100644 --- a/src/cli/commands/update.command.ts +++ b/src/cli/commands/update.command.ts @@ -1,6 +1,7 @@ import * as program from 'commander'; import * as fetch from 'node-fetch'; +import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { Response } from '../models/response'; @@ -9,8 +10,8 @@ import { MessageResponse } from '../models/response/messageResponse'; export class UpdateCommand { inPkg: boolean = false; - constructor(private platformUtilsService: PlatformUtilsService, private repoName: string, - private executableName: string) { + constructor(private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, + private repoName: string, private executableName: string, private showExtendedMessage: boolean) { this.inPkg = !!(process as any).pkg; } @@ -68,12 +69,14 @@ export class UpdateCommand { res.message += 'You can download this update at ' + downloadUrl; - if (this.inPkg) { - res.message += '\n\nIf you installed this CLI through a package manager ' + - 'you should probably update using its update command instead.'; - } else { - res.message += '\n\nIf you installed this CLI through NPM ' + - 'you should update using `npm install -g @bitwarden/' + this.repoName + '`'; + if (this.showExtendedMessage) { + if (this.inPkg) { + res.message += '\n\nIf you installed this CLI through a package manager ' + + 'you should probably update using its update command instead.'; + } else { + res.message += '\n\nIf you installed this CLI through NPM ' + + 'you should update using `npm install -g @bitwarden/' + this.repoName + '`'; + } } return Response.success(res); } else { From bc475a668e3088544f551d445ffec9225ae1b73c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 19 Mar 2019 09:07:40 -0400 Subject: [PATCH 0756/1626] always show raw if requested --- src/cli/baseProgram.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/baseProgram.ts b/src/cli/baseProgram.ts index bf33a92b1b..b8b6248d05 100644 --- a/src/cli/baseProgram.ts +++ b/src/cli/baseProgram.ts @@ -69,7 +69,7 @@ export abstract class BaseProgram { protected getMessage(response: Response) { const message = (response.data as MessageResponse); - if (process.env.BW_RAW === 'true' && message.raw != null) { + if (process.env.BW_RAW === 'true') { return message.raw; } From c02d6f3f6c7f63e6426517383b3e0a0ac8a5cb18 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 19 Mar 2019 09:14:07 -0400 Subject: [PATCH 0757/1626] appveyor.yml --- appveyor.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 appveyor.yml diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000000..1bef398056 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,35 @@ +image: +- Visual Studio 2017 + +init: +- ps: | + if($isWindows -and $env:DEBUG_RDP -eq "true") { + iex ((new-object net.webclient).DownloadString(` + 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) + } +- ps: Install-Product node 10 + +install: +- ps: choco install cloc --no-progress +- ps: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git + +before_build: +- cmd: node --version +- cmd: npm --version + +build_script: +- cmd: npm install +- cmd: npm run build +- cmd: npm run test +- cmd: 7z a coverage-%APPVEYOR_BUILD_NUMBER%.zip coverage\* + +on_finish: + - ps: | + if($isWindows -and $env:DEBUG_RDP -eq "true") { + $blockRdp = $true + iex ((new-object net.webclient).DownloadString(` + 'https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) + } + +artifacts: +- path: coverage-%APPVEYOR_BUILD_NUMBER%.zip From d8f9177c03549667cf6d1e5f30536d010acc7b7d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 19 Mar 2019 11:31:53 -0400 Subject: [PATCH 0758/1626] move ciphers paging in jslib for shared use --- package-lock.json | 182 +++++++++++++++++++- package.json | 1 + src/angular/components/ciphers.component.ts | 53 +++++- 3 files changed, 225 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index a0850947d6..55ceac35f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -769,11 +769,27 @@ "babel-runtime": "^6.22.0" } }, + "babel-polyfill": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.23.0.tgz", + "integrity": "sha1-g2TKYt+Or7gwSZ9pkXdGbDsDSZ0=", + "requires": { + "babel-runtime": "^6.22.0", + "core-js": "^2.4.0", + "regenerator-runtime": "^0.10.0" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=" + } + } + }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, "requires": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" @@ -2403,6 +2419,14 @@ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", "dev": true }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "~0.4.13" + } + }, "end-of-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", @@ -3927,7 +3951,6 @@ "version": "0.4.23", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -4255,8 +4278,7 @@ "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, "is-typedarray": { "version": "1.0.0", @@ -5383,6 +5405,14 @@ "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", "dev": true }, + "ngx-infinite-scroll": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-7.0.1.tgz", + "integrity": "sha512-be9DAAuabV7VGI06/JUnS6pXC6mcBOzA4+SBCwOcP9WwJ2r5GjdZyOa34ls9hi1MnCOj3zrXLvPKQ/UDp6csIw==", + "requires": { + "opencollective": "^1.0.3" + } + }, "node-abi": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.5.0.tgz", @@ -6007,6 +6037,143 @@ "mimic-fn": "^1.0.0" } }, + "opencollective": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/opencollective/-/opencollective-1.0.3.tgz", + "integrity": "sha1-ruY3K8KBRFg2kMPKja7PwSDdDvE=", + "requires": { + "babel-polyfill": "6.23.0", + "chalk": "1.1.3", + "inquirer": "3.0.6", + "minimist": "1.2.0", + "node-fetch": "1.6.3", + "opn": "4.0.2" + }, + "dependencies": { + "ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=" + }, + "external-editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "requires": { + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "inquirer": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.0.6.tgz", + "integrity": "sha1-4EqqnQW3o8ubD0B9BDdfBEcZA0c=", + "requires": { + "ansi-escapes": "^1.1.0", + "chalk": "^1.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.0.1", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rx": "^4.1.0", + "string-width": "^2.0.0", + "strip-ansi": "^3.0.0", + "through": "^2.3.6" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "node-fetch": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.6.3.tgz", + "integrity": "sha1-3CNO3WSJmC1Y6PDbT2lQKavNjAQ=", + "requires": { + "encoding": "^0.1.11", + "is-stream": "^1.0.1" + } + }, + "rx": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", + "integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "opn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/opn/-/opn-4.0.2.tgz", + "integrity": "sha1-erwi5kTf9jsKltWrfyeQwPAavJU=", + "requires": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + } + }, "optimist": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", @@ -6282,14 +6449,12 @@ "pinkie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" }, "pinkie-promise": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, "requires": { "pinkie": "^2.0.0" } @@ -6605,8 +6770,7 @@ "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" }, "regex-not": { "version": "1.0.2", diff --git a/package.json b/package.json index 909e8a4d2f..b72bb603b9 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "keytar": "4.2.1", "lowdb": "1.0.0", "lunr": "2.3.3", + "ngx-infinite-scroll": "7.0.1", "node-fetch": "2.2.0", "node-forge": "0.7.6", "papaparse": "4.6.0", diff --git a/src/angular/components/ciphers.component.ts b/src/angular/components/ciphers.component.ts index bd9c07b0d8..b7220f514c 100644 --- a/src/angular/components/ciphers.component.ts +++ b/src/angular/components/ciphers.component.ts @@ -17,13 +17,18 @@ export class CiphersComponent { loaded: boolean = false; ciphers: CipherView[] = []; + pagedCiphers: CipherView[] = []; searchText: string; searchPlaceholder: string = null; filter: (cipher: CipherView) => boolean = null; protected searchPending = false; + protected didScroll = false; + protected pageSize = 100; private searchTimeout: any = null; + private pagedCiphersCount = 0; + private refreshing = false; constructor(protected searchService: SearchService) { } @@ -32,10 +37,35 @@ export class CiphersComponent { this.loaded = true; } - async refresh() { + loadMore() { + if (this.ciphers.length <= this.pageSize) { + return; + } + const pagedLength = this.pagedCiphers.length; + let pagedSize = this.pageSize; + if (this.refreshing && pagedLength === 0 && this.pagedCiphersCount > this.pageSize) { + pagedSize = this.pagedCiphersCount; + } + if (this.ciphers.length > pagedLength) { + this.pagedCiphers = this.pagedCiphers.concat(this.ciphers.slice(pagedLength, pagedLength + pagedSize)); + } + this.pagedCiphersCount = this.pagedCiphers.length; + this.didScroll = this.pagedCiphers.length > this.pageSize; + } + + async reload(filter: (cipher: CipherView) => boolean = null) { this.loaded = false; this.ciphers = []; - await this.load(this.filter); + await this.load(filter); + } + + async refresh() { + try { + this.refreshing = true; + await this.reload(this.filter); + } finally { + this.refreshing = false; + } } async applyFilter(filter: (cipher: CipherView) => boolean = null) { @@ -50,11 +80,13 @@ export class CiphersComponent { } if (timeout == null) { this.ciphers = await this.searchService.searchCiphers(this.searchText, this.filter); + await this.resetPaging(); return; } this.searchPending = true; this.searchTimeout = setTimeout(async () => { this.ciphers = await this.searchService.searchCiphers(this.searchText, this.filter); + await this.resetPaging(); this.searchPending = false; }, timeout); } @@ -74,4 +106,21 @@ export class CiphersComponent { addCipherOptions() { this.onAddCipherOptions.emit(); } + + isSearching() { + return !this.searchPending && this.searchService.isSearchable(this.searchText); + } + + isPaging() { + const searching = this.isSearching(); + if (searching && this.didScroll) { + this.resetPaging(); + } + return !searching && this.ciphers.length > this.pageSize; + } + + async resetPaging() { + this.pagedCiphers = []; + this.loadMore(); + } } From fc1a73c9f2ef2c18f05dfebb5cf80ce84520d118 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 19 Mar 2019 15:44:48 -0400 Subject: [PATCH 0759/1626] check if authed before trying to lock --- src/services/lock.service.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts index 230fd33d10..0c091d9fc0 100644 --- a/src/services/lock.service.ts +++ b/src/services/lock.service.ts @@ -9,6 +9,7 @@ import { MessagingService } from '../abstractions/messaging.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { SearchService } from '../abstractions/search.service'; import { StorageService } from '../abstractions/storage.service'; +import { UserService } from '../abstractions/user.service'; export class LockService implements LockServiceAbstraction { pinLocked = false; @@ -19,7 +20,7 @@ export class LockService implements LockServiceAbstraction { private collectionService: CollectionService, private cryptoService: CryptoService, private platformUtilsService: PlatformUtilsService, private storageService: StorageService, private messagingService: MessagingService, private searchService: SearchService, - private lockedCallback: () => Promise = null) { + private userService: UserService, private lockedCallback: () => Promise = null) { } init(checkOnInterval: boolean) { @@ -48,6 +49,11 @@ export class LockService implements LockServiceAbstraction { return; } + const authed = await this.userService.isAuthenticated(); + if (!authed) { + return; + } + if (await this.isLocked()) { return; } @@ -74,6 +80,11 @@ export class LockService implements LockServiceAbstraction { } async lock(allowSoftLock = false): Promise { + const authed = await this.userService.isAuthenticated(); + if (!authed) { + return; + } + if (allowSoftLock) { const pinSet = await this.isPinLockSet(); if (pinSet[0]) { From 50e6f2467925a3e46ebaadddec88e985b1bfd4fb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 20 Mar 2019 11:44:02 -0400 Subject: [PATCH 0760/1626] update keytar --- package-lock.json | 46 ++++++++++++++++++++++++++-------------------- package.json | 2 +- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 55ceac35f7..3a0063cbf1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2717,9 +2717,9 @@ } }, "expand-template": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-1.1.1.tgz", - "integrity": "sha512-cebqLtV8KOZfw0UI8TEFWxtczxxC1jvyUvx6H4fyp1K1FN7A4Q+uggVUlOsI1K8AGU0rwOGqP8nCapdrw8CYQg==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" }, "extend": { "version": "3.0.2", @@ -4877,12 +4877,12 @@ } }, "keytar": { - "version": "4.2.1", - "resolved": "http://registry.npmjs.org/keytar/-/keytar-4.2.1.tgz", - "integrity": "sha1-igamV3/fY3PgqmsRInfmPex3/RI=", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-4.4.1.tgz", + "integrity": "sha512-6xEe7ybXSR5EZC+z0GI2yqLYZjV1tyPQY2xSZ8rGsBxrrLEh8VR/Lfqv59uGX+I+W+OZxH0jCXN1dU1++ify4g==", "requires": { - "nan": "2.8.0", - "prebuild-install": "^2.4.1" + "nan": "2.12.1", + "prebuild-install": "5.2.4" } }, "kind-of": { @@ -5337,9 +5337,9 @@ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" }, "nan": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", - "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=" + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz", + "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==" }, "nanomatch": { "version": "1.2.13", @@ -5399,6 +5399,11 @@ } } }, + "napi-build-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.1.tgz", + "integrity": "sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA==" + }, "negotiator": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", @@ -5414,9 +5419,9 @@ } }, "node-abi": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.5.0.tgz", - "integrity": "sha512-9g2twBGSP6wIR5PW7tXvAWnEWKJDH/VskdXp168xsw9VVxpEGov8K4jsP4/VeoC7b2ZAyzckvMCuQuQlw44lXg==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.7.1.tgz", + "integrity": "sha512-OV8Bq1OrPh6z+Y4dqwo05HqrRL9YNF7QVMRfq1/pguwKLG+q9UB/Lk0x5qXjO23JjJg+/jqCHSTaG1P3tfKfuw==", "requires": { "semver": "^5.4.1" } @@ -6520,21 +6525,22 @@ "dev": true }, "prebuild-install": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-2.5.3.tgz", - "integrity": "sha512-/rI36cN2g7vDQnKWN8Uzupi++KjyqS9iS+/fpwG4Ea8d0Pip0PQ5bshUNzVwt+/D2MRfhVAplYMMvWLqWrCF/g==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.2.4.tgz", + "integrity": "sha512-CG3JnpTZXdmr92GW4zbcba4jkDha6uHraJ7hW4Fn8j0mExxwOKK20hqho8ZuBDCKYCHYIkFM1P2jhtG+KpP4fg==", "requires": { "detect-libc": "^1.0.3", - "expand-template": "^1.0.2", + "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.0", "mkdirp": "^0.5.1", - "node-abi": "^2.2.0", + "napi-build-utils": "^1.0.1", + "node-abi": "^2.7.0", "noop-logger": "^0.1.1", "npmlog": "^4.0.1", "os-homedir": "^1.0.1", "pump": "^2.0.1", - "rc": "^1.1.6", + "rc": "^1.2.7", "simple-get": "^2.7.0", "tar-fs": "^1.13.0", "tunnel-agent": "^0.6.0", diff --git a/package.json b/package.json index b72bb603b9..2428aa6c8b 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "form-data": "2.3.2", "inquirer": "6.2.0", "jsdom": "13.2.0", - "keytar": "4.2.1", + "keytar": "4.4.1", "lowdb": "1.0.0", "lunr": "2.3.3", "ngx-infinite-scroll": "7.0.1", From cefab5f47f49a40f844b9ce5a23ce7a6eef5251a Mon Sep 17 00:00:00 2001 From: Philipp Rudloff Date: Thu, 21 Mar 2019 14:51:37 +0100 Subject: [PATCH 0761/1626] Fix appFlexCopy improperly trimming newlines and spaces (#31) --- src/angular/directives/flex-copy.directive.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/angular/directives/flex-copy.directive.ts b/src/angular/directives/flex-copy.directive.ts index 5b8636f0ab..e5e700dbba 100644 --- a/src/angular/directives/flex-copy.directive.ts +++ b/src/angular/directives/flex-copy.directive.ts @@ -21,7 +21,13 @@ export class FlexCopyDirective { for (let i = 0; i < selection.rangeCount; i++) { const range = selection.getRangeAt(i); const text = range.toString(); - copyText += text; + + // The selection should only contain one line of text. In some cases however, the + // selection contains newlines and space characters from the identation of following + // sibling nodes. To avoid copying passwords containing trailing newlines and spaces + // that aren’t part of the password, the selection has to be trimmed. + const stringEndPos = text.includes('\n') ? text.search(/\r?\n/) : text.length; + copyText += text.substring(0, stringEndPos); } this.platformUtilsService.copyToClipboard(copyText, { window: window }); } From 7bd0733b88d1a7959c311e5abaf276fea286724c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 21 Mar 2019 09:55:45 -0400 Subject: [PATCH 0762/1626] optimize newLinePos search --- src/angular/directives/flex-copy.directive.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/angular/directives/flex-copy.directive.ts b/src/angular/directives/flex-copy.directive.ts index e5e700dbba..89607fdedc 100644 --- a/src/angular/directives/flex-copy.directive.ts +++ b/src/angular/directives/flex-copy.directive.ts @@ -23,10 +23,14 @@ export class FlexCopyDirective { const text = range.toString(); // The selection should only contain one line of text. In some cases however, the - // selection contains newlines and space characters from the identation of following + // selection contains newlines and space characters from the indentation of following // sibling nodes. To avoid copying passwords containing trailing newlines and spaces - // that aren’t part of the password, the selection has to be trimmed. - const stringEndPos = text.includes('\n') ? text.search(/\r?\n/) : text.length; + // that aren't part of the password, the selection has to be trimmed. + let stringEndPos = text.length; + const newLinePos = text.search(/(?:\r\n|\r|\n)/); + if (newLinePos > -1) { + stringEndPos = newLinePos; + } copyText += text.substring(0, stringEndPos); } this.platformUtilsService.copyToClipboard(copyText, { window: window }); From aebd1b57fc0de80f5a8efbfee28e96b70e93db3d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 21 Mar 2019 10:05:31 -0400 Subject: [PATCH 0763/1626] make sure there isn't more content after newline --- src/angular/directives/flex-copy.directive.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/angular/directives/flex-copy.directive.ts b/src/angular/directives/flex-copy.directive.ts index 89607fdedc..6bdf387f7c 100644 --- a/src/angular/directives/flex-copy.directive.ts +++ b/src/angular/directives/flex-copy.directive.ts @@ -29,7 +29,10 @@ export class FlexCopyDirective { let stringEndPos = text.length; const newLinePos = text.search(/(?:\r\n|\r|\n)/); if (newLinePos > -1) { - stringEndPos = newLinePos; + const otherPart = text.substr(newLinePos).trim(); + if (otherPart === '') { + stringEndPos = newLinePos; + } } copyText += text.substring(0, stringEndPos); } From 593870e9365d68e924955f1ec192a70216b63621 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 21 Mar 2019 21:38:52 -0400 Subject: [PATCH 0764/1626] org upgrade api --- src/abstractions/api.service.ts | 2 ++ src/models/request/organizationUpgradeRequest.ts | 9 +++++++++ src/services/api.service.ts | 5 +++++ 3 files changed, 16 insertions(+) create mode 100644 src/models/request/organizationUpgradeRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 8b20fc0fd5..55a636a624 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -21,6 +21,7 @@ import { KdfRequest } from '../models/request/kdfRequest'; import { KeysRequest } from '../models/request/keysRequest'; import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest'; import { OrganizationUpdateRequest } from '../models/request/organizationUpdateRequest'; +import { OrganizationUpgradeRequest } from '../models/request/organizationUpgradeRequest'; import { OrganizationUserAcceptRequest } from '../models/request/organizationUserAcceptRequest'; import { OrganizationUserConfirmRequest } from '../models/request/organizationUserConfirmRequest'; import { OrganizationUserInviteRequest } from '../models/request/organizationUserInviteRequest'; @@ -238,6 +239,7 @@ export abstract class ApiService { postOrganizationLicenseUpdate: (id: string, data: FormData) => Promise; postOrganizationApiKey: (id: string, request: PasswordVerificationRequest) => Promise; postOrganizationRotateApiKey: (id: string, request: PasswordVerificationRequest) => Promise; + postOrganizationUpgrade: (id: string, request: OrganizationUpgradeRequest) => Promise; postOrganizationSeat: (id: string, request: SeatRequest) => Promise; postOrganizationStorage: (id: string, request: StorageRequest) => Promise; postOrganizationPayment: (id: string, request: PaymentRequest) => Promise; diff --git a/src/models/request/organizationUpgradeRequest.ts b/src/models/request/organizationUpgradeRequest.ts new file mode 100644 index 0000000000..7f8793b2d1 --- /dev/null +++ b/src/models/request/organizationUpgradeRequest.ts @@ -0,0 +1,9 @@ +import { PlanType } from '../../enums/planType'; + +export class OrganizationUpgradeRequest { + businessName: string; + planType: PlanType; + additionalSeats: number; + additionalStorageGb: number; + premiumAccessAddon: boolean; +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 36622409a9..8b4acd2bd9 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -27,6 +27,7 @@ import { KdfRequest } from '../models/request/kdfRequest'; import { KeysRequest } from '../models/request/keysRequest'; import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest'; import { OrganizationUpdateRequest } from '../models/request/organizationUpdateRequest'; +import { OrganizationUpgradeRequest } from '../models/request/organizationUpgradeRequest'; import { OrganizationUserAcceptRequest } from '../models/request/organizationUserAcceptRequest'; import { OrganizationUserConfirmRequest } from '../models/request/organizationUserConfirmRequest'; import { OrganizationUserInviteRequest } from '../models/request/organizationUserInviteRequest'; @@ -778,6 +779,10 @@ export class ApiService implements ApiServiceAbstraction { return new ApiKeyResponse(r); } + postOrganizationUpgrade(id: string, request: OrganizationUpgradeRequest): Promise { + return this.send('POST', '/organizations/' + id + '/upgrade', request, true, false); + } + postOrganizationSeat(id: string, request: SeatRequest): Promise { return this.send('POST', '/organizations/' + id + '/seat', request, true, false); } From f874ec253d2e0f7f305149874fe54aa2b5fa0bd6 Mon Sep 17 00:00:00 2001 From: Robert Wachs Date: Sat, 23 Mar 2019 17:27:50 +0100 Subject: [PATCH 0765/1626] 1password 1pif importer: create hidden fields (#32) * allow base importer to receive custom field type * 1password importer uses hidden field type for custom fields marked as 'concealed' * 1password 1pif importer specs * remove 'focus' from specs * change field type logic into simple one liner --- .../importers/onepassword1PifImporter.spec.ts | 95 +++++++++++++++++++ src/importers/baseImporter.ts | 4 +- src/importers/onepassword1PifImporter.ts | 4 +- 3 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 spec/common/importers/onepassword1PifImporter.spec.ts diff --git a/spec/common/importers/onepassword1PifImporter.spec.ts b/spec/common/importers/onepassword1PifImporter.spec.ts new file mode 100644 index 0000000000..c75373c3d8 --- /dev/null +++ b/spec/common/importers/onepassword1PifImporter.spec.ts @@ -0,0 +1,95 @@ +import { FieldType } from '../../../src/enums/fieldType'; +import { OnePassword1PifImporter as Importer } from '../../../src/importers/onepassword1PifImporter'; + +import { Utils } from '../../../src/misc/utils'; + +if (Utils.isNode) { + // Polyfills + // tslint:disable-next-line + const jsdom: any = require('jsdom'); + (global as any).DOMParser = new jsdom.JSDOM().window.DOMParser; +} + +const TestData: string = `***aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee***\n` + + JSON.stringify({ + uuid: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + updatedAt: 1486071244, + securityLevel: 'SL5', + contentsHash: 'aaaaaaaa', + title: 'Imported Entry', + location: 'https://www.google.com', + secureContents: { + fields: [ + { + value: 'user@test.net', + id: 'email-input', + name: 'email', + type: 'T', + designation: 'username', + }, + { + value: 'myservicepassword', + id: 'password-input', + name: 'password', + type: 'P', + designation: 'password', + }, + ], + sections: [ + { + fields: [ + { + k: 'concealed', + n: 'AAAAAAAAAAAABBBBBBBBBBBCCCCCCCCC', + v: 'console-password-123', + t: 'console password', + }, + ], + title: 'Admin Console', + name: 'admin_console', + }, + ], + }, + URLs: [ + { + label: 'website', + url: 'https://www.google.com', + }, + ], + txTimestamp: 1508941334, + createdAt: 1390426636, + typeName: 'webforms.WebForm', +}); + +describe('1Password 1Pif Importer', () => { + it('should parse data', async () => { + const importer = new Importer(); + const result = importer.parse(TestData); + expect(result != null).toBe(true); + + const cipher = result.ciphers.shift(); + expect(cipher.login.username).toEqual('user@test.net'); + expect(cipher.login.password).toEqual('myservicepassword'); + expect(cipher.login.uris.length).toEqual(1); + const uriView = cipher.login.uris.shift(); + expect(uriView.uri).toEqual('https://www.google.com'); + }); + + it('should create concealed field as "hidden" type', async () => { + const importer = new Importer(); + const result = importer.parse(TestData); + expect(result != null).toBe(true); + + const ciphers = result.ciphers; + expect(ciphers.length).toEqual(1); + + const cipher = ciphers.shift(); + const fields = cipher.fields; + expect(fields.length).toEqual(1); + + const field = fields.shift(); + expect(field.name).toEqual('console password'); + expect(field.value).toEqual('console-password-123'); + expect(field.type).toEqual(FieldType.Hidden); + }); +}); diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index a1196b587c..fcb33e796c 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -302,7 +302,7 @@ export abstract class BaseImporter { } } - protected processKvp(cipher: CipherView, key: string, value: string) { + protected processKvp(cipher: CipherView, key: string, value: string, type: FieldType = FieldType.Text) { if (this.isNullOrWhitespace(value)) { return; } @@ -319,7 +319,7 @@ export abstract class BaseImporter { cipher.fields = []; } const field = new FieldView(); - field.type = FieldType.Text; + field.type = type; field.name = key; field.value = value; cipher.fields.push(field); diff --git a/src/importers/onepassword1PifImporter.ts b/src/importers/onepassword1PifImporter.ts index eb004fd78d..021c10a1c0 100644 --- a/src/importers/onepassword1PifImporter.ts +++ b/src/importers/onepassword1PifImporter.ts @@ -8,6 +8,7 @@ import { CipherView } from '../models/view/cipherView'; import { SecureNoteView } from '../models/view/secureNoteView'; import { CipherType } from '../enums/cipherType'; +import { FieldType } from '../enums/fieldType'; import { SecureNoteType } from '../enums/secureNoteType'; export class OnePassword1PifImporter extends BaseImporter implements Importer { @@ -168,8 +169,9 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { } } + const fieldType = (field.k === 'concealed') ? FieldType.Hidden : FieldType.Text; const fieldName = this.isNullOrWhitespace(field[nameKey]) ? 'no_name' : field[nameKey]; - this.processKvp(cipher, fieldName, fieldValue); + this.processKvp(cipher, fieldName, fieldValue, fieldType); }); } } From 6f202f06373c75cab196aa5110191f00d1d1bef6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 23 Mar 2019 12:29:19 -0400 Subject: [PATCH 0766/1626] parens not needed --- src/importers/onepassword1PifImporter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/importers/onepassword1PifImporter.ts b/src/importers/onepassword1PifImporter.ts index 021c10a1c0..6c2b4e73a4 100644 --- a/src/importers/onepassword1PifImporter.ts +++ b/src/importers/onepassword1PifImporter.ts @@ -169,7 +169,7 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { } } - const fieldType = (field.k === 'concealed') ? FieldType.Hidden : FieldType.Text; + const fieldType = field.k === 'concealed' ? FieldType.Hidden : FieldType.Text; const fieldName = this.isNullOrWhitespace(field[nameKey]) ? 'no_name' : field[nameKey]; this.processKvp(cipher, fieldName, fieldValue, fieldType); }); From c17e8b458cb64e767559b24c9c69a7776bcb0ea4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 23 Mar 2019 12:31:52 -0400 Subject: [PATCH 0767/1626] use single quote --- spec/common/importers/onepassword1PifImporter.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/common/importers/onepassword1PifImporter.spec.ts b/spec/common/importers/onepassword1PifImporter.spec.ts index c75373c3d8..724be90c5e 100644 --- a/spec/common/importers/onepassword1PifImporter.spec.ts +++ b/spec/common/importers/onepassword1PifImporter.spec.ts @@ -10,7 +10,7 @@ if (Utils.isNode) { (global as any).DOMParser = new jsdom.JSDOM().window.DOMParser; } -const TestData: string = `***aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee***\n` + +const TestData: string = '***aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee***\n' + JSON.stringify({ uuid: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', updatedAt: 1486071244, From 2bd47a19dfbfeb3a66cc3b24bb285725e90335c0 Mon Sep 17 00:00:00 2001 From: Robert Wachs Date: Sun, 24 Mar 2019 03:21:43 +0100 Subject: [PATCH 0768/1626] 1password 1pif importer: create identity records (#34) * 1password 1pif importer: create identity records * importer: do not store empty strings replace them with null instead --- .../importers/onepassword1PifImporter.spec.ts | 301 ++++++++++++++++++ src/importers/onepassword1PifImporter.ts | 34 ++ 2 files changed, 335 insertions(+) diff --git a/spec/common/importers/onepassword1PifImporter.spec.ts b/spec/common/importers/onepassword1PifImporter.spec.ts index 724be90c5e..1a2d5d7a6f 100644 --- a/spec/common/importers/onepassword1PifImporter.spec.ts +++ b/spec/common/importers/onepassword1PifImporter.spec.ts @@ -61,6 +61,284 @@ const TestData: string = '***aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee***\n' + typeName: 'webforms.WebForm', }); +const IdentityTestData = JSON.stringify({ + uuid: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + updatedAt: 1553365894, + securityLevel: 'SL5', + contentsHash: 'eeeeeeee', + title: 'Test Identity', + secureContents: { + lastname: 'Fritzenberger', + zip: '223344', + birthdate_dd: '11', + homephone: '+49 333 222 111', + company: 'Web Inc.', + firstname: 'Frank', + birthdate_mm: '3', + country: 'de', + sex: 'male', + sections: [ + { + fields: [ + { + k: 'string', + inputTraits: { + autocapitalization: 'Words', + }, + n: 'firstname', + v: 'Frank', + a: { + guarded: 'yes', + }, + t: 'first name', + }, + { + k: 'string', + inputTraits: { + autocapitalization: 'Words', + }, + n: 'initial', + v: 'MD', + a: { + guarded: 'yes', + }, + t: 'initial', + }, + { + k: 'string', + inputTraits: { + autocapitalization: 'Words', + }, + n: 'lastname', + v: 'Fritzenberger', + a: { + guarded: 'yes', + }, + t: 'last name', + }, + { + k: 'menu', + v: 'male', + n: 'sex', + a: { + guarded: 'yes', + }, + t: 'sex', + }, + { + k: 'date', + v: 1552305660, + n: 'birthdate', + a: { + guarded: 'yes', + }, + t: 'birth date', + }, + { + k: 'string', + inputTraits: { + autocapitalization: 'Words', + }, + n: 'occupation', + v: 'Engineer', + a: { + guarded: 'yes', + }, + t: 'occupation', + }, + { + k: 'string', + inputTraits: { + autocapitalization: 'Words', + }, + n: 'company', + v: 'Web Inc.', + a: { + guarded: 'yes', + }, + t: 'company', + }, + { + k: 'string', + inputTraits: { + autocapitalization: 'Words', + }, + n: 'department', + v: 'IT', + a: { + guarded: 'yes', + }, + t: 'department', + }, + { + k: 'string', + inputTraits: { + autocapitalization: 'Words', + }, + n: 'jobtitle', + v: 'Developer', + a: { + guarded: 'yes', + }, + t: 'job title', + }, + ], + title: 'Identification', + name: 'name', + }, + { + fields: [ + { + k: 'address', + inputTraits: { + autocapitalization: 'Sentences', + }, + n: 'address', + v: { + street: 'Mainstreet 1', + city: 'Berlin', + country: 'de', + zip: '223344', + }, + a: { + guarded: 'yes', + }, + t: 'address', + }, + { + k: 'phone', + v: '+49 001 222 333 44', + n: 'defphone', + a: { + guarded: 'yes', + }, + t: 'default phone', + }, + { + k: 'phone', + v: '+49 333 222 111', + n: 'homephone', + a: { + guarded: 'yes', + }, + t: 'home', + }, + { + k: 'phone', + n: 'cellphone', + a: { + guarded: 'yes', + }, + t: 'mobile', + }, + { + k: 'phone', + n: 'busphone', + a: { + guarded: 'yes', + }, + t: 'business', + }, + ], + title: 'Address', + name: 'address', + }, + { + fields: [ + { + k: 'string', + n: 'username', + a: { + guarded: 'yes', + }, + t: 'username', + }, + { + k: 'string', + n: 'reminderq', + t: 'reminder question', + }, + { + k: 'string', + n: 'remindera', + t: 'reminder answer', + }, + { + k: 'string', + inputTraits: { + keyboard: 'EmailAddress', + }, + n: 'email', + v: 'test@web.de', + a: { + guarded: 'yes', + }, + t: 'email', + }, + { + k: 'string', + n: 'website', + inputTraits: { + keyboard: 'URL', + }, + t: 'website', + }, + { + k: 'string', + n: 'icq', + t: 'ICQ', + }, + { + k: 'string', + n: 'skype', + t: 'skype', + }, + { + k: 'string', + n: 'aim', + t: 'AOL/AIM', + }, + { + k: 'string', + n: 'yahoo', + t: 'Yahoo', + }, + { + k: 'string', + n: 'msn', + t: 'MSN', + }, + { + k: 'string', + n: 'forumsig', + t: 'forum signature', + }, + ], + title: 'Internet Details', + name: 'internet', + }, + { + title: 'Related Items', + name: 'linked items', + }, + ], + initial: 'MD', + address1: 'Mainstreet 1', + city: 'Berlin', + jobtitle: 'Developer', + occupation: 'Engineer', + department: 'IT', + email: 'test@web.de', + birthdate_yy: '2019', + homephone_local: '+49 333 222 111', + defphone_local: '+49 001 222 333 44', + defphone: '+49 001 222 333 44', + }, + txTimestamp: 1553365894, + createdAt: 1553364679, + typeName: 'identities.Identity', +}); + describe('1Password 1Pif Importer', () => { it('should parse data', async () => { const importer = new Importer(); @@ -92,4 +370,27 @@ describe('1Password 1Pif Importer', () => { expect(field.value).toEqual('console-password-123'); expect(field.type).toEqual(FieldType.Hidden); }); + + it('should create identity records', async () => { + const importer = new Importer(); + const result = importer.parse(IdentityTestData); + expect(result != null).toBe(true); + const cipher = result.ciphers.shift(); + expect(cipher.name).toEqual('Test Identity'); + + const identity = cipher.identity; + expect(identity.firstName).toEqual('Frank'); + expect(identity.middleName).toEqual('MD'); + expect(identity.lastName).toEqual('Fritzenberger'); + expect(identity.company).toEqual('Web Inc.'); + expect(identity.address1).toEqual('Mainstreet 1'); + expect(identity.country).toEqual('de'); + expect(identity.city).toEqual('Berlin'); + expect(identity.postalCode).toEqual('223344'); + expect(identity.phone).toEqual('+49 001 222 333 44'); + expect(identity.email).toEqual('test@web.de'); + + // remaining fields as custom fields + expect(cipher.fields.length).toEqual(6); + }); }); diff --git a/src/importers/onepassword1PifImporter.ts b/src/importers/onepassword1PifImporter.ts index 6c2b4e73a4..e7ed3041e1 100644 --- a/src/importers/onepassword1PifImporter.ts +++ b/src/importers/onepassword1PifImporter.ts @@ -5,6 +5,7 @@ import { ImportResult } from '../models/domain/importResult'; import { CardView } from '../models/view/cardView'; import { CipherView } from '../models/view/cipherView'; +import { IdentityView } from '../models/view/identityView'; import { SecureNoteView } from '../models/view/secureNoteView'; import { CipherType } from '../enums/cipherType'; @@ -86,6 +87,9 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { } else if (item.typeName === 'wallet.financial.CreditCard') { cipher.type = CipherType.Card; cipher.card = new CardView(); + } else if (item.typeName === 'identities.Identity') { + cipher.type = CipherType.Identity; + cipher.identity = new IdentityView(); } else { cipher.login.uris = this.makeUriArray(item.location); } @@ -167,6 +171,36 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { // Skip since brand was determined from number above return; } + } else if (cipher.type === CipherType.Identity) { + const identity = cipher.identity; + + if (this.isNullOrWhitespace(identity.firstName) && fieldDesignation === 'firstname') { + identity.firstName = fieldValue; + return; + } else if (this.isNullOrWhitespace(identity.lastName) && fieldDesignation === 'lastname') { + identity.lastName = fieldValue; + return; + } else if (this.isNullOrWhitespace(identity.middleName) && fieldDesignation === 'initial') { + identity.middleName = fieldValue; + return; + } else if (this.isNullOrWhitespace(identity.phone) && fieldDesignation === 'defphone') { + identity.phone = fieldValue; + return; + } else if (this.isNullOrWhitespace(identity.company) && fieldDesignation === 'company') { + identity.company = fieldValue; + return; + } else if (this.isNullOrWhitespace(identity.email) && fieldDesignation === 'email') { + identity.email = fieldValue; + return; + } else if (fieldDesignation === 'address') { + // fieldValue is an object casted into a string, so access the plain value instead + const { street, city, country, zip } = field[valueKey]; + identity.address1 = this.getValueOrDefault(street); + identity.city = this.getValueOrDefault(city); + identity.country = this.getValueOrDefault(country); // lower case iso code + identity.postalCode = this.getValueOrDefault(zip); + return; + } } const fieldType = field.k === 'concealed' ? FieldType.Hidden : FieldType.Text; From 38795dc95b8be4e25397fdcf9ffc320e40db449a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 23 Mar 2019 22:45:37 -0400 Subject: [PATCH 0769/1626] support for opvault identity. add username --- src/importers/onepassword1PifImporter.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/importers/onepassword1PifImporter.ts b/src/importers/onepassword1PifImporter.ts index e7ed3041e1..c607f5a709 100644 --- a/src/importers/onepassword1PifImporter.ts +++ b/src/importers/onepassword1PifImporter.ts @@ -56,6 +56,10 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { if (!this.isNullOrWhitespace(item.details.ccnum) || !this.isNullOrWhitespace(item.details.cvv)) { cipher.type = CipherType.Card; cipher.card = new CardView(); + } else if (!this.isNullOrWhitespace(item.details.firstname) || + !this.isNullOrWhitespace(item.details.address1)) { + cipher.type = CipherType.Identity; + cipher.identity = new IdentityView(); } if (cipher.type === CipherType.Login && !this.isNullOrWhitespace(item.details.password)) { cipher.login.password = item.details.password; @@ -173,7 +177,6 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { } } else if (cipher.type === CipherType.Identity) { const identity = cipher.identity; - if (this.isNullOrWhitespace(identity.firstName) && fieldDesignation === 'firstname') { identity.firstName = fieldValue; return; @@ -192,12 +195,17 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { } else if (this.isNullOrWhitespace(identity.email) && fieldDesignation === 'email') { identity.email = fieldValue; return; + } else if (this.isNullOrWhitespace(identity.username) && fieldDesignation === 'username') { + identity.username = fieldValue; + return; } else if (fieldDesignation === 'address') { // fieldValue is an object casted into a string, so access the plain value instead const { street, city, country, zip } = field[valueKey]; identity.address1 = this.getValueOrDefault(street); identity.city = this.getValueOrDefault(city); - identity.country = this.getValueOrDefault(country); // lower case iso code + if (!this.isNullOrWhitespace(country)) { + identity.country = country.toUpperCase(); + } identity.postalCode = this.getValueOrDefault(zip); return; } From df429fe178f1d7920feb2616996eda6db25eb60b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 23 Mar 2019 22:49:22 -0400 Subject: [PATCH 0770/1626] country is now upper --- spec/common/importers/onepassword1PifImporter.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/common/importers/onepassword1PifImporter.spec.ts b/spec/common/importers/onepassword1PifImporter.spec.ts index 1a2d5d7a6f..6584ac3769 100644 --- a/spec/common/importers/onepassword1PifImporter.spec.ts +++ b/spec/common/importers/onepassword1PifImporter.spec.ts @@ -384,7 +384,7 @@ describe('1Password 1Pif Importer', () => { expect(identity.lastName).toEqual('Fritzenberger'); expect(identity.company).toEqual('Web Inc.'); expect(identity.address1).toEqual('Mainstreet 1'); - expect(identity.country).toEqual('de'); + expect(identity.country).toEqual('DE'); expect(identity.city).toEqual('Berlin'); expect(identity.postalCode).toEqual('223344'); expect(identity.phone).toEqual('+49 001 222 333 44'); From 8ed27eeeeca4250a984d8c8fe6f70744bf1f0da6 Mon Sep 17 00:00:00 2001 From: Robert Wachs Date: Sun, 24 Mar 2019 15:50:49 +0100 Subject: [PATCH 0771/1626] 1password 1pif: import password history (#33) * 1password 1pif import password history * 1password 1pif importer: process windows password history * linter fix --- .../importers/onepassword1PifImporter.spec.ts | 102 ++++++++++++++++++ spec/common/misc/throttle.spec.ts | 2 +- src/importers/onepassword1PifImporter.ts | 17 +++ 3 files changed, 120 insertions(+), 1 deletion(-) diff --git a/spec/common/importers/onepassword1PifImporter.spec.ts b/spec/common/importers/onepassword1PifImporter.spec.ts index 6584ac3769..971e82e6a2 100644 --- a/spec/common/importers/onepassword1PifImporter.spec.ts +++ b/spec/common/importers/onepassword1PifImporter.spec.ts @@ -49,6 +49,12 @@ const TestData: string = '***aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee***\n' + name: 'admin_console', }, ], + passwordHistory: [ + { + value: 'old-password', + time: 1447791421, + }, + ], }, URLs: [ { @@ -61,6 +67,76 @@ const TestData: string = '***aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee***\n' + typeName: 'webforms.WebForm', }); +const WindowsTestData = JSON.stringify({ + category: '001', + created: 1544823719, + hmac: 'NtyBmTTPOb88HV3JUKPx1xl/vcMhac9kvCfe/NtszY0=', + k: 'XC/z20QveYCoV8xQ4tCJZZp/uum77xLkMSMEhlUULndryXgSmkG+VBtkW7AfuerfKc8Rtu43a4Sd078j7XfZTcwUCEKtBECUTDNbEgv4+4hoFVk1EzZgEUy/0bW1Ap+jNLmmdSU9h74+REu6pdxsvQ==', + tx: 1553395669, + updated: 1553395669, + uuid: '528AB076FB5F4FBF960884B8E01619AC', + overview: { + title: 'Google', + URLs: [ + { + u: 'google.com', + }, + ], + url: 'google.com', + ps: 26, + ainfo: 'googluser', + }, + details: { + passwordHistory: [ + { + value: 'oldpass2', + time: 1553394449, + }, + { + value: 'oldpass1', + time: 1553394457, + }, + ], + fields: [ + { + type: 'T', + id: 'username', + name: 'username', + value: 'googluser', + designation: 'username', + }, + { + type: 'P', + id: 'password', + name: 'password', + value: '12345678901', + designation: 'password', + }, + ], + notesPlain: 'This is a note\r\n\r\nline1\r\nline2', + sections: [ + { + title: 'test', + name: '1214FD88CD30405D9EED14BEB4D61B60', + fields: [ + { + k: 'string', + n: '6CC3BD77482D4559A4B8BB2D360F821B', + v: 'fgfg', + t: 'fgggf', + }, + { + k: 'concealed', + n: '5CFE7BCAA1DF4578BBF7EB508959BFF3', + v: 'dfgdfgfdg', + t: 'pwfield', + }, + ], + }, + ], + }, +}); + const IdentityTestData = JSON.stringify({ uuid: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', updatedAt: 1553365894, @@ -393,4 +469,30 @@ describe('1Password 1Pif Importer', () => { // remaining fields as custom fields expect(cipher.fields.length).toEqual(6); }); + + it('should create password history', async () => { + const importer = new Importer(); + const result = importer.parse(TestData); + const cipher = result.ciphers.shift(); + + expect(cipher.passwordHistory.length).toEqual(1); + const ph = cipher.passwordHistory.shift(); + expect(ph.password).toEqual('old-password'); + expect(ph.lastUsedDate.toISOString()).toEqual('2015-11-17T20:17:01.000Z'); + }); + + it('should create password history from windows 1pif format', async () => { + const importer = new Importer(); + const result = importer.parse(WindowsTestData); + const cipher = result.ciphers.shift(); + + expect(cipher.passwordHistory.length).toEqual(2); + let ph = cipher.passwordHistory.shift(); + expect(ph.password).toEqual('oldpass2'); + expect(ph.lastUsedDate.toISOString()).toEqual('2019-03-24T02:27:29.000Z'); + + ph = cipher.passwordHistory.shift(); + expect(ph.password).toEqual('oldpass1'); + expect(ph.lastUsedDate.toISOString()).toEqual('2019-03-24T02:27:37.000Z'); + }); }); diff --git a/spec/common/misc/throttle.spec.ts b/spec/common/misc/throttle.spec.ts index 3e5a988341..f7a273a6f9 100644 --- a/spec/common/misc/throttle.spec.ts +++ b/spec/common/misc/throttle.spec.ts @@ -1,5 +1,5 @@ -import { throttle } from '../../../src/misc/throttle'; import { sequentialize } from '../../../src/misc/sequentialize'; +import { throttle } from '../../../src/misc/throttle'; describe('throttle decorator', () => { it('should call the function once at a time', async () => { diff --git a/src/importers/onepassword1PifImporter.ts b/src/importers/onepassword1PifImporter.ts index c607f5a709..ced4c9104d 100644 --- a/src/importers/onepassword1PifImporter.ts +++ b/src/importers/onepassword1PifImporter.ts @@ -6,6 +6,7 @@ import { ImportResult } from '../models/domain/importResult'; import { CardView } from '../models/view/cardView'; import { CipherView } from '../models/view/cipherView'; import { IdentityView } from '../models/view/identityView'; +import { PasswordHistoryView } from '../models/view/passwordHistoryView'; import { SecureNoteView } from '../models/view/secureNoteView'; import { CipherType } from '../enums/cipherType'; @@ -77,9 +78,22 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { } }); } + if (item.details.passwordHistory != null) { + this.processPasswordHistory(item.details.passwordHistory, cipher); + } } } + private processPasswordHistory(items: any[], cipher: CipherView) { + cipher.passwordHistory = cipher.passwordHistory || []; + items.forEach((entry: any) => { + const phv = new PasswordHistoryView(); + phv.password = entry.value; + phv.lastUsedDate = new Date(entry.time * 1000); + cipher.passwordHistory.push(phv); + }); + } + private processStandardItem(item: any, cipher: CipherView) { cipher.favorite = item.openContents && item.openContents.faveIndex ? true : false; cipher.name = this.getValueOrDefault(item.title); @@ -128,6 +142,9 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { } }); } + if (item.secureContents.passwordHistory != null) { + this.processPasswordHistory(item.secureContents.passwordHistory, cipher); + } } } From 58c34b896c214cc599ecb106adce6aee8a400802 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 25 Mar 2019 09:10:33 -0400 Subject: [PATCH 0772/1626] sort and limit password history parsing --- .../importers/onepassword1PifImporter.spec.ts | 150 ++++++++++-------- src/importers/onepassword1PifImporter.ts | 24 +-- 2 files changed, 99 insertions(+), 75 deletions(-) diff --git a/spec/common/importers/onepassword1PifImporter.spec.ts b/spec/common/importers/onepassword1PifImporter.spec.ts index 971e82e6a2..352c3cb651 100644 --- a/spec/common/importers/onepassword1PifImporter.spec.ts +++ b/spec/common/importers/onepassword1PifImporter.spec.ts @@ -12,62 +12,62 @@ if (Utils.isNode) { const TestData: string = '***aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee***\n' + JSON.stringify({ - uuid: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - updatedAt: 1486071244, - securityLevel: 'SL5', - contentsHash: 'aaaaaaaa', - title: 'Imported Entry', - location: 'https://www.google.com', - secureContents: { - fields: [ - { - value: 'user@test.net', - id: 'email-input', - name: 'email', - type: 'T', - designation: 'username', - }, - { - value: 'myservicepassword', - id: 'password-input', - name: 'password', - type: 'P', - designation: 'password', - }, - ], - sections: [ - { - fields: [ - { - k: 'concealed', - n: 'AAAAAAAAAAAABBBBBBBBBBBCCCCCCCCC', - v: 'console-password-123', - t: 'console password', - }, - ], - title: 'Admin Console', - name: 'admin_console', - }, - ], - passwordHistory: [ - { - value: 'old-password', - time: 1447791421, - }, - ], - }, - URLs: [ - { - label: 'website', - url: 'https://www.google.com', + uuid: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + updatedAt: 1486071244, + securityLevel: 'SL5', + contentsHash: 'aaaaaaaa', + title: 'Imported Entry', + location: 'https://www.google.com', + secureContents: { + fields: [ + { + value: 'user@test.net', + id: 'email-input', + name: 'email', + type: 'T', + designation: 'username', + }, + { + value: 'myservicepassword', + id: 'password-input', + name: 'password', + type: 'P', + designation: 'password', + }, + ], + sections: [ + { + fields: [ + { + k: 'concealed', + n: 'AAAAAAAAAAAABBBBBBBBBBBCCCCCCCCC', + v: 'console-password-123', + t: 'console password', + }, + ], + title: 'Admin Console', + name: 'admin_console', + }, + ], + passwordHistory: [ + { + value: 'old-password', + time: 1447791421, + }, + ], }, - ], - txTimestamp: 1508941334, - createdAt: 1390426636, - typeName: 'webforms.WebForm', -}); + URLs: [ + { + label: 'website', + url: 'https://www.google.com', + }, + ], + txTimestamp: 1508941334, + createdAt: 1390426636, + typeName: 'webforms.WebForm', + }); -const WindowsTestData = JSON.stringify({ +const WindowsOpVaultTestData = JSON.stringify({ category: '001', created: 1544823719, hmac: 'NtyBmTTPOb88HV3JUKPx1xl/vcMhac9kvCfe/NtszY0=', @@ -89,13 +89,29 @@ const WindowsTestData = JSON.stringify({ details: { passwordHistory: [ { - value: 'oldpass2', + value: 'oldpass1', time: 1553394449, }, { - value: 'oldpass1', + value: 'oldpass2', time: 1553394457, }, + { + value: 'oldpass3', + time: 1553394458, + }, + { + value: 'oldpass4', + time: 1553394459, + }, + { + value: 'oldpass5', + time: 1553394460, + }, + { + value: 'oldpass6', + time: 1553394461, + }, ], fields: [ { @@ -481,18 +497,26 @@ describe('1Password 1Pif Importer', () => { expect(ph.lastUsedDate.toISOString()).toEqual('2015-11-17T20:17:01.000Z'); }); - it('should create password history from windows 1pif format', async () => { + it('should create password history from windows opvault 1pif format', async () => { const importer = new Importer(); - const result = importer.parse(WindowsTestData); + const result = importer.parse(WindowsOpVaultTestData); const cipher = result.ciphers.shift(); - expect(cipher.passwordHistory.length).toEqual(2); + expect(cipher.passwordHistory.length).toEqual(5); let ph = cipher.passwordHistory.shift(); - expect(ph.password).toEqual('oldpass2'); - expect(ph.lastUsedDate.toISOString()).toEqual('2019-03-24T02:27:29.000Z'); - + expect(ph.password).toEqual('oldpass6'); + expect(ph.lastUsedDate.toISOString()).toEqual('2019-03-24T02:27:41.000Z'); ph = cipher.passwordHistory.shift(); - expect(ph.password).toEqual('oldpass1'); + expect(ph.password).toEqual('oldpass5'); + expect(ph.lastUsedDate.toISOString()).toEqual('2019-03-24T02:27:40.000Z'); + ph = cipher.passwordHistory.shift(); + expect(ph.password).toEqual('oldpass4'); + expect(ph.lastUsedDate.toISOString()).toEqual('2019-03-24T02:27:39.000Z'); + ph = cipher.passwordHistory.shift(); + expect(ph.password).toEqual('oldpass3'); + expect(ph.lastUsedDate.toISOString()).toEqual('2019-03-24T02:27:38.000Z'); + ph = cipher.passwordHistory.shift(); + expect(ph.password).toEqual('oldpass2'); expect(ph.lastUsedDate.toISOString()).toEqual('2019-03-24T02:27:37.000Z'); }); }); diff --git a/src/importers/onepassword1PifImporter.ts b/src/importers/onepassword1PifImporter.ts index ced4c9104d..eb2d4d532e 100644 --- a/src/importers/onepassword1PifImporter.ts +++ b/src/importers/onepassword1PifImporter.ts @@ -79,21 +79,11 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { }); } if (item.details.passwordHistory != null) { - this.processPasswordHistory(item.details.passwordHistory, cipher); + this.parsePasswordHistory(item.details.passwordHistory, cipher); } } } - private processPasswordHistory(items: any[], cipher: CipherView) { - cipher.passwordHistory = cipher.passwordHistory || []; - items.forEach((entry: any) => { - const phv = new PasswordHistoryView(); - phv.password = entry.value; - phv.lastUsedDate = new Date(entry.time * 1000); - cipher.passwordHistory.push(phv); - }); - } - private processStandardItem(item: any, cipher: CipherView) { cipher.favorite = item.openContents && item.openContents.faveIndex ? true : false; cipher.name = this.getValueOrDefault(item.title); @@ -143,11 +133,21 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { }); } if (item.secureContents.passwordHistory != null) { - this.processPasswordHistory(item.secureContents.passwordHistory, cipher); + this.parsePasswordHistory(item.secureContents.passwordHistory, cipher); } } } + private parsePasswordHistory(items: any[], cipher: CipherView) { + const maxSize = items.length > 5 ? 5 : items.length; + cipher.passwordHistory = items.sort((a, b) => b.time - a.time).slice(0, maxSize).map((entry: any) => { + const ph = new PasswordHistoryView(); + ph.password = entry.value; + ph.lastUsedDate = new Date(entry.time * 1000); + return ph; + }); + } + private parseFields(fields: any[], cipher: CipherView, designationKey: string, valueKey: string, nameKey: string) { fields.forEach((field: any) => { if (field[valueKey] == null || field[valueKey].toString().trim() === '') { From a884f779381d0958ce31a99c2cd9e62cd9fba521 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 25 Mar 2019 09:15:38 -0400 Subject: [PATCH 0773/1626] limit ph import to 5 max --- src/importers/bitwardenJsonImporter.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/importers/bitwardenJsonImporter.ts b/src/importers/bitwardenJsonImporter.ts index e4c376940e..1e94ad94db 100644 --- a/src/importers/bitwardenJsonImporter.ts +++ b/src/importers/bitwardenJsonImporter.ts @@ -46,6 +46,11 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { cipher.organizationId = null; cipher.collectionIds = null; + // make sure password history is limited + if (cipher.passwordHistory != null && cipher.passwordHistory.length > 5) { + cipher.passwordHistory = cipher.passwordHistory.slice(0, 5); + } + if (!this.organization && c.folderId != null && groupingsMap.has(c.folderId)) { result.folderRelationships.push([result.ciphers.length, groupingsMap.get(c.folderId)]); } else if (this.organization && c.collectionIds != null) { From 19516c20ff6f788a912fcf1fe70f2d3a2b74e4f2 Mon Sep 17 00:00:00 2001 From: Kovah Date: Wed, 27 Mar 2019 19:22:59 +0100 Subject: [PATCH 0774/1626] Add support for CdkDragDrop in add-edit.component.js (#35) --- package-lock.json | 9 +++++++++ package.json | 1 + src/angular/components/add-edit.component.ts | 6 ++++++ 3 files changed, 16 insertions(+) diff --git a/package-lock.json b/package-lock.json index 3a0063cbf1..4751d46f43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,15 @@ "tslib": "^1.9.0" } }, + "@angular/cdk": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-7.2.1.tgz", + "integrity": "sha512-oU1Pjq3JkDtkXquLxWK84A2jOCeYRf352dVGbQCxWoSOQ5KBtMAd42huGidPiOSHN6/f7xZwL3n4fq3fVIut8A==", + "requires": { + "parse5": "^5.0.0", + "tslib": "^1.7.1" + } + }, "@angular/common": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/@angular/common/-/common-7.2.1.tgz", diff --git a/package.json b/package.json index 2428aa6c8b..a41955315e 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ }, "dependencies": { "@angular/animations": "7.2.1", + "@angular/cdk": "7.2.1", "@angular/common": "7.2.1", "@angular/compiler": "7.2.1", "@angular/core": "7.2.1", diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 94f6ba3b16..8a4ae00abe 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -1,3 +1,4 @@ +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { EventEmitter, Input, @@ -345,6 +346,11 @@ export class AddEditComponent implements OnInit { u.showOptions = u.showOptions == null ? true : u.showOptions; } + + drop(event: CdkDragDrop) { + moveItemInArray(this.cipher.fields, event.previousIndex, event.currentIndex); + } + async organizationChanged() { if (this.writeableCollections != null) { this.writeableCollections.forEach((c) => (c as any).checked = false); From 6c940538d264aaf6fb86bfc5b1dceb8543414fb2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 27 Mar 2019 14:25:23 -0400 Subject: [PATCH 0775/1626] some audit fixes --- package-lock.json | 105 ++++++++++++++++++++-------------------------- 1 file changed, 45 insertions(+), 60 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4751d46f43..4345fe629d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -524,12 +524,12 @@ } }, "append-transform": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", - "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", + "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=", "dev": true, "requires": { - "default-require-extensions": "^2.0.0" + "default-require-extensions": "^1.0.0" } }, "aproba": { @@ -1692,12 +1692,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.18.0.tgz", "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==" }, - "compare-versions": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.3.1.tgz", - "integrity": "sha512-GkIcfJ9sDt4+gS+RWH3X+kR7ezuKdu3fg2oA9nRA8HZoqZwAKv3ml3TyfB9OyV2iFXxCw7q5XfV6SyPbSCT2pw==", - "dev": true - }, "component-bind": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", @@ -2099,20 +2093,12 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" }, "default-require-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", - "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", + "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=", "dev": true, "requires": { - "strip-bom": "^3.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - } + "strip-bom": "^2.0.0" } }, "defaults": { @@ -4399,44 +4385,43 @@ } }, "istanbul-api": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.1.tgz", - "integrity": "sha512-duj6AlLcsWNwUpfyfHt0nWIeRiZpuShnP40YTxOGQgtaN8fd6JYSxsvxUphTDy8V5MfDXo4s/xVCIIvVCO808g==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.7.tgz", + "integrity": "sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA==", "dev": true, "requires": { "async": "^2.1.4", - "compare-versions": "^3.1.0", "fileset": "^2.0.2", - "istanbul-lib-coverage": "^1.2.0", - "istanbul-lib-hook": "^1.2.0", - "istanbul-lib-instrument": "^1.10.1", - "istanbul-lib-report": "^1.1.4", - "istanbul-lib-source-maps": "^1.2.4", - "istanbul-reports": "^1.3.0", + "istanbul-lib-coverage": "^1.2.1", + "istanbul-lib-hook": "^1.2.2", + "istanbul-lib-instrument": "^1.10.2", + "istanbul-lib-report": "^1.1.5", + "istanbul-lib-source-maps": "^1.2.6", + "istanbul-reports": "^1.5.1", "js-yaml": "^3.7.0", "mkdirp": "^0.5.1", "once": "^1.4.0" } }, "istanbul-lib-coverage": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz", - "integrity": "sha512-GvgM/uXRwm+gLlvkWHTjDAvwynZkL9ns15calTrmhGgowlwJBbWMYzWbKqE2DT6JDP1AFXKa+Zi0EkqNCUqY0A==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", + "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", "dev": true }, "istanbul-lib-hook": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.1.tgz", - "integrity": "sha512-eLAMkPG9FU0v5L02lIkcj/2/Zlz9OuluaXikdr5iStk8FDbSwAixTK9TkYxbF0eNnzAJTwM2fkV2A1tpsIp4Jg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz", + "integrity": "sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw==", "dev": true, "requires": { - "append-transform": "^1.0.0" + "append-transform": "^0.4.0" } }, "istanbul-lib-instrument": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz", - "integrity": "sha512-1dYuzkOCbuR5GRJqySuZdsmsNKPL3PTuyPevQfoCXJePT9C8y1ga75neU+Tuy9+yS3G/dgx8wgOmp2KLpgdoeQ==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz", + "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", "dev": true, "requires": { "babel-generator": "^6.18.0", @@ -4444,30 +4429,30 @@ "babel-traverse": "^6.18.0", "babel-types": "^6.18.0", "babylon": "^6.18.0", - "istanbul-lib-coverage": "^1.2.0", + "istanbul-lib-coverage": "^1.2.1", "semver": "^5.3.0" } }, "istanbul-lib-report": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.4.tgz", - "integrity": "sha512-Azqvq5tT0U09nrncK3q82e/Zjkxa4tkFZv7E6VcqP0QCPn6oNljDPfrZEC/umNXds2t7b8sRJfs6Kmpzt8m2kA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz", + "integrity": "sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw==", "dev": true, "requires": { - "istanbul-lib-coverage": "^1.2.0", + "istanbul-lib-coverage": "^1.2.1", "mkdirp": "^0.5.1", "path-parse": "^1.0.5", "supports-color": "^3.1.2" } }, "istanbul-lib-source-maps": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.5.tgz", - "integrity": "sha512-8O2T/3VhrQHn0XcJbP1/GN7kXMiRAlPi+fj3uEHrjBD8Oz7Py0prSC25C09NuAZS6bgW1NNKAvCSHZXB0irSGA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz", + "integrity": "sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg==", "dev": true, "requires": { "debug": "^3.1.0", - "istanbul-lib-coverage": "^1.2.0", + "istanbul-lib-coverage": "^1.2.1", "mkdirp": "^0.5.1", "rimraf": "^2.6.1", "source-map": "^0.5.3" @@ -4482,9 +4467,9 @@ } }, "istanbul-reports": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.3.0.tgz", - "integrity": "sha512-y2Z2IMqE1gefWUaVjrBm0mSKvUkaBy9Vqz8iwr/r40Y9hBbIteH5wqHG/9DLTfJ9xUnUT2j7A3+VVJ6EaYBllA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.5.1.tgz", + "integrity": "sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw==", "dev": true, "requires": { "handlebars": "^4.0.3" @@ -4542,9 +4527,9 @@ "dev": true }, "js-yaml": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", - "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.0.tgz", + "integrity": "sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==", "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -4971,9 +4956,9 @@ } }, "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" }, "lodash.debounce": { "version": "4.0.8", From a22bb09fc3370c94b2a9e1e8eba3a898b34781a9 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 27 Mar 2019 14:41:06 -0400 Subject: [PATCH 0776/1626] update karma deps --- package-lock.json | 1976 +++++++++++++++++++++++++++------------------ package.json | 19 +- tsconfig.json | 4 + 3 files changed, 1199 insertions(+), 800 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4345fe629d..0f3b3d4769 100644 --- a/package-lock.json +++ b/package-lock.json @@ -106,6 +106,146 @@ "msgpack5": "^4.0.2" } }, + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/generator": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.0.tgz", + "integrity": "sha512-/v5I+a1jhGSKLgZDcmAUZ4K/VePi43eRkUs3yePW1HB1iANOD5tqJXwGSG4BZhSksP8J9ejSlwGeTiiOFZOrXQ==", + "dev": true, + "requires": { + "@babel/types": "^7.4.0", + "jsesc": "^2.5.1", + "lodash": "^4.17.11", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-function-name": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", + "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", + "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.0.tgz", + "integrity": "sha512-7Cuc6JZiYShaZnybDmfwhY4UYHzI6rlqhWjaIqbsJGsIqPimEYy5uh3akSRLMg65LSdSEnJ8a8/bWQN6u2oMGw==", + "dev": true, + "requires": { + "@babel/types": "^7.4.0" + } + }, + "@babel/highlight": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", + "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + } + } + }, + "@babel/parser": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.2.tgz", + "integrity": "sha512-9fJTDipQFvlfSVdD/JBtkiY0br9BtfvW2R8wo6CX/Ej2eMuV0gWPk1M67Mt3eggQvBqYW1FCEk8BN7WvGm/g5g==", + "dev": true + }, + "@babel/template": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.0.tgz", + "integrity": "sha512-SOWwxxClTTh5NdbbYZ0BmaBVzxzTh2tO/TeLTbF6MO6EzVhHTnff8CdBXx3mEtazFBoysmEM6GU/wF+SuSx4Fw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.4.0", + "@babel/types": "^7.4.0" + } + }, + "@babel/traverse": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.0.tgz", + "integrity": "sha512-/DtIHKfyg2bBKnIN+BItaIlEg5pjAnzHOIQe5w+rHAw/rg9g0V7T4rqPX8BJPfW11kt3koyjAnTNwCzb28Y1PA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.4.0", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.0", + "@babel/parser": "^7.4.0", + "@babel/types": "^7.4.0", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.11" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.0.tgz", + "integrity": "sha512-aPvkXyU2SPOnztlgo8n9cEiXW755mgyvueUPcpStqdzoSPm0fjO0vQBjLkt3JKJW7ufikfcnMTTPsN1xaTsBPA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.11", + "to-fast-properties": "^2.0.0" + } + }, "@types/commander": { "version": "2.12.2", "resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.2.tgz", @@ -369,9 +509,9 @@ } }, "acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", + "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", "dev": true }, "acorn-globals": { @@ -412,22 +552,12 @@ "uri-js": "^4.2.2" } }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "dev": true, - "requires": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - } - }, "amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true + "dev": true, + "optional": true }, "ansi-align": { "version": "2.0.0", @@ -471,13 +601,13 @@ } } }, - "ansi-cyan": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", - "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=", + "ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", "dev": true, "requires": { - "ansi-wrap": "0.1.0" + "ansi-wrap": "^0.1.0" } }, "ansi-escapes": { @@ -485,15 +615,6 @@ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" }, - "ansi-red": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", - "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", - "dev": true, - "requires": { - "ansi-wrap": "0.1.0" - } - }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -524,12 +645,12 @@ } }, "append-transform": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", - "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", "dev": true, "requires": { - "default-require-extensions": "^1.0.0" + "default-require-extensions": "^2.0.0" } }, "aproba": { @@ -567,9 +688,9 @@ "dev": true }, "arr-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", - "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", "dev": true }, "array-equal": { @@ -583,12 +704,6 @@ "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", "dev": true }, - "array-slice": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", - "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", - "dev": true - }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", @@ -658,12 +773,12 @@ "dev": true }, "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", + "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", "dev": true, "requires": { - "lodash": "^4.17.10" + "lodash": "^4.17.11" } }, "async-each": { @@ -745,39 +860,6 @@ } } }, - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "dev": true, - "requires": { - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "detect-indent": "^4.0.0", - "jsesc": "^1.3.0", - "lodash": "^4.17.4", - "source-map": "^0.5.7", - "trim-right": "^1.0.1" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, "babel-polyfill": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.23.0.tgz", @@ -804,65 +886,6 @@ "regenerator-runtime": "^0.11.0" } }, - "babel-template": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "lodash": "^4.17.4" - } - }, - "babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "dev": true, - "requires": { - "babel-code-frame": "^6.26.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "debug": "^2.6.8", - "globals": "^9.18.0", - "invariant": "^2.2.2", - "lodash": "^4.17.4" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - } - }, - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "dev": true - }, "backo2": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", @@ -1004,15 +1027,15 @@ } }, "blob": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", - "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=", + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==", "dev": true }, "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", + "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==", "dev": true }, "bluebird-lst": { @@ -1180,17 +1203,6 @@ "snapdragon-node": "^2.0.1", "split-string": "^3.0.2", "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } } }, "brorand": { @@ -1293,9 +1305,9 @@ } }, "buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.0.tgz", - "integrity": "sha512-nUJyfChH7PMJy75eRDCCKtszSEFokUNXC1hNVSe+o+VdcgvDPLs20k3v8UXI8ruRYAJiYtyRea8mYyqPxoHWDw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", + "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", "dev": true, "requires": { "base64-js": "^1.0.2", @@ -1440,17 +1452,6 @@ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "dev": true, - "optional": true, - "requires": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" - } - }, "chalk": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", @@ -1482,24 +1483,591 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, "chokidar": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", - "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.5.tgz", + "integrity": "sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A==", "dev": true, "requires": { "anymatch": "^2.0.0", - "async-each": "^1.0.0", - "braces": "^2.3.0", - "fsevents": "^1.2.2", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", "glob-parent": "^3.1.0", - "inherits": "^2.0.1", + "inherits": "^2.0.3", "is-binary-path": "^1.0.0", "is-glob": "^4.0.0", - "lodash.debounce": "^4.0.8", - "normalize-path": "^2.1.1", + "normalize-path": "^3.0.0", "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0", - "upath": "^1.0.5" + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "dependencies": { + "fsevents": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", + "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.9.2", + "node-pre-gyp": "^0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.2.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "upath": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", + "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==", + "dev": true + } } }, "chownr": { @@ -1583,27 +2151,6 @@ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "dev": true, - "optional": true, - "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", - "dev": true, - "optional": true - } - } - }, "clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", @@ -1644,15 +2191,6 @@ "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", "dev": true }, - "combine-lists": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/combine-lists/-/combine-lists-1.0.1.tgz", - "integrity": "sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y=", - "dev": true, - "requires": { - "lodash": "^4.5.0" - } - }, "combine-source-map": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", @@ -1692,6 +2230,12 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.18.0.tgz", "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==" }, + "compare-versions": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.4.0.tgz", + "integrity": "sha512-tK69D7oNXXqUW3ZNo/z7NXTEz22TCF0pTE+YF9cxvaAM9XnkLo1fV621xCLrRR6aevJlKxExkss0vWqUCUpqdg==", + "dev": true + }, "component-bind": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", @@ -1870,10 +2414,13 @@ "dev": true }, "convert-source-map": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", - "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", - "dev": true + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } }, "cookie": { "version": "0.3.1", @@ -1985,9 +2532,9 @@ "integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A==" }, "cssstyle": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.1.1.tgz", - "integrity": "sha512-364AI1l/M5TYcFH83JnOH/pSqgaNnKmYgKrm0didZMGKWjQB60dymwWy1rKUgL3J1ffdq9xVi2yGLHdSjjSNog==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.2.1.tgz", + "integrity": "sha512-7DYm8qe+gPx/h77QlCyFmX80+fGaE/6A/Ekl0zaszYOubvySO2saYFdQ78P29D0UsULxFKCetDGNaNRUdSF+2A==", "requires": { "cssom": "0.3.x" } @@ -2032,9 +2579,9 @@ "dev": true }, "date-format": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-0.0.0.tgz", - "integrity": "sha1-CSBoY6sHDrRZrOpVQsvYVrEZZrM=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.0.0.tgz", + "integrity": "sha512-M6UqVvZVgFYqZL1SfHsRGIQSz3ZL+qgbsV5Lp1Vj61LZVYuEwcMXYay7DRDtYs2HQQBK5hQtQ0fD9aEJ89V0LA==", "dev": true }, "date-now": { @@ -2093,12 +2640,20 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" }, "default-require-extensions": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", - "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", "dev": true, "requires": { - "strip-bom": "^2.0.0" + "strip-bom": "^3.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } } }, "defaults": { @@ -2189,15 +2744,6 @@ "minimalistic-assert": "^1.0.0" } }, - "detect-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -2394,9 +2940,9 @@ } }, "elliptic": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", - "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", + "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", "dev": true, "requires": { "bn.js": "^4.4.0", @@ -2431,9 +2977,9 @@ } }, "engine.io": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.0.tgz", - "integrity": "sha512-mRbgmAtQ4GAlKwuPnnAvXXwdPhEx+jkc0OBCLrXuD/CRvwNK3AxRSnqK4FSqmAMRRHryVJP8TopOvmEaA64fKw==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.1.tgz", + "integrity": "sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==", "dev": true, "requires": { "accepts": "~1.3.4", @@ -2464,15 +3010,15 @@ } }, "engine.io-parser": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.2.tgz", - "integrity": "sha512-dInLFzr80RijZ1rGpx1+56/uFoH7/7InhH3kZt+Ms6hT8tNx3NGW/WNSA/f8As1WkOfkuyb3tnRyuXGxusclMw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz", + "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==", "dev": true, "requires": { "after": "0.8.2", "arraybuffer.slice": "~0.0.7", "base64-arraybuffer": "0.1.5", - "blob": "0.0.4", + "blob": "0.0.5", "has-binary2": "~1.0.2" } }, @@ -2585,9 +3131,9 @@ "dev": true }, "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", + "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", "dev": true }, "evp_bytestokey": { @@ -2615,34 +3161,6 @@ "strip-eof": "^1.0.0" } }, - "expand-braces": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/expand-braces/-/expand-braces-0.1.2.tgz", - "integrity": "sha1-SIsdHSRRyz06axks/AMPRMWFX+o=", - "dev": true, - "requires": { - "array-slice": "^0.2.3", - "array-unique": "^0.2.1", - "braces": "^0.1.2" - }, - "dependencies": { - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", - "dev": true - }, - "braces": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz", - "integrity": "sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY=", - "dev": true, - "requires": { - "expand-range": "^0.1.0" - } - } - } - }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -2675,39 +3193,6 @@ "requires": { "is-descriptor": "^0.1.0" } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expand-range": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz", - "integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=", - "dev": true, - "requires": { - "is-number": "^0.1.1", - "repeat-string": "^0.2.2" - }, - "dependencies": { - "is-number": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz", - "integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=", - "dev": true - }, - "repeat-string": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-0.2.2.tgz", - "integrity": "sha1-x6jTI2BoNiBZp+RlH8aITosftK4=", - "dev": true } } }, @@ -2722,20 +3207,12 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "extend-shallow": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", - "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "kind-of": "^1.1.0" - }, - "dependencies": { - "kind-of": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", - "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", - "dev": true - } + "is-extendable": "^0.1.0" } }, "external-editor": { @@ -2783,15 +3260,6 @@ "is-descriptor": "^1.0.0" } }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", @@ -2909,17 +3377,6 @@ "is-number": "^3.0.0", "repeat-string": "^1.6.1", "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } } }, "finalhandler": { @@ -2964,13 +3421,36 @@ "pinkie-promise": "^2.0.0" } }, + "flatted": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz", + "integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==", + "dev": true + }, "follow-redirects": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.6.tgz", - "integrity": "sha512-xay/eYZGgdpb3rpugZj1HunNaPcqc6fud/RW7LNEQntvKzuRO4DDLL+MnJIbTHh6t3Kda3v2RvhY2doxUddnig==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz", + "integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==", "dev": true, "requires": { - "debug": "^3.1.0" + "debug": "^3.2.6" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } } }, "for-in": { @@ -3089,7 +3569,8 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", @@ -3114,7 +3595,8 @@ "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", @@ -3675,9 +4157,9 @@ } }, "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.11.0.tgz", + "integrity": "sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw==", "dev": true }, "got": { @@ -3705,32 +4187,15 @@ "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" }, "handlebars": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", - "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.1.tgz", + "integrity": "sha512-3Zhi6C0euYZL5sM0Zcy7lInLXKQ+YLcF/olbN010mzGQ4XVm50JeyBnMqofHh696GrciGruC7kCcApPDJvVgwA==", "dev": true, "requires": { - "async": "^1.4.0", + "neo-async": "^2.6.0", "optimist": "^0.6.1", - "source-map": "^0.4.4", - "uglify-js": "^2.6" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" } }, "har-schema": { @@ -3869,9 +4334,9 @@ } }, "hash.js": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz", - "integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", "dev": true, "requires": { "inherits": "^2.0.3", @@ -3951,9 +4416,9 @@ } }, "ieee754": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", - "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", "dev": true }, "ignore-by-default": { @@ -4074,15 +4539,6 @@ } } }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "requires": { - "loose-envify": "^1.0.0" - } - }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", @@ -4385,110 +4841,137 @@ } }, "istanbul-api": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.7.tgz", - "integrity": "sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-2.1.1.tgz", + "integrity": "sha512-kVmYrehiwyeBAk/wE71tW6emzLiHGjYIiDrc8sfyty4F8M02/lrgXSm+R1kXysmF20zArvmZXjlE/mg24TVPJw==", "dev": true, "requires": { - "async": "^2.1.4", - "fileset": "^2.0.2", - "istanbul-lib-coverage": "^1.2.1", - "istanbul-lib-hook": "^1.2.2", - "istanbul-lib-instrument": "^1.10.2", - "istanbul-lib-report": "^1.1.5", - "istanbul-lib-source-maps": "^1.2.6", - "istanbul-reports": "^1.5.1", - "js-yaml": "^3.7.0", - "mkdirp": "^0.5.1", + "async": "^2.6.1", + "compare-versions": "^3.2.1", + "fileset": "^2.0.3", + "istanbul-lib-coverage": "^2.0.3", + "istanbul-lib-hook": "^2.0.3", + "istanbul-lib-instrument": "^3.1.0", + "istanbul-lib-report": "^2.0.4", + "istanbul-lib-source-maps": "^3.0.2", + "istanbul-reports": "^2.1.1", + "js-yaml": "^3.12.0", + "make-dir": "^1.3.0", + "minimatch": "^3.0.4", "once": "^1.4.0" } }, "istanbul-lib-coverage": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", - "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-dKWuzRGCs4G+67VfW9pBFFz2Jpi4vSp/k7zBcJ888ofV5Mi1g5CUML5GvMvV6u9Cjybftu+E8Cgp+k0dI1E5lw==", "dev": true }, "istanbul-lib-hook": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz", - "integrity": "sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.3.tgz", + "integrity": "sha512-CLmEqwEhuCYtGcpNVJjLV1DQyVnIqavMLFHV/DP+np/g3qvdxu3gsPqYoJMXm15sN84xOlckFB3VNvRbf5yEgA==", "dev": true, "requires": { - "append-transform": "^0.4.0" + "append-transform": "^1.0.0" } }, "istanbul-lib-instrument": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz", - "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.1.0.tgz", + "integrity": "sha512-ooVllVGT38HIk8MxDj/OIHXSYvH+1tq/Vb38s8ixt9GoJadXska4WkGY+0wkmtYCZNYtaARniH/DixUGGLZ0uA==", "dev": true, "requires": { - "babel-generator": "^6.18.0", - "babel-template": "^6.16.0", - "babel-traverse": "^6.18.0", - "babel-types": "^6.18.0", - "babylon": "^6.18.0", - "istanbul-lib-coverage": "^1.2.1", - "semver": "^5.3.0" + "@babel/generator": "^7.0.0", + "@babel/parser": "^7.0.0", + "@babel/template": "^7.0.0", + "@babel/traverse": "^7.0.0", + "@babel/types": "^7.0.0", + "istanbul-lib-coverage": "^2.0.3", + "semver": "^5.5.0" } }, "istanbul-lib-report": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz", - "integrity": "sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.4.tgz", + "integrity": "sha512-sOiLZLAWpA0+3b5w5/dq0cjm2rrNdAfHWaGhmn7XEFW6X++IV9Ohn+pnELAl9K3rfpaeBfbmH9JU5sejacdLeA==", "dev": true, "requires": { - "istanbul-lib-coverage": "^1.2.1", - "mkdirp": "^0.5.1", - "path-parse": "^1.0.5", - "supports-color": "^3.1.2" + "istanbul-lib-coverage": "^2.0.3", + "make-dir": "^1.3.0", + "supports-color": "^6.0.0" + }, + "dependencies": { + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } } }, "istanbul-lib-source-maps": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz", - "integrity": "sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.2.tgz", + "integrity": "sha512-JX4v0CiKTGp9fZPmoxpu9YEkPbEqCqBbO3403VabKjH+NRXo72HafD5UgnjTEqHL2SAjaZK1XDuDOkn6I5QVfQ==", "dev": true, "requires": { - "debug": "^3.1.0", - "istanbul-lib-coverage": "^1.2.1", - "mkdirp": "^0.5.1", - "rimraf": "^2.6.1", - "source-map": "^0.5.3" + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.3", + "make-dir": "^1.3.0", + "rimraf": "^2.6.2", + "source-map": "^0.6.1" }, "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true } } }, "istanbul-reports": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.5.1.tgz", - "integrity": "sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.1.1.tgz", + "integrity": "sha512-FzNahnidyEPBCI0HcufJoSEoKykesRlFcSzQqjH9x0+LC8tnnE/p/90PBLu8iZTxr8yYZNyTtiAujUqyN+CIxw==", "dev": true, "requires": { - "handlebars": "^4.0.3" + "handlebars": "^4.1.0" } }, "jasmine": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.2.0.tgz", - "integrity": "sha512-qv6TZ32r+slrQz8fbx2EhGbD9zlJo3NwPrpLK1nE8inILtZO9Fap52pyHk7mNTh4tG50a+1+tOiWVT3jO5I0Sg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.3.1.tgz", + "integrity": "sha512-/vU3/H7U56XsxIXHwgEuWpCgQ0bRi2iiZeUpx7Nqo8n1TpoDHfZhkPIc7CO8I4pnMzYsi3XaSZEiy8cnTfujng==", "dev": true, "requires": { "glob": "^7.0.6", - "jasmine-core": "~3.2.0" + "jasmine-core": "~3.3.0" } }, "jasmine-core": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.2.1.tgz", - "integrity": "sha512-pa9tbBWgU0EE4SWgc85T4sa886ufuQdsgruQANhECYjwqgV4z7Vw/499aCaP8ZH79JDS4vhm8doDG9HO4+e4sA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.3.0.tgz", + "integrity": "sha512-3/xSmG/d35hf80BEN66Y6g9Ca5l/Isdeg/j6zvbTYlTzeKinzmaTM4p9am5kYqOmE05D7s1t8FGjzdSnbUbceA==", "dev": true }, "jasmine-spec-reporter": { @@ -4625,9 +5108,9 @@ } }, "jsesc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, "json-schema": { @@ -4665,27 +5148,27 @@ } }, "karma": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/karma/-/karma-3.0.0.tgz", - "integrity": "sha512-ZTjyuDXVXhXsvJ1E4CnZzbCjSxD6sEdzEsFYogLuZM0yqvg/mgz+O+R1jb0J7uAQeuzdY8kJgx6hSNXLwFuHIQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/karma/-/karma-4.0.1.tgz", + "integrity": "sha512-ind+4s03BqIXas7ZmraV3/kc5+mnqwCd+VDX1FndS6jxbt03kQKX2vXrWxNLuCjVYmhMwOZosAEKMM0a2q7w7A==", "dev": true, "requires": { "bluebird": "^3.3.0", "body-parser": "^1.16.1", + "braces": "^2.3.2", "chokidar": "^2.0.3", "colors": "^1.1.0", - "combine-lists": "^1.0.0", "connect": "^3.6.0", "core-js": "^2.2.0", "di": "^0.0.1", "dom-serialize": "^2.2.0", - "expand-braces": "^0.1.1", + "flatted": "^2.0.0", "glob": "^7.1.1", "graceful-fs": "^4.1.2", "http-proxy": "^1.13.0", "isbinaryfile": "^3.0.0", - "lodash": "^4.17.4", - "log4js": "^3.0.0", + "lodash": "^4.17.11", + "log4js": "^4.0.0", "mime": "^2.3.1", "minimatch": "^3.0.2", "optimist": "^0.6.1", @@ -4696,7 +5179,7 @@ "socket.io": "2.1.1", "source-map": "^0.6.1", "tmp": "0.0.33", - "useragent": "2.2.1" + "useragent": "2.3.0" } }, "karma-chrome-launcher": { @@ -4710,12 +5193,12 @@ } }, "karma-cli": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/karma-cli/-/karma-cli-1.0.1.tgz", - "integrity": "sha1-rmw8WKMTodALRRZMRVubhs4X+WA=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/karma-cli/-/karma-cli-2.0.0.tgz", + "integrity": "sha512-1Kb28UILg1ZsfqQmeELbPzuEb5C6GZJfVIk0qOr8LNYQuYWmAaqP16WpbpKEjhejDrDYyYOwwJXSZO6u7q5Pvw==", "dev": true, "requires": { - "resolve": "^1.1.6" + "resolve": "^1.3.3" } }, "karma-coverage": { @@ -4740,19 +5223,19 @@ } }, "karma-coverage-istanbul-reporter": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-2.0.1.tgz", - "integrity": "sha512-UcgrHkFehI5+ivMouD8NH/UOHiX4oCAtwaANylzPFdcAuD52fnCUuelacq2gh8tZ4ydhU3+xiXofSq7j5Ehygw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-2.0.5.tgz", + "integrity": "sha512-yPvAlKtY3y+rKKWbOo0CzBMVTvJEeMOgbMXuVv3yWvS8YtYKC98AU9vFF0mVBZ2RP1E9SgS90+PT6Kf14P3S4w==", "dev": true, "requires": { - "istanbul-api": "^1.3.1", + "istanbul-api": "^2.1.1", "minimatch": "^3.0.4" } }, "karma-detect-browsers": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/karma-detect-browsers/-/karma-detect-browsers-2.3.2.tgz", - "integrity": "sha512-EFku2S5IpUEpJR2XxJa/onW6tIuapa3kYWJDD7Tk6LqhhIxfKWvJ+vnleLop6utXT28204hZptnfH7PGSmk4Nw==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/karma-detect-browsers/-/karma-detect-browsers-2.3.3.tgz", + "integrity": "sha512-ltFVyA3ijThv9l9TQ+TKnccoMk6YAWn8OMaccL+n8pO2LGwMOcy6tUWy3Mnv9If29jqvVHDCWntj7wBQpPtv7Q==", "dev": true, "requires": { "which": "^1.2.4" @@ -4774,15 +5257,26 @@ "dev": true }, "karma-jasmine": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.1.2.tgz", - "integrity": "sha1-OU8rJf+0pkS5rabyLUQ+L9CIhsM=", - "dev": true + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-2.0.1.tgz", + "integrity": "sha512-iuC0hmr9b+SNn1DaUD2QEYtUxkS1J+bSJSn7ejdEexs7P8EYvA1CWkEdrDQ+8jVH3AgWlCNwjYsT1chjcNW9lA==", + "dev": true, + "requires": { + "jasmine-core": "^3.3" + }, + "dependencies": { + "jasmine-core": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.3.0.tgz", + "integrity": "sha512-3/xSmG/d35hf80BEN66Y6g9Ca5l/Isdeg/j6zvbTYlTzeKinzmaTM4p9am5kYqOmE05D7s1t8FGjzdSnbUbceA==", + "dev": true + } + } }, "karma-jasmine-html-reporter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.3.1.tgz", - "integrity": "sha512-J8pUc58QeRhpHQ+sXBRZ016ZW9ZOjU4vjYA6Jah69WvBaqR7tGvXUzy7w/DoULbNrD8+hnZCpvdeEtqXtirY2g==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.4.0.tgz", + "integrity": "sha512-0wxhwA8PLPpICZ4o2GRnPi67hf3JhfQm5WCB8nElh4qsE6wRNOTtrqooyBPNqI087Xr2SBhxLg5fU+BJ/qxRrw==", "dev": true }, "karma-safari-launcher": { @@ -4792,80 +5286,104 @@ "dev": true }, "karma-typescript": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/karma-typescript/-/karma-typescript-3.0.13.tgz", - "integrity": "sha1-iUivvRA6wZh6WWGg9DoboocQZ80=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/karma-typescript/-/karma-typescript-4.0.0.tgz", + "integrity": "sha512-D5mrwspHW8GtvbjYCz29DbxXYqoCOFhbnhZRkmSytWMCTFAwjSHMKxozOM5HeZomS2lqWV+S0WPk4DH9nxkGgA==", "dev": true, "requires": { - "acorn": "^4.0.4", + "acorn": "^6.0.5", + "acorn-walk": "^6.1.1", "assert": "^1.4.1", - "async": "^2.1.4", - "browser-resolve": "^1.11.0", + "async": "^2.6.1", + "browser-resolve": "^1.11.3", "browserify-zlib": "^0.2.0", - "buffer": "^5.0.6", + "buffer": "^5.2.1", "combine-source-map": "^0.8.0", "console-browserify": "^1.1.0", "constants-browserify": "^1.0.0", - "convert-source-map": "^1.5.0", - "crypto-browserify": "^3.11.1", - "diff": "^3.2.0", - "domain-browser": "^1.1.7", - "events": "^1.1.1", - "glob": "^7.1.1", + "convert-source-map": "^1.6.0", + "crypto-browserify": "^3.12.0", + "diff": "^4.0.1", + "domain-browser": "^1.2.0", + "events": "^3.0.0", + "glob": "^7.1.3", "https-browserify": "^1.0.0", "istanbul": "0.4.5", "json-stringify-safe": "^5.0.1", "karma-coverage": "^1.1.1", - "lodash": "^4.17.4", - "log4js": "^1.1.1", - "minimatch": "^3.0.3", + "lodash": "^4.17.11", + "log4js": "^4.0.1", + "minimatch": "^3.0.4", "os-browserify": "^0.3.0", - "pad": "^2.0.0", - "path-browserify": "0.0.0", + "pad": "^2.2.2", + "path-browserify": "^1.0.0", "process": "^0.11.10", - "punycode": "^1.4.1", + "punycode": "^2.1.1", "querystring-es3": "^0.2.1", - "readable-stream": "^2.3.3", - "remap-istanbul": "^0.10.1", - "source-map": "0.6.1", - "stream-browserify": "^2.0.1", - "stream-http": "^2.7.2", - "string_decoder": "^1.0.3", - "timers-browserify": "^2.0.2", - "tmp": "0.0.29", - "tty-browserify": "0.0.0", + "readable-stream": "^3.1.1", + "remap-istanbul": "^0.13.0", + "source-map": "^0.7.3", + "stream-browserify": "^2.0.2", + "stream-http": "^3.0.0", + "string_decoder": "^1.2.0", + "timers-browserify": "^2.0.10", + "tmp": "^0.0.33", + "tty-browserify": "^0.0.1", "url": "^0.11.0", - "util": "^0.10.3", - "vm-browserify": "0.0.4" + "util": "^0.11.1", + "vm-browserify": "1.1.0" }, "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "diff": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", + "dev": true + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "requires": { - "ms": "2.0.0" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, - "log4js": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-1.1.1.tgz", - "integrity": "sha1-wh0px2BAieTyVYM+f5SzRh3h/0M=", + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "readable-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.2.0.tgz", + "integrity": "sha512-RV20kLjdmpZuTF1INEb9IA3L68Nmi+Ri7ppZqo78wj//Pn62fCoJyV9zalccNzDD/OuJpMG4f+pfMl8+L6QdGw==", "dev": true, "requires": { - "debug": "^2.2.0", - "semver": "^5.3.0", - "streamroller": "^0.4.0" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" } }, - "tmp": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz", - "integrity": "sha1-8lEl/w3Z2jzLDC3Tce4SiLuRKMA=", + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + }, + "string_decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", + "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", "dev": true, "requires": { - "os-tmpdir": "~1.0.1" + "safe-buffer": "~5.1.0" } } } @@ -4897,13 +5415,6 @@ "package-json": "^4.0.0" } }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "dev": true, - "optional": true - }, "lazy-val": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.3.tgz", @@ -4983,59 +5494,35 @@ "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" }, "log4js": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-3.0.5.tgz", - "integrity": "sha512-IX5c3G/7fuTtdr0JjOT2OIR12aTESVhsH6cEsijloYwKgcPRlO6DgOU72v0UFhWcoV1HN6+M3dwT89qVPLXm0w==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-4.1.0.tgz", + "integrity": "sha512-eDa+zZPeVEeK6QGJAePyXM6pg4P3n3TO5rX9iZMVY48JshsTyLJZLIL5HipI1kQ2qLsSyOpUqNND/C5H4WhhiA==", "dev": true, "requires": { - "circular-json": "^0.5.5", - "date-format": "^1.2.0", - "debug": "^3.1.0", + "date-format": "^2.0.0", + "debug": "^4.1.1", + "flatted": "^2.0.0", "rfdc": "^1.1.2", - "streamroller": "0.7.0" + "streamroller": "^1.0.4" }, "dependencies": { - "circular-json": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.5.5.tgz", - "integrity": "sha512-13YaR6kiz0kBNmIVM87Io8Hp7bWOo4r61vkEANy8iH9R9bc6avud/1FT0SBpqR1RpIQADOh/Q+yHZDA1iL6ysA==", - "dev": true - }, - "date-format": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-1.2.0.tgz", - "integrity": "sha1-YV6CjiM90aubua4JUODOzPpuytg=", - "dev": true - }, - "streamroller": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-0.7.0.tgz", - "integrity": "sha512-WREzfy0r0zUqp3lGO096wRuUp7ho1X6uo/7DJfTlEi0Iv/4gT7YHqXDjKC2ioVGBZtE8QzsQD9nx1nIuoZ57jQ==", + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { - "date-format": "^1.2.0", - "debug": "^3.1.0", - "mkdirp": "^0.5.1", - "readable-stream": "^2.3.0" + "ms": "^2.1.1" } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true } } }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, "loud-rejection": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", @@ -5115,13 +5602,14 @@ } }, "md5.js": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", - "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", "dev": true, "requires": { "hash-base": "^3.0.0", - "inherits": "^2.0.1" + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" } }, "media-typer": { @@ -5207,9 +5695,9 @@ } }, "mime": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz", - "integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz", + "integrity": "sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==", "dev": true }, "mime-db": { @@ -5404,6 +5892,12 @@ "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", "dev": true }, + "neo-async": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz", + "integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==", + "dev": true + }, "ngx-infinite-scroll": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-7.0.1.tgz", @@ -6267,18 +6761,18 @@ } }, "pad": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pad/-/pad-2.1.0.tgz", - "integrity": "sha512-VeCrKYxgIoJ5RBpxogsoDiizLbZ2ZN+9ua+caIxe4e/a64kIzb9eVZ2CwWYd+arPbnM4N81lzlhTsHLtjSKJ0g==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/pad/-/pad-2.2.2.tgz", + "integrity": "sha512-w5m0/ISM+Jc/xsxNwKzHhOX+KHKYP3tT+UMmZfCgMSLdCccbjMNYx6LtafOql60qI4NYv13ZGAkSiehJj9ttcQ==", "dev": true, "requires": { "wcwidth": "^1.0.1" } }, "pako": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", - "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", + "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", "dev": true }, "papaparse": { @@ -6287,16 +6781,17 @@ "integrity": "sha512-ylm8pmgyz9rkS3Ng/ru5tHUF3JxWwKYP0aZZWZ8eCGdSxoqgYiDUXLNQei73mUJOjHw8QNu5ZNCsLoDpkMA6sg==" }, "parse-asn1": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", - "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz", + "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==", "dev": true, "requires": { "asn1.js": "^4.0.0", "browserify-aes": "^1.0.0", "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3" + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" } }, "parse-json": { @@ -6344,9 +6839,9 @@ "dev": true }, "path-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", - "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.0.tgz", + "integrity": "sha512-Hkavx/nY4/plImrZPHRk2CL9vpOymZLgEbMNX1U0bjcBL7QN9wODxyx0yaMZURSQaUtSEvDrfAvxa9oPb0at9g==", "dev": true }, "path-dirname": { @@ -6417,9 +6912,9 @@ } }, "pbkdf2": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz", - "integrity": "sha512-y4CXP3thSxqf7c0qmOF+9UeOTrifiVTIM+u7NWlq+PRsHbr7r7dpCmvzrZxa96JJUNi0Y5w9VqG5ZNeCVMoDcA==", + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", "dev": true, "requires": { "create-hash": "^1.1.2", @@ -6477,26 +6972,34 @@ } }, "plugin-error": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", - "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", "dev": true, "requires": { - "ansi-cyan": "^0.1.1", - "ansi-red": "^0.1.1", - "arr-diff": "^1.0.1", - "arr-union": "^2.0.1", - "extend-shallow": "^1.1.2" + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" }, "dependencies": { - "arr-diff": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", - "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo=", + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", "dev": true, "requires": { - "arr-flatten": "^1.0.1", - "array-slice": "^0.2.3" + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" } } } @@ -6613,16 +7116,17 @@ } }, "public-encrypt": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", - "integrity": "sha512-4kJ5Esocg8X3h8YgJsKAuoesBgB7mqH3eowiDzMUPKiRDDE7E/BqqZD1hnTByIaAFiwAw246YEltSq7tdrOH0Q==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", "dev": true, "requires": { "bn.js": "^4.1.0", "browserify-rsa": "^4.0.0", "create-hash": "^1.1.0", "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1" + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" } }, "pump": { @@ -6663,9 +7167,9 @@ "dev": true }, "randombytes": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", - "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, "requires": { "safe-buffer": "^5.1.0" @@ -6823,53 +7327,26 @@ } }, "remap-istanbul": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/remap-istanbul/-/remap-istanbul-0.10.1.tgz", - "integrity": "sha512-gsNQXs5kJLhErICSyYhzVZ++C8LBW8dgwr874Y2QvzAUS75zBlD/juZgXs39nbYJ09fZDlX2AVLVJAY2jbFJoQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/remap-istanbul/-/remap-istanbul-0.13.0.tgz", + "integrity": "sha512-rS5ZpVAx3fGtKZkiBe1esXg5mKYbgW9iz8kkADFt3p6lo3NsBBUX1q6SwdhwUtYCGnr7nK6gRlbYK3i8R0jbRA==", "dev": true, "requires": { - "amdefine": "^1.0.0", "istanbul": "0.4.5", - "minimatch": "^3.0.3", - "plugin-error": "^0.1.2", - "source-map": "^0.6.1", - "through2": "2.0.1" + "minimatch": "^3.0.4", + "plugin-error": "^1.0.1", + "source-map": "0.6.1", + "through2": "3.0.0" }, "dependencies": { - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", - "dev": true - }, - "readable-stream": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "~1.0.0", - "process-nextick-args": "~1.0.6", - "string_decoder": "~0.10.x", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, "through2": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.1.tgz", - "integrity": "sha1-OE51MU1J8y3hLuu4E2uOtrXVnak=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.0.tgz", + "integrity": "sha512-8B+sevlqP4OiCjonI1Zw03Sf8PuV1eRsYQgLad5eonILOdyeRsY27A/2Ze8IlvlMvq31OH+3fz/styI7Ya62yQ==", "dev": true, "requires": { - "readable-stream": "~2.0.0", - "xtend": "~4.0.0" + "readable-stream": "2 || 3", + "xtend": "~4.0.1" } } } @@ -6988,16 +7465,6 @@ "integrity": "sha512-92ktAgvZhBzYTIK0Mja9uen5q5J3NRVMoDkJL2VMwq6SXjVCgqvQeVP2XAaUY6HT+XpQYeLSjb3UoitBryKmdA==", "dev": true }, - "right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "dev": true, - "optional": true, - "requires": { - "align-text": "^0.1.1" - } - }, "rimraf": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", @@ -7553,9 +8020,9 @@ } }, "stream-browserify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", - "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", "dev": true, "requires": { "inherits": "~2.0.1", @@ -7572,60 +8039,41 @@ } }, "stream-http": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", - "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.0.0.tgz", + "integrity": "sha512-JELJfd+btL9GHtxU3+XXhg9NLYrKFnhybfvRuDghtyVkOFydz3PKNT1df07AMr88qW03WHF+FSV0PySpXignCA==", "dev": true, "requires": { "builtin-status-codes": "^3.0.0", "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "to-arraybuffer": "^1.0.0", + "readable-stream": "^3.0.6", "xtend": "^4.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.2.0.tgz", + "integrity": "sha512-RV20kLjdmpZuTF1INEb9IA3L68Nmi+Ri7ppZqo78wj//Pn62fCoJyV9zalccNzDD/OuJpMG4f+pfMl8+L6QdGw==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, "streamroller": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-0.4.1.tgz", - "integrity": "sha1-1DW9WXQ3Or2b2QaDWVEwhRBswF8=", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-1.0.4.tgz", + "integrity": "sha512-Wc2Gm5ygjSX8ZpW9J7Y9FwiSzTlKSvcl0FTTMd3rn7RoxDXpBW+xD9TY5sWL2n0UR61COB0LG1BQvN6nTUQbLQ==", "dev": true, "requires": { - "date-format": "^0.0.0", - "debug": "^0.7.2", - "mkdirp": "^0.5.1", - "readable-stream": "^1.1.7" - }, - "dependencies": { - "debug": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", - "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=", - "dev": true - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } + "async": "^2.6.1", + "date-format": "^2.0.0", + "debug": "^3.1.0", + "fs-extra": "^7.0.0", + "lodash": "^4.17.10" } }, "string-width": { @@ -7855,21 +8303,15 @@ "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", "dev": true }, - "to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", - "dev": true - }, "to-buffer": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" }, "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true }, "to-object-path": { @@ -8075,9 +8517,9 @@ } }, "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", "dev": true }, "tunnel-agent": { @@ -8135,33 +8577,25 @@ "dev": true }, "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.2.tgz", + "integrity": "sha512-imog1WIsi9Yb56yRt5TfYVxGmnWs3WSGU73ieSOlMVFwhJCA9W8fqFFMMj4kgDqiS/80LGdsYnWL7O9UcjEBlg==", "dev": true, "optional": true, "requires": { - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" + "commander": "~2.19.0", + "source-map": "~0.6.1" }, "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "commander": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", "dev": true, "optional": true } } }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "dev": true, - "optional": true - }, "ultron": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", @@ -8417,27 +8851,19 @@ "dev": true }, "useragent": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.2.1.tgz", - "integrity": "sha1-z1k+9PLRdYdei7ZY6pLhik/QbY4=", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", + "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==", "dev": true, "requires": { - "lru-cache": "2.2.x", + "lru-cache": "4.1.x", "tmp": "0.0.x" - }, - "dependencies": { - "lru-cache": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.2.4.tgz", - "integrity": "sha1-bGWGGb7PFAMdDQtZSxYELOTcBj0=", - "dev": true - } } }, "util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", "dev": true, "requires": { "inherits": "2.0.3" @@ -8480,13 +8906,10 @@ } }, "vm-browserify": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", - "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", - "dev": true, - "requires": { - "indexof": "0.0.1" - } + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", + "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==", + "dev": true }, "void-elements": { "version": "2.0.1", @@ -8623,13 +9046,6 @@ } } }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", - "dev": true, - "optional": true - }, "wordwrap": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", @@ -8695,28 +9111,6 @@ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", "dev": true }, - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "dev": true, - "optional": true, - "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" - }, - "dependencies": { - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "dev": true, - "optional": true - } - } - }, "yauzl": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", diff --git a/package.json b/package.json index a41955315e..e2609e4a8d 100644 --- a/package.json +++ b/package.json @@ -38,22 +38,23 @@ "@types/webcrypto": "0.0.28", "@types/zxcvbn": "^4.4.0", "concurrently": "3.5.1", + "cssstyle": "1.2.1", "electron": "3.0.14", - "jasmine": "^3.2.0", - "jasmine-core": "^3.2.1", + "jasmine": "^3.3.1", + "jasmine-core": "^3.3.0", "jasmine-spec-reporter": "^4.2.1", "jasmine-ts-console-reporter": "^3.1.1", - "karma": "^3.0.0", + "karma": "^4.0.1", "karma-chrome-launcher": "^2.2.0", - "karma-cli": "^1.0.1", - "karma-coverage-istanbul-reporter": "^2.0.1", - "karma-detect-browsers": "^2.3.2", + "karma-cli": "^2.0.0", + "karma-coverage-istanbul-reporter": "^2.0.5", + "karma-detect-browsers": "^2.3.3", "karma-edge-launcher": "^0.4.2", "karma-firefox-launcher": "^1.1.0", - "karma-jasmine": "^1.1.2", - "karma-jasmine-html-reporter": "^1.3.1", + "karma-jasmine": "^2.0.1", + "karma-jasmine-html-reporter": "^1.4.0", "karma-safari-launcher": "^1.0.0", - "karma-typescript": "^3.0.13", + "karma-typescript": "^4.0.0", "nodemon": "^1.17.3", "rimraf": "^2.6.2", "tslint": "^5.12.1", diff --git a/tsconfig.json b/tsconfig.json index 9db279a707..6f8a590992 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,5 +19,9 @@ "include": [ "src", "spec" + ], + "exclude": [ + "node_modules", + "dist" ] } From f39bdc4269c1105211ac901b7d2d51978375b222 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 27 Mar 2019 14:46:34 -0400 Subject: [PATCH 0777/1626] fix lint issues --- spec/common/importers/onepassword1PifImporter.spec.ts | 2 +- spec/common/misc/utils.spec.ts | 3 ++- src/angular/components/add-edit.component.ts | 6 ++++-- tslint.json | 1 + 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/spec/common/importers/onepassword1PifImporter.spec.ts b/spec/common/importers/onepassword1PifImporter.spec.ts index 352c3cb651..276f36397a 100644 --- a/spec/common/importers/onepassword1PifImporter.spec.ts +++ b/spec/common/importers/onepassword1PifImporter.spec.ts @@ -71,7 +71,7 @@ const WindowsOpVaultTestData = JSON.stringify({ category: '001', created: 1544823719, hmac: 'NtyBmTTPOb88HV3JUKPx1xl/vcMhac9kvCfe/NtszY0=', - k: 'XC/z20QveYCoV8xQ4tCJZZp/uum77xLkMSMEhlUULndryXgSmkG+VBtkW7AfuerfKc8Rtu43a4Sd078j7XfZTcwUCEKtBECUTDNbEgv4+4hoFVk1EzZgEUy/0bW1Ap+jNLmmdSU9h74+REu6pdxsvQ==', + k: '**REMOVED LONG LINE FOR LINTER** -Kyle', tx: 1553395669, updated: 1553395669, uuid: '528AB076FB5F4FBF960884B8E01619AC', diff --git a/spec/common/misc/utils.spec.ts b/spec/common/misc/utils.spec.ts index 3638b5964c..a224cabfd5 100644 --- a/spec/common/misc/utils.spec.ts +++ b/spec/common/misc/utils.spec.ts @@ -20,7 +20,8 @@ describe('Utils Service', () => { expect(Utils.getDomain('https://bitwarden.com')).toBe('bitwarden.com'); expect(Utils.getDomain('http://bitwarden.com')).toBe('bitwarden.com'); expect(Utils.getDomain('http://vault.bitwarden.com')).toBe('bitwarden.com'); - expect(Utils.getDomain('https://user:password@bitwarden.com:8080/password/sites?and&query#hash')).toBe('bitwarden.com'); + expect(Utils.getDomain('https://user:password@bitwarden.com:8080/password/sites?and&query#hash')) + .toBe('bitwarden.com'); expect(Utils.getDomain('https://bitwarden.unknown')).toBe('bitwarden.unknown'); }); diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 8a4ae00abe..b04bedc1a2 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -1,4 +1,7 @@ -import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { + CdkDragDrop, + moveItemInArray, +} from '@angular/cdk/drag-drop'; import { EventEmitter, Input, @@ -346,7 +349,6 @@ export class AddEditComponent implements OnInit { u.showOptions = u.showOptions == null ? true : u.showOptions; } - drop(event: CdkDragDrop) { moveItemInArray(this.cipher.fields, event.previousIndex, event.currentIndex); } diff --git a/tslint.json b/tslint.json index 7e4320f723..bb9ef3827d 100644 --- a/tslint.json +++ b/tslint.json @@ -1,6 +1,7 @@ { "extends": "tslint:recommended", "rules": { + "interface-name": [false], "align": [ true, "statements", "members" ], "ban-types": { "options": [ From dc37528d223cd12f37297c80e6ad54ab68dfb109 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 29 Mar 2019 00:09:21 -0400 Subject: [PATCH 0778/1626] update jasmine types --- package-lock.json | 19 ++++++++++++------- package.json | 2 +- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0f3b3d4769..7d91135c7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -275,9 +275,9 @@ } }, "@types/jasmine": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.8.tgz", - "integrity": "sha512-OJSUxLaxXsjjhob2DBzqzgrkLmukM3+JMpRp0r0E4HTdT1nwDCWhaswjYxazPij6uOdzHCJfNbDjmQ1/rnNbCg==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.3.12.tgz", + "integrity": "sha512-lXvr2xFQEVQLkIhuGaR3GC1L9lMU1IxeWnAF/wNY5ZWpC4p9dgxkKkzMp7pntpAdv9pZSnYqgsBkCg32MXSZMg==", "dev": true }, "@types/lodash": { @@ -1522,7 +1522,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -1937,7 +1938,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -1993,6 +1995,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2036,12 +2039,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, diff --git a/package.json b/package.json index e2609e4a8d..c7e9eec91c 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@types/commander": "^2.12.2", "@types/form-data": "^2.2.1", "@types/inquirer": "^0.0.43", - "@types/jasmine": "^2.8.8", + "@types/jasmine": "^3.3.12", "@types/lowdb": "^1.0.5", "@types/lunr": "^2.1.6", "@types/node": "^10.9.4", From 0f72df3166f1a5a88dd67110408a3d6578cb3e51 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 1 Apr 2019 13:16:37 -0400 Subject: [PATCH 0779/1626] rename to select copy --- src/angular/directives/flex-copy.directive.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/angular/directives/flex-copy.directive.ts b/src/angular/directives/flex-copy.directive.ts index 6bdf387f7c..e91feaecd9 100644 --- a/src/angular/directives/flex-copy.directive.ts +++ b/src/angular/directives/flex-copy.directive.ts @@ -7,9 +7,9 @@ import { import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; @Directive({ - selector: '[appFlexCopy]', + selector: '[appSelectCopy]', }) -export class FlexCopyDirective { +export class SelectCopyDirective { constructor(private el: ElementRef, private platformUtilsService: PlatformUtilsService) { } @HostListener('copy') onCopy() { From 2ef1b7d65c02f7f204a0bb5a46c28aa093be274d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 1 Apr 2019 13:16:53 -0400 Subject: [PATCH 0780/1626] rename select copy --- .../{flex-copy.directive.ts => select-copy.directive.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/angular/directives/{flex-copy.directive.ts => select-copy.directive.ts} (100%) diff --git a/src/angular/directives/flex-copy.directive.ts b/src/angular/directives/select-copy.directive.ts similarity index 100% rename from src/angular/directives/flex-copy.directive.ts rename to src/angular/directives/select-copy.directive.ts From e979dd4f12634a72ee50989d7dd6a6852fa149fa Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 1 Apr 2019 14:31:42 -0400 Subject: [PATCH 0781/1626] make blob first --- src/services/cipher.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 957feedc46..c737299eac 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -569,8 +569,8 @@ export class CipherService implements CipherServiceAbstraction { const fd = new FormData(); try { - fd.append('key', dataEncKey[1].encryptedString); const blob = new Blob([encData], { type: 'application/octet-stream' }); + fd.append('key', dataEncKey[1].encryptedString); fd.append('data', blob, encFileName.encryptedString); } catch (e) { if (Utils.isNode && !Utils.isBrowser) { @@ -808,8 +808,8 @@ export class CipherService implements CipherServiceAbstraction { const fd = new FormData(); try { - fd.append('key', dataEncKey[1].encryptedString); const blob = new Blob([encData], { type: 'application/octet-stream' }); + fd.append('key', dataEncKey[1].encryptedString); fd.append('data', blob, encFileName.encryptedString); } catch (e) { if (Utils.isNode && !Utils.isBrowser) { From 6ac679355ddfe045e2aca9bb469d5b7e82811371 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 1 Apr 2019 22:36:07 -0400 Subject: [PATCH 0782/1626] a11y title --- .../directives/a11y-title.directive.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/angular/directives/a11y-title.directive.ts diff --git a/src/angular/directives/a11y-title.directive.ts b/src/angular/directives/a11y-title.directive.ts new file mode 100644 index 0000000000..e8d5766935 --- /dev/null +++ b/src/angular/directives/a11y-title.directive.ts @@ -0,0 +1,28 @@ +import { + Directive, + ElementRef, + Input, + Renderer2, +} from '@angular/core'; + +@Directive({ + selector: '[appA11yTitle]', +}) +export class A11yTitleDirective { + @Input() set appA11yTitle(title: string) { + this.title = title; + } + + private title: string; + + constructor(private el: ElementRef, private renderer: Renderer2) { } + + ngOnInit() { + if (!this.el.nativeElement.hasAttribute('title')) { + this.renderer.setAttribute(this.el.nativeElement, 'title', this.title); + } + if (!this.el.nativeElement.hasAttribute('aria-label')) { + this.renderer.setAttribute(this.el.nativeElement, 'aria-label', this.title); + } + } +} From 1044a8759ac3bbd72ccdee9d5a2b12373bb0004f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 1 Apr 2019 23:09:02 -0400 Subject: [PATCH 0783/1626] hide icon --- src/angular/components/icon.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/icon.component.html b/src/angular/components/icon.component.html index fd42b99dde..00d2c591c6 100644 --- a/src/angular/components/icon.component.html +++ b/src/angular/components/icon.component.html @@ -1,4 +1,4 @@ -
+ From e5d6861662967f6cc8ed33d81600cfabd9120fa0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 1 Apr 2019 23:52:13 -0400 Subject: [PATCH 0784/1626] add "Login" suipport for msecure --- src/importers/msecureCsvImporter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/importers/msecureCsvImporter.ts b/src/importers/msecureCsvImporter.ts index 60c4e3252e..d7c14e69cc 100644 --- a/src/importers/msecureCsvImporter.ts +++ b/src/importers/msecureCsvImporter.ts @@ -28,7 +28,7 @@ export class MSecureCsvImporter extends BaseImporter implements Importer { const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(value[2], '--'); - if (value[1] === 'Web Logins') { + if (value[1] === 'Web Logins' || value[1] === 'Login') { cipher.login.uris = this.makeUriArray(value[4]); cipher.login.username = this.getValueOrDefault(value[5]); cipher.login.password = this.getValueOrDefault(value[6]); From c63ff4485eb8a0ff5b4e5c8ef3c851f4ca056646 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 2 Apr 2019 09:02:08 -0400 Subject: [PATCH 0785/1626] modal messages --- src/angular/components/modal.component.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/angular/components/modal.component.ts b/src/angular/components/modal.component.ts index d63c78af7f..d939cac1ee 100644 --- a/src/angular/components/modal.component.ts +++ b/src/angular/components/modal.component.ts @@ -9,6 +9,8 @@ import { ViewContainerRef, } from '@angular/core'; +import { MessagingService } from '../../abstractions/messaging.service'; + @Component({ selector: 'app-modal', template: ``, @@ -22,7 +24,8 @@ export class ModalComponent implements OnDestroy { parentContainer: ViewContainerRef = null; fade: boolean = true; - constructor(protected componentFactoryResolver: ComponentFactoryResolver) { } + constructor(protected componentFactoryResolver: ComponentFactoryResolver, + protected messagingService: MessagingService) { } ngOnDestroy() { document.body.classList.remove('modal-open'); @@ -31,6 +34,7 @@ export class ModalComponent implements OnDestroy { show(type: Type, parentContainer: ViewContainerRef, fade: boolean = true): T { this.onShow.emit(); + this.messagingService.send('modalShow'); this.parentContainer = parentContainer; this.fade = fade; @@ -54,12 +58,15 @@ export class ModalComponent implements OnDestroy { } this.onShown.emit(); + this.messagingService.send('modalShown'); return componentRef.instance; } close() { this.onClose.emit(); + this.messagingService.send('modalClose'); this.onClosed.emit(); + this.messagingService.send('modalClosed'); if (this.parentContainer != null) { this.parentContainer.clear(); } From 0b0245b90fbb99fe677a94a2f97b8245cd255c1f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 3 Apr 2019 14:18:55 -0400 Subject: [PATCH 0786/1626] cache keyHash, proper param order for cipherString --- src/models/domain/cipherString.ts | 6 +++--- src/services/crypto.service.ts | 13 +++++++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/models/domain/cipherString.ts b/src/models/domain/cipherString.ts index 6d49d1724e..3c5bed833c 100644 --- a/src/models/domain/cipherString.ts +++ b/src/models/domain/cipherString.ts @@ -16,11 +16,11 @@ export class CipherString { if (data != null) { // data and header const encType = encryptedStringOrType as EncryptionType; - this.encryptedString = encType + '.' + data; - // iv if (iv != null) { - this.encryptedString += ('|' + iv); + this.encryptedString = encType + '.' + iv + '|' + data; + } else { + this.encryptedString = encType + '.' + data; } // mac diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index b2da1ce532..66f4c810a7 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -96,12 +96,17 @@ export class CryptoService implements CryptoServiceAbstraction { return key == null ? null : this.key; } - getKeyHash(): Promise { + async getKeyHash(): Promise { if (this.keyHash != null) { - return Promise.resolve(this.keyHash); + return this.keyHash; } - return this.storageService.get(Keys.keyHash); + const keyHash = await this.secureStorageService.get(Keys.keyHash); + if (keyHash != null) { + this.keyHash = keyHash; + } + + return keyHash == null ? null : this.keyHash; } @sequentialize(() => 'getEncKey') @@ -380,7 +385,7 @@ export class CryptoService implements CryptoServiceAbstraction { const iv = Utils.fromBufferToB64(encObj.iv); const data = Utils.fromBufferToB64(encObj.data); const mac = encObj.mac != null ? Utils.fromBufferToB64(encObj.mac) : null; - return new CipherString(encObj.key.encType, iv, data, mac); + return new CipherString(encObj.key.encType, data, iv, mac); } async encryptToBytes(plainValue: ArrayBuffer, key?: SymmetricCryptoKey): Promise { From bb6c194eab4fb1d36d8d1e8a7c2c0e5cc5b75f4a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Apr 2019 09:06:05 -0400 Subject: [PATCH 0787/1626] crypto service update --- src/services/crypto.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 66f4c810a7..b411c3fe8f 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -714,7 +714,7 @@ export class CryptoService implements CryptoServiceAbstraction { return phrase; } - private async buildEncKey(key: SymmetricCryptoKey, encKey: ArrayBuffer = null) + private async buildEncKey(key: SymmetricCryptoKey, encKey: ArrayBuffer) : Promise<[SymmetricCryptoKey, CipherString]> { let encKeyEnc: CipherString = null; if (key.key.byteLength === 32) { From 3ec0b7fb5dfb01f995c15ad71795f7211f444973 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 10 Apr 2019 15:39:36 -0400 Subject: [PATCH 0788/1626] isJsonResponse helper --- src/services/api.service.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 8b4acd2bd9..fcff0bf77d 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -166,8 +166,7 @@ export class ApiService implements ApiServiceAbstraction { })); let responseJson: any = null; - const typeHeader = response.headers.get('content-type'); - if (typeHeader != null && typeHeader.indexOf('application/json') > -1) { + if (this.isJsonResponse(response)) { responseJson = await response.json(); } @@ -932,8 +931,7 @@ export class ApiService implements ApiServiceAbstraction { } let responseJson: any = null; - const typeHeader = response.headers.get('content-type'); - if (typeHeader != null && typeHeader.indexOf('application/json') > -1) { + if (this.isJsonResponse(response)) { responseJson = await response.json(); } @@ -1001,4 +999,9 @@ export class ApiService implements ApiServiceAbstraction { } return base; } + + private isJsonResponse(response: Response): boolean { + const typeHeader = response.headers.get('content-type'); + return typeHeader != null && typeHeader.indexOf('application/json') > -1; + } } From 2fa1270f0bea737b07d18652e7c9eac120433a3a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 10 Apr 2019 15:40:40 -0400 Subject: [PATCH 0789/1626] null check optimizations --- src/services/user.service.ts | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 429c6f2169..62ddd0a04a 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -45,47 +45,37 @@ export class UserService implements UserServiceAbstraction { } async getUserId(): Promise { - if (this.userId != null) { - return this.userId; + if (this.userId == null) { + this.userId = await this.storageService.get(Keys.userId); } - - this.userId = await this.storageService.get(Keys.userId); return this.userId; } async getEmail(): Promise { - if (this.email != null) { - return this.email; + if (this.email == null) { + this.email = await this.storageService.get(Keys.userEmail); } - - this.email = await this.storageService.get(Keys.userEmail); return this.email; } async getSecurityStamp(): Promise { - if (this.stamp != null) { - return this.stamp; + if (this.stamp == null) { + this.stamp = await this.storageService.get(Keys.stamp); } - - this.stamp = await this.storageService.get(Keys.stamp); return this.stamp; } async getKdf(): Promise { - if (this.kdf != null) { - return this.kdf; + if (this.kdf == null) { + this.kdf = await this.storageService.get(Keys.kdf); } - - this.kdf = await this.storageService.get(Keys.kdf); return this.kdf; } async getKdfIterations(): Promise { - if (this.kdfIterations != null) { - return this.kdfIterations; + if (this.kdfIterations == null) { + this.kdfIterations = await this.storageService.get(Keys.kdfIterations); } - - this.kdfIterations = await this.storageService.get(Keys.kdfIterations); return this.kdfIterations; } @@ -117,7 +107,7 @@ export class UserService implements UserServiceAbstraction { } async canAccessPremium(): Promise { - const tokenPremium = await this.tokenService.getPremium(); + const tokenPremium = this.tokenService.getPremium(); if (tokenPremium) { return true; } From aa4f811e9eb2352c07a3fd17dd459cef507b799d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 11 Apr 2019 16:32:07 -0400 Subject: [PATCH 0790/1626] use clearCache --- src/services/settings.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/settings.service.ts b/src/services/settings.service.ts index 506bdf01a0..51d970f3c1 100644 --- a/src/services/settings.service.ts +++ b/src/services/settings.service.ts @@ -27,7 +27,7 @@ export class SettingsService implements SettingsServiceAbstraction { async clear(userId: string): Promise { await this.storageService.remove(Keys.settingsPrefix + userId); - this.settingsCache = null; + this.clearCache(); } // Helpers From 1de193eea1a5a564494140e7580d3b09c7b68e3f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 12 Apr 2019 09:45:47 -0400 Subject: [PATCH 0791/1626] use map --- src/models/data/loginData.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/models/data/loginData.ts b/src/models/data/loginData.ts index e3139a8c51..c173c2f162 100644 --- a/src/models/data/loginData.ts +++ b/src/models/data/loginData.ts @@ -20,10 +20,7 @@ export class LoginData { this.totp = data.totp; if (data.uris) { - this.uris = []; - data.uris.forEach((u) => { - this.uris.push(new LoginUriData(u)); - }); + this.uris = data.uris.map((u) => new LoginUriData(u)); } } } From 20fb4d3a391ef8e9457cab98a50de6f5e5a883cf Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 12 Apr 2019 09:50:24 -0400 Subject: [PATCH 0792/1626] no need to just search name on edge any longer --- src/angular/pipes/search-ciphers.pipe.ts | 8 -------- src/services/search.service.ts | 7 +------ 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/angular/pipes/search-ciphers.pipe.ts b/src/angular/pipes/search-ciphers.pipe.ts index e81371c4e8..2b0e442a90 100644 --- a/src/angular/pipes/search-ciphers.pipe.ts +++ b/src/angular/pipes/search-ciphers.pipe.ts @@ -5,20 +5,12 @@ import { import { CipherView } from '../../models/view/cipherView'; -import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; - -import { DeviceType } from '../../enums'; - @Pipe({ name: 'searchCiphers', }) export class SearchCiphersPipe implements PipeTransform { private onlySearchName = false; - constructor(platformUtilsService: PlatformUtilsService) { - this.onlySearchName = platformUtilsService.getDevice() === DeviceType.EdgeExtension; - } - transform(ciphers: CipherView[], searchText: string): CipherView[] { if (ciphers == null || ciphers.length === 0) { return []; diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 550a6ab933..7cc1b91e60 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -3,11 +3,9 @@ import * as lunr from 'lunr'; import { CipherView } from '../models/view/cipherView'; import { CipherService } from '../abstractions/cipher.service'; -import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { SearchService as SearchServiceAbstraction } from '../abstractions/search.service'; import { CipherType } from '../enums/cipherType'; -import { DeviceType } from '../enums/deviceType'; import { FieldType } from '../enums/fieldType'; import { UriMatchType } from '../enums/uriMatchType'; @@ -16,10 +14,7 @@ export class SearchService implements SearchServiceAbstraction { private index: lunr.Index = null; private onlySearchName = false; - constructor(private cipherService: CipherService, platformUtilsService: PlatformUtilsService) { - this.onlySearchName = platformUtilsService == null || - platformUtilsService.getDevice() === DeviceType.EdgeExtension; - } + constructor(private cipherService: CipherService) { } clearIndex(): void { this.index = null; From 45cb346be1f0af1f7046312f2665e581298ebf47 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 12 Apr 2019 09:56:15 -0400 Subject: [PATCH 0793/1626] Revert "no need to just search name on edge any longer" This reverts commit 20fb4d3a391ef8e9457cab98a50de6f5e5a883cf. --- src/angular/pipes/search-ciphers.pipe.ts | 8 ++++++++ src/services/search.service.ts | 7 ++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/angular/pipes/search-ciphers.pipe.ts b/src/angular/pipes/search-ciphers.pipe.ts index 2b0e442a90..e81371c4e8 100644 --- a/src/angular/pipes/search-ciphers.pipe.ts +++ b/src/angular/pipes/search-ciphers.pipe.ts @@ -5,12 +5,20 @@ import { import { CipherView } from '../../models/view/cipherView'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +import { DeviceType } from '../../enums'; + @Pipe({ name: 'searchCiphers', }) export class SearchCiphersPipe implements PipeTransform { private onlySearchName = false; + constructor(platformUtilsService: PlatformUtilsService) { + this.onlySearchName = platformUtilsService.getDevice() === DeviceType.EdgeExtension; + } + transform(ciphers: CipherView[], searchText: string): CipherView[] { if (ciphers == null || ciphers.length === 0) { return []; diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 7cc1b91e60..550a6ab933 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -3,9 +3,11 @@ import * as lunr from 'lunr'; import { CipherView } from '../models/view/cipherView'; import { CipherService } from '../abstractions/cipher.service'; +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { SearchService as SearchServiceAbstraction } from '../abstractions/search.service'; import { CipherType } from '../enums/cipherType'; +import { DeviceType } from '../enums/deviceType'; import { FieldType } from '../enums/fieldType'; import { UriMatchType } from '../enums/uriMatchType'; @@ -14,7 +16,10 @@ export class SearchService implements SearchServiceAbstraction { private index: lunr.Index = null; private onlySearchName = false; - constructor(private cipherService: CipherService) { } + constructor(private cipherService: CipherService, platformUtilsService: PlatformUtilsService) { + this.onlySearchName = platformUtilsService == null || + platformUtilsService.getDevice() === DeviceType.EdgeExtension; + } clearIndex(): void { this.index = null; From 42771c1a2d3110fb4bb6ecb4337ad4ee75bd6305 Mon Sep 17 00:00:00 2001 From: Antoni Sobkowicz Date: Sun, 14 Apr 2019 02:11:32 +0200 Subject: [PATCH 0794/1626] prettier ui on macOS (titleBarStyle set to hiddenInset). Requires changes from desktop part of bitwarden client. (#37) --- src/electron/window.main.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index db1d7c0e11..0c4ab9515f 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -94,6 +94,7 @@ export class WindowMain { y: this.windowStates[Keys.mainWindowSize].y, title: app.getName(), icon: process.platform === 'linux' ? path.join(__dirname, '/images/icon.png') : undefined, + titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : undefined, show: false, }); From b7a736294b97f312f64fd5f997e17f75e9819ed4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 13 Apr 2019 21:20:04 -0400 Subject: [PATCH 0795/1626] improved some syntax and a few fixes --- src/models/data/attachmentData.ts | 2 +- src/models/data/cipherData.ts | 24 +++-------------- src/models/domain/attachment.ts | 3 ++- src/models/domain/cipher.ts | 32 +++++------------------ src/models/domain/password.ts | 5 ++-- src/models/response/attachmentResponse.ts | 2 +- src/models/view/attachmentView.ts | 2 +- 7 files changed, 17 insertions(+), 53 deletions(-) diff --git a/src/models/data/attachmentData.ts b/src/models/data/attachmentData.ts index 004376a8dd..dc0f222065 100644 --- a/src/models/data/attachmentData.ts +++ b/src/models/data/attachmentData.ts @@ -5,7 +5,7 @@ export class AttachmentData { url: string; fileName: string; key: string; - size: number; + size: string; sizeName: string; constructor(response?: AttachmentResponse) { diff --git a/src/models/data/cipherData.ts b/src/models/data/cipherData.ts index 84dece2e81..19478debad 100644 --- a/src/models/data/cipherData.ts +++ b/src/models/data/cipherData.ts @@ -48,12 +48,7 @@ export class CipherData { this.type = response.type; this.name = response.name; this.notes = response.notes; - - if (collectionIds != null) { - this.collectionIds = collectionIds; - } else { - this.collectionIds = response.collectionIds; - } + this.collectionIds = collectionIds != null ? collectionIds : response.collectionIds; switch (this.type) { case CipherType.Login: @@ -73,24 +68,13 @@ export class CipherData { } if (response.fields != null) { - this.fields = []; - response.fields.forEach((field) => { - this.fields.push(new FieldData(field)); - }); + this.fields = response.fields.map((f) => new FieldData(f)); } - if (response.attachments != null) { - this.attachments = []; - response.attachments.forEach((attachment) => { - this.attachments.push(new AttachmentData(attachment)); - }); + this.attachments = response.attachments.map((a) => new AttachmentData(a)); } - if (response.passwordHistory != null) { - this.passwordHistory = []; - response.passwordHistory.forEach((ph) => { - this.passwordHistory.push(new PasswordHistoryData(ph)); - }); + this.passwordHistory = response.passwordHistory.map((ph) => new PasswordHistoryData(ph)); } } } diff --git a/src/models/domain/attachment.ts b/src/models/domain/attachment.ts index 6db86bebdd..838b83b9e6 100644 --- a/src/models/domain/attachment.ts +++ b/src/models/domain/attachment.ts @@ -13,7 +13,7 @@ import { Utils } from '../../misc/utils'; export class Attachment extends Domain { id: string; url: string; - size: number; + size: string; sizeName: string; key: CipherString; fileName: CipherString; @@ -62,6 +62,7 @@ export class Attachment extends Domain { toAttachmentData(): AttachmentData { const a = new AttachmentData(); + a.size = this.size; this.buildDataModel(this, a, { id: null, url: null, diff --git a/src/models/domain/cipher.ts b/src/models/domain/cipher.ts index 708774456c..85a961dd8f 100644 --- a/src/models/domain/cipher.ts +++ b/src/models/domain/cipher.ts @@ -76,28 +76,19 @@ export class Cipher extends Domain { } if (obj.attachments != null) { - this.attachments = []; - obj.attachments.forEach((attachment) => { - this.attachments.push(new Attachment(attachment, alreadyEncrypted)); - }); + this.attachments = obj.attachments.map((a) => new Attachment(a, alreadyEncrypted)); } else { this.attachments = null; } if (obj.fields != null) { - this.fields = []; - obj.fields.forEach((field) => { - this.fields.push(new Field(field, alreadyEncrypted)); - }); + this.fields = obj.fields.map((f) => new Field(f, alreadyEncrypted)); } else { this.fields = null; } if (obj.passwordHistory != null) { - this.passwordHistory = []; - obj.passwordHistory.forEach((ph) => { - this.passwordHistory.push(new Password(ph, alreadyEncrypted)); - }); + this.passwordHistory = obj.passwordHistory.map((ph) => new Password(ph, alreadyEncrypted)); } else { this.passwordHistory = null; } @@ -205,24 +196,13 @@ export class Cipher extends Domain { } if (this.fields != null) { - c.fields = []; - this.fields.forEach((field) => { - c.fields.push(field.toFieldData()); - }); + c.fields = this.fields.map((f) => f.toFieldData()); } - if (this.attachments != null) { - c.attachments = []; - this.attachments.forEach((attachment) => { - c.attachments.push(attachment.toAttachmentData()); - }); + c.attachments = this.attachments.map((a) => a.toAttachmentData()); } - if (this.passwordHistory != null) { - c.passwordHistory = []; - this.passwordHistory.forEach((ph) => { - c.passwordHistory.push(ph.toPasswordHistoryData()); - }); + c.passwordHistory = this.passwordHistory.map((ph) => ph.toPasswordHistoryData()); } return c; } diff --git a/src/models/domain/password.ts b/src/models/domain/password.ts index 5ef38a2010..72c374b6f8 100644 --- a/src/models/domain/password.ts +++ b/src/models/domain/password.ts @@ -21,11 +21,10 @@ export class Password extends Domain { this.lastUsedDate = new Date(obj.lastUsedDate); } - async decrypt(orgId: string): Promise { - const view = await this.decryptObj(new PasswordHistoryView(this), { + decrypt(orgId: string): Promise { + return this.decryptObj(new PasswordHistoryView(this), { password: null, }, orgId); - return view; } toPasswordHistoryData(): PasswordHistoryData { diff --git a/src/models/response/attachmentResponse.ts b/src/models/response/attachmentResponse.ts index 241fba02c2..47c01cadd1 100644 --- a/src/models/response/attachmentResponse.ts +++ b/src/models/response/attachmentResponse.ts @@ -5,7 +5,7 @@ export class AttachmentResponse extends BaseResponse { url: string; fileName: string; key: string; - size: number; + size: string; sizeName: string; constructor(response: any) { diff --git a/src/models/view/attachmentView.ts b/src/models/view/attachmentView.ts index 43f0716763..96bd42a7e1 100644 --- a/src/models/view/attachmentView.ts +++ b/src/models/view/attachmentView.ts @@ -6,7 +6,7 @@ import { SymmetricCryptoKey } from '../domain/symmetricCryptoKey'; export class AttachmentView implements View { id: string = null; url: string = null; - size: number = null; + size: string = null; sizeName: string = null; fileName: string = null; key: SymmetricCryptoKey = null; From 3d958279d219f0bd76c2fc4c0558c1f1a6e4fc5e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 13 Apr 2019 21:26:16 -0400 Subject: [PATCH 0796/1626] fix keyhash storage --- src/services/crypto.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index b411c3fe8f..dae82f5be9 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -101,7 +101,7 @@ export class CryptoService implements CryptoServiceAbstraction { return this.keyHash; } - const keyHash = await this.secureStorageService.get(Keys.keyHash); + const keyHash = await this.storageService.get(Keys.keyHash); if (keyHash != null) { this.keyHash = keyHash; } From 37d9afc58bf4a27b9bafe5c485d6a1f9fb06f91b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 13 Apr 2019 21:37:35 -0400 Subject: [PATCH 0797/1626] hideTitleBar param --- src/electron/window.main.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index 0c4ab9515f..cb6d5d70df 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -18,7 +18,8 @@ export class WindowMain { private windowStateChangeTimer: NodeJS.Timer; private windowStates: { [key: string]: any; } = {}; - constructor(private storageService: StorageService, private defaultWidth = 950, private defaultHeight = 600) { } + constructor(private storageService: StorageService, private hideTitleBar = false, + private defaultWidth = 950, private defaultHeight = 600) { } init(): Promise { return new Promise((resolve, reject) => { @@ -94,7 +95,7 @@ export class WindowMain { y: this.windowStates[Keys.mainWindowSize].y, title: app.getName(), icon: process.platform === 'linux' ? path.join(__dirname, '/images/icon.png') : undefined, - titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : undefined, + titleBarStyle: this.hideTitleBar && process.platform === 'darwin' ? 'hiddenInset' : undefined, show: false, }); From 05dde1051cedfc511fa75f73bb87413c2c7076f6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 15 Apr 2019 08:43:18 -0400 Subject: [PATCH 0798/1626] decrypt optimization --- src/models/domain/cipherString.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/domain/cipherString.ts b/src/models/domain/cipherString.ts index 3c5bed833c..703945bd5b 100644 --- a/src/models/domain/cipherString.ts +++ b/src/models/domain/cipherString.ts @@ -90,8 +90,8 @@ export class CipherString { } async decrypt(orgId: string): Promise { - if (this.decryptedValue) { - return Promise.resolve(this.decryptedValue); + if (this.decryptedValue != null) { + return this.decryptedValue; } let cryptoService: CryptoService; From d3a2dfe2e8ea27662aaf189e155d81681ced0d8e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 15 Apr 2019 21:15:35 -0400 Subject: [PATCH 0799/1626] null checks --- src/services/cipher.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index c737299eac..3c86989d0f 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -112,8 +112,8 @@ export class CipherService implements CipherServiceAbstraction { const hiddenFields = model.fields == null ? [] : model.fields.filter((f) => f.type === FieldType.Hidden && f.name != null && f.name !== ''); existingHiddenFields.forEach((ef) => { - const matchedField = hiddenFields.filter((f) => f.name === ef.name); - if (matchedField.length === 0 || matchedField[0].value !== ef.value) { + const matchedField = hiddenFields.find((f) => f.name === ef.name); + if (matchedField == null || matchedField.value !== ef.value) { const ph = new PasswordHistoryView(); ph.password = ef.name + ': ' + ef.value; ph.lastUsedDate = new Date(); @@ -320,7 +320,7 @@ export class CipherService implements CipherServiceAbstraction { } async getAllDecryptedForUrl(url: string, includeOtherTypes?: CipherType[]): Promise { - if (url == null && !includeOtherTypes) { + if (url == null && includeOtherTypes == null) { return Promise.resolve([]); } @@ -351,7 +351,7 @@ export class CipherService implements CipherServiceAbstraction { } return ciphers.filter((cipher) => { - if (includeOtherTypes && includeOtherTypes.indexOf(cipher.type) > -1) { + if (includeOtherTypes != null && includeOtherTypes.indexOf(cipher.type) > -1) { return true; } From 39c1384a98d02519bccd3185ec0384b73668bca8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 15 Apr 2019 23:02:14 -0400 Subject: [PATCH 0800/1626] processingNotes takes remainder of row lines --- src/importers/lastpassCsvImporter.ts | 34 ++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/importers/lastpassCsvImporter.ts b/src/importers/lastpassCsvImporter.ts index 0c017fb856..a068d6349a 100644 --- a/src/importers/lastpassCsvImporter.ts +++ b/src/importers/lastpassCsvImporter.ts @@ -210,31 +210,37 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { let notes: string = null; const dataObj: any = {}; + let processingNotes = false; extraParts.forEach((extraPart) => { - if (this.isNullOrWhitespace(extraPart)) { - return; - } let key: string = null; let val: string = null; - const colonIndex = extraPart.indexOf(':'); - if (colonIndex === -1) { - key = extraPart; - } else { - key = extraPart.substring(0, colonIndex); - if (extraPart.length > colonIndex) { - val = extraPart.substring(colonIndex + 1); + if (!processingNotes) { + if (this.isNullOrWhitespace(extraPart)) { + return; + } + const colonIndex = extraPart.indexOf(':'); + if (colonIndex === -1) { + key = extraPart; + } else { + key = extraPart.substring(0, colonIndex); + if (extraPart.length > colonIndex) { + val = extraPart.substring(colonIndex + 1); + } + } + if (this.isNullOrWhitespace(key) || this.isNullOrWhitespace(val) || key === 'NoteType') { + return; } } - if (this.isNullOrWhitespace(key) || this.isNullOrWhitespace(val) || key === 'NoteType') { - return; - } - if (key === 'Notes') { + if (processingNotes) { + notes += ('\n' + extraPart); + } else if (key === 'Notes') { if (!this.isNullOrWhitespace(notes)) { notes += ('\n' + val); } else { notes = val; } + processingNotes = true; } else if (map.hasOwnProperty(key)) { dataObj[map[key]] = val; } else { From 7252e7cad08bd7a70ad414361ac7785d873b130f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 16 Apr 2019 23:32:02 -0400 Subject: [PATCH 0801/1626] updates --- src/models/response/globalDomainResponse.ts | 2 +- src/models/view/loginUriView.ts | 2 ++ src/services/cipher.service.ts | 7 ++++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/models/response/globalDomainResponse.ts b/src/models/response/globalDomainResponse.ts index 5f37c7e625..861282a92a 100644 --- a/src/models/response/globalDomainResponse.ts +++ b/src/models/response/globalDomainResponse.ts @@ -3,7 +3,7 @@ import { BaseResponse } from './baseResponse'; export class GlobalDomainResponse extends BaseResponse { type: number; domains: string[]; - excluded: number[]; + excluded: boolean; constructor(response: any) { super(response); diff --git a/src/models/view/loginUriView.ts b/src/models/view/loginUriView.ts index 1d51561c11..4e171ad967 100644 --- a/src/models/view/loginUriView.ts +++ b/src/models/view/loginUriView.ts @@ -15,6 +15,8 @@ const CanLaunchWhitelist = [ 'irc://', 'vnc://', 'chrome://', + 'iosapp://', + 'androidapp://', ]; export class LoginUriView implements View { diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 3c86989d0f..87bd575d27 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -598,7 +598,7 @@ export class CipherService implements CipherServiceAbstraction { const userId = await this.userService.getUserId(); const cData = new CipherData(response, userId, cipher.collectionIds); if (!admin) { - this.upsert(cData); + await this.upsert(cData); } return new Cipher(cData); } @@ -728,14 +728,15 @@ export class CipherService implements CipherServiceAbstraction { 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) { + const bothNotNull = aLastUsed != null && bLastUsed != null; + if (bothNotNull && aLastUsed < bLastUsed) { return 1; } if (aLastUsed != null && bLastUsed == null) { return -1; } - if (bLastUsed != null && aLastUsed != null && aLastUsed > bLastUsed) { + if (bothNotNull && aLastUsed > bLastUsed) { return -1; } if (bLastUsed != null && aLastUsed == null) { From 7b0bc41f8872562cb921f5304438c2675895aacf Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 17 Apr 2019 11:06:12 -0400 Subject: [PATCH 0802/1626] remove public stamp removal --- src/services/sync.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index 405322228b..3ea2c1db6b 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -246,7 +246,6 @@ export class SyncService implements SyncServiceAbstraction { await this.cryptoService.setEncPrivateKey(response.privateKey); await this.cryptoService.setOrgKeys(response.organizations); await this.userService.setSecurityStamp(response.securityStamp); - await this.userService.setSecurityStamp(response.securityStamp); const organizations: { [id: string]: OrganizationData; } = {}; response.organizations.forEach((o) => { From 30a1e27aa628d9c56f8c0ca8dfd33e37474f8c33 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 17 Apr 2019 12:13:04 -0400 Subject: [PATCH 0803/1626] fix sync bug --- src/services/sync.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index 3ea2c1db6b..6db32a6e6f 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -160,7 +160,7 @@ export class SyncService implements SyncServiceAbstraction { const collections = await this.collectionService.getAll(); if (collections != null) { for (let i = 0; i < collections.length; i++) { - if (notification.collectionIds.indexOf(collections[i].id)) { + if (notification.collectionIds.indexOf(collections[i].id) > -1) { shouldUpdate = true; break; } From 7ec8d3a3b53e1ac158586a19fcd2b32015e8c6ab Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 18 Apr 2019 09:47:00 -0400 Subject: [PATCH 0804/1626] missing await --- src/services/auth.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index d1336ed421..0a4ab62e6f 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -252,7 +252,7 @@ export class AuthService { const tokenResponse = response as IdentityTokenResponse; if (tokenResponse.twoFactorToken != null) { - this.tokenService.setTwoFactorToken(tokenResponse.twoFactorToken, email); + await this.tokenService.setTwoFactorToken(tokenResponse.twoFactorToken, email); } await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken); From bc43c68eb98799c8b9e30a1dd8f60431dc1236c6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 18 Apr 2019 10:06:19 -0400 Subject: [PATCH 0805/1626] auth guard auth blocked message --- src/angular/services/auth-guard.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/services/auth-guard.service.ts b/src/angular/services/auth-guard.service.ts index 9b6ebb263e..d8efd78b77 100644 --- a/src/angular/services/auth-guard.service.ts +++ b/src/angular/services/auth-guard.service.ts @@ -18,7 +18,7 @@ export class AuthGuardService implements CanActivate { async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) { const isAuthed = await this.userService.isAuthenticated(); if (!isAuthed) { - this.messagingService.send('logout'); + this.messagingService.send('authBlocked'); return false; } From 8edc99dfd13c75ad9faaf790f221c6b9739ab88a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 19 Apr 2019 17:41:33 -0400 Subject: [PATCH 0806/1626] dont sequentialize in throttle spec --- spec/common/misc/throttle.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/common/misc/throttle.spec.ts b/spec/common/misc/throttle.spec.ts index f7a273a6f9..d35d13f9e1 100644 --- a/spec/common/misc/throttle.spec.ts +++ b/spec/common/misc/throttle.spec.ts @@ -94,7 +94,6 @@ class Foo { }); } - @sequentialize((args) => 'qux' + args[0]) @throttle(1, () => 'qux') qux(a: number) { this.calls++; From a0a1142f1f3152aea2abf3ca51feffa21e60e9a9 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 19 Apr 2019 19:46:48 -0400 Subject: [PATCH 0807/1626] Revert "dont sequentialize in throttle spec" This reverts commit 8edc99dfd13c75ad9faaf790f221c6b9739ab88a. --- spec/common/misc/throttle.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/common/misc/throttle.spec.ts b/spec/common/misc/throttle.spec.ts index d35d13f9e1..f7a273a6f9 100644 --- a/spec/common/misc/throttle.spec.ts +++ b/spec/common/misc/throttle.spec.ts @@ -94,6 +94,7 @@ class Foo { }); } + @sequentialize((args) => 'qux' + args[0]) @throttle(1, () => 'qux') qux(a: number) { this.calls++; From 61723f74d26ae933a5d8555059811af5450fa982 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 26 Apr 2019 20:51:53 -0400 Subject: [PATCH 0808/1626] password wallet txt importer --- src/importers/passwordWalletTxtImporter.ts | 38 ++++++++++++++++++++++ src/services/import.service.ts | 4 +++ 2 files changed, 42 insertions(+) create mode 100644 src/importers/passwordWalletTxtImporter.ts diff --git a/src/importers/passwordWalletTxtImporter.ts b/src/importers/passwordWalletTxtImporter.ts new file mode 100644 index 0000000000..0e691bc610 --- /dev/null +++ b/src/importers/passwordWalletTxtImporter.ts @@ -0,0 +1,38 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +export class PasswordWalletTxtImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, false); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + if (value.length < 6) { + return; + } + + this.processFolder(result, value[5]); + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(value[0], '--'); + cipher.notes = this.getValueOrDefault(value[4]); + cipher.login.username = this.getValueOrDefault(value[2]); + cipher.login.password = this.getValueOrDefault(value[3]); + cipher.login.uris = this.makeUriArray(value[1]); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + if (this.organization) { + this.moveFoldersToCollections(result); + } + + result.success = true; + return result; + } +} diff --git a/src/services/import.service.ts b/src/services/import.service.ts index 7c66014e32..5d69fde20a 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -55,6 +55,7 @@ import { PasswordAgentCsvImporter } from '../importers/passwordAgentCsvImporter' import { PasswordBossJsonImporter } from '../importers/passwordBossJsonImporter'; import { PasswordDragonXmlImporter } from '../importers/passwordDragonXmlImporter'; import { PasswordSafeXmlImporter } from '../importers/passwordSafeXmlImporter'; +import { PasswordWalletTxtImporter } from '../importers/passwordWalletTxtImporter'; import { RememBearCsvImporter } from '../importers/rememBearCsvImporter'; import { RoboFormCsvImporter } from '../importers/roboformCsvImporter'; import { SafeInCloudXmlImporter } from '../importers/safeInCloudXmlImporter'; @@ -113,6 +114,7 @@ export class ImportService implements ImportServiceAbstraction { { id: 'fsecurefsk', name: 'F-Secure KEY (fsk)' }, { id: 'kasperskytxt', name: 'Kaspersky Password Manager (txt)' }, { id: 'remembearcsv', name: 'RememBear (csv)' }, + { id: 'passwordwallettxt', name: 'PasswordWallet (txt)' }, ]; constructor(private cipherService: CipherService, private folderService: FolderService, @@ -243,6 +245,8 @@ export class ImportService implements ImportServiceAbstraction { return new KasperskyTxtImporter(); case 'remembearcsv': return new RememBearCsvImporter(); + case 'passwordwallettxt': + return new PasswordWalletTxtImporter(); default: return null; } From 520f20d428b09acdea40edda1e64224e99e65021 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 26 Apr 2019 21:00:17 -0400 Subject: [PATCH 0809/1626] fixes for password wallet --- src/importers/passwordWalletTxtImporter.ts | 23 +++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/importers/passwordWalletTxtImporter.ts b/src/importers/passwordWalletTxtImporter.ts index 0e691bc610..c98459f354 100644 --- a/src/importers/passwordWalletTxtImporter.ts +++ b/src/importers/passwordWalletTxtImporter.ts @@ -13,17 +13,26 @@ export class PasswordWalletTxtImporter extends BaseImporter implements Importer } results.forEach((value) => { - if (value.length < 6) { + if (value.length < 1) { return; } - - this.processFolder(result, value[5]); + if (value.length > 5) { + this.processFolder(result, value[5]); + } const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(value[0], '--'); - cipher.notes = this.getValueOrDefault(value[4]); - cipher.login.username = this.getValueOrDefault(value[2]); - cipher.login.password = this.getValueOrDefault(value[3]); - cipher.login.uris = this.makeUriArray(value[1]); + if (value.length > 4) { + cipher.notes = this.getValueOrDefault(value[4], '').split('¬').join('\n'); + } + if (value.length > 2) { + cipher.login.username = this.getValueOrDefault(value[2]); + } + if (value.length > 3) { + cipher.login.password = this.getValueOrDefault(value[3]); + } + if (value.length > 1) { + cipher.login.uris = this.makeUriArray(value[1]); + } this.cleanupCipher(cipher); result.ciphers.push(cipher); }); From 169be0f8da168e368e2ad81c1822f9f08e39df45 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 29 Apr 2019 09:30:00 -0400 Subject: [PATCH 0810/1626] should be inverse --- src/models/view/identityView.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/models/view/identityView.ts b/src/models/view/identityView.ts index b6adae4d69..ff5449d12d 100644 --- a/src/models/view/identityView.ts +++ b/src/models/view/identityView.ts @@ -88,14 +88,14 @@ export class IdentityView implements View { get fullAddress(): string { let address = this.address1; - if (Utils.isNullOrWhitespace(this.address2)) { - if (Utils.isNullOrWhitespace(address)) { + if (!Utils.isNullOrWhitespace(this.address2)) { + if (!Utils.isNullOrWhitespace(address)) { address += ', '; } address += this.address2; } - if (Utils.isNullOrWhitespace(this.address3)) { - if (Utils.isNullOrWhitespace(address)) { + if (!Utils.isNullOrWhitespace(this.address3)) { + if (!Utils.isNullOrWhitespace(address)) { address += ', '; } address += this.address3; From 4d57221daa405b23766b63c9bd76c9147897e556 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 29 Apr 2019 10:13:32 -0400 Subject: [PATCH 0811/1626] full address part 2 --- src/models/view/identityView.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/models/view/identityView.ts b/src/models/view/identityView.ts index ff5449d12d..65fdbcde0e 100644 --- a/src/models/view/identityView.ts +++ b/src/models/view/identityView.ts @@ -102,4 +102,14 @@ export class IdentityView implements View { } return address; } + + get fullAddressPart2(): string { + if (this.city == null && this.state == null && this.postalCode == null) { + return null; + } + const city = this.city || '-'; + const state = this.state || '-'; + const postalCode = this.postalCode || '-'; + return city + ', ' + state + ', ' + postalCode; + } } From c300b6102f8d3d9c2e048f816a8dec80b64ee05a Mon Sep 17 00:00:00 2001 From: setyb <32158528+setyb@users.noreply.github.com> Date: Mon, 29 Apr 2019 10:52:33 -0700 Subject: [PATCH 0812/1626] Add capital letter "I" to ambiguous characters (#39) See https://community.bitwarden.com/t/add-capital-letter-i-to-ambiguous-characters/5810 --- src/services/passwordGeneration.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 3e5dcd1895..b5c74822cd 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -110,9 +110,9 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr allCharSet += lowercaseCharSet; } - let uppercaseCharSet = 'ABCDEFGHIJKLMNPQRSTUVWXYZ'; + let uppercaseCharSet = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; if (o.ambiguous) { - uppercaseCharSet += 'O'; + uppercaseCharSet += 'IO'; } if (o.uppercase) { allCharSet += uppercaseCharSet; From ebedf215d23475649b861457149db688af9ef80b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 1 May 2019 10:35:52 -0400 Subject: [PATCH 0813/1626] file size prop --- src/models/view/attachmentView.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/models/view/attachmentView.ts b/src/models/view/attachmentView.ts index 96bd42a7e1..65c896eaa1 100644 --- a/src/models/view/attachmentView.ts +++ b/src/models/view/attachmentView.ts @@ -21,4 +21,13 @@ export class AttachmentView implements View { this.size = a.size; this.sizeName = a.sizeName; } + + get fileSize(): number { + try { + if (this.size != null) { + return parseInt(this.size, null); + } + } catch { } + return 0; + } } From b0b6ad4ebda9adc7f0ad403c7c24648ec9887c0b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 6 May 2019 21:32:05 -0400 Subject: [PATCH 0814/1626] overwriteExisting on org user import --- src/models/request/importDirectoryRequest.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/models/request/importDirectoryRequest.ts b/src/models/request/importDirectoryRequest.ts index cd6eb06309..1fd717bc8f 100644 --- a/src/models/request/importDirectoryRequest.ts +++ b/src/models/request/importDirectoryRequest.ts @@ -4,4 +4,5 @@ import { ImportDirectoryRequestUser } from './importDirectoryRequestUser'; export class ImportDirectoryRequest { groups: ImportDirectoryRequestGroup[] = []; users: ImportDirectoryRequestUser[] = []; + overwriteExisting = false; } From 7b1ffbbcc90af7cb73cb49827d5dc8d16cd5098d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 10 May 2019 14:10:28 -0400 Subject: [PATCH 0815/1626] share page optimizations --- src/angular/components/share.component.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/angular/components/share.component.ts b/src/angular/components/share.component.ts index d0c7dea4fa..4c656a5b13 100644 --- a/src/angular/components/share.component.ts +++ b/src/angular/components/share.component.ts @@ -5,6 +5,8 @@ import { Output, } from '@angular/core'; +import { OrganizationUserStatusType } from '../../enums/organizationUserStatusType'; + import { CipherService } from '../../abstractions/cipher.service'; import { CollectionService } from '../../abstractions/collection.service'; import { I18nService } from '../../abstractions/i18n.service'; @@ -39,10 +41,10 @@ export class ShareComponent implements OnInit { async load() { const allCollections = await this.collectionService.getAllDecrypted(); - this.writeableCollections = allCollections.map((c) => c).filter((c) => !c.readOnly) - .sort(Utils.getSortFunction(this.i18nService, 'name')); + this.writeableCollections = allCollections.map((c) => c).filter((c) => !c.readOnly); const orgs = await this.userService.getAllOrganizations(); - this.organizations = orgs.sort(Utils.getSortFunction(this.i18nService, 'name')); + this.organizations = orgs.sort(Utils.getSortFunction(this.i18nService, 'name')) + .filter((o) => o.enabled && o.status === OrganizationUserStatusType.Confirmed); const cipherDomain = await this.cipherService.get(this.cipherId); this.cipher = await cipherDomain.decrypt(); @@ -61,7 +63,7 @@ export class ShareComponent implements OnInit { } } - async submit() { + async submit(): Promise { const cipherDomain = await this.cipherService.get(this.cipherId); const cipherView = await cipherDomain.decrypt(); @@ -74,7 +76,9 @@ export class ShareComponent implements OnInit { this.platformUtilsService.showToast('success', null, this.i18nService.t('sharedItem')); }); await this.formPromise; + return true; } catch { } + return false; } get canSave() { From 741e060d999c7b40e98b5b2b8afab4e43d59afcb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 11 May 2019 20:38:07 -0400 Subject: [PATCH 0816/1626] fix for empty password history --- src/importers/onepassword1PifImporter.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/importers/onepassword1PifImporter.ts b/src/importers/onepassword1PifImporter.ts index eb2d4d532e..3f2420a185 100644 --- a/src/importers/onepassword1PifImporter.ts +++ b/src/importers/onepassword1PifImporter.ts @@ -140,12 +140,16 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { private parsePasswordHistory(items: any[], cipher: CipherView) { const maxSize = items.length > 5 ? 5 : items.length; - cipher.passwordHistory = items.sort((a, b) => b.time - a.time).slice(0, maxSize).map((entry: any) => { - const ph = new PasswordHistoryView(); - ph.password = entry.value; - ph.lastUsedDate = new Date(entry.time * 1000); - return ph; - }); + cipher.passwordHistory = items + .filter((h: any) => !this.isNullOrWhitespace(h.value) && h.time != null) + .sort((a, b) => b.time - a.time) + .slice(0, maxSize) + .map((h: any) => { + const ph = new PasswordHistoryView(); + ph.password = h.value; + ph.lastUsedDate = new Date(h.time * 1000); + return ph; + }); } private parseFields(fields: any[], cipher: CipherView, designationKey: string, valueKey: string, nameKey: string) { From 1bcd43088410a19ecc4ebed9cfcc6f6d16a8a916 Mon Sep 17 00:00:00 2001 From: Michael Honan Date: Thu, 16 May 2019 12:44:25 +1000 Subject: [PATCH 0817/1626] MacOS: Closing with red button won't open window again through tray icon (#40) * Fixed issue on MacOS where closing BW via the red button then reopening using tray icon wouldn't work * Added MacOS only condition to the window recreation of the toggleWindow method. Made createWindow public in WindowMain. --- src/electron/tray.main.ts | 19 ++++++++++++------- src/electron/window.main.ts | 2 +- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/electron/tray.main.ts b/src/electron/tray.main.ts index 287d233303..7645cf6af7 100644 --- a/src/electron/tray.main.ts +++ b/src/electron/tray.main.ts @@ -121,14 +121,19 @@ export class TrayMain { } private toggleWindow() { - if (this.windowMain.win == null) { - return; - } - - if (this.windowMain.win.isVisible()) { - this.windowMain.win.hide(); + if (this.windowMain.win === null) { + if (process.platform === 'darwin') { + // On MacOS, closing the window via the red button destroys the BrowserWindow instance. + this.windowMain.createWindow().then(() => { + this.windowMain.win.show(); + }); + } } else { - this.windowMain.win.show(); + if (this.windowMain.win.isVisible()) { + this.windowMain.win.hide(); + } else { + this.windowMain.win.show(); + } } } diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index cb6d5d70df..865ea81368 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -81,7 +81,7 @@ export class WindowMain { }); } - private async createWindow() { + async createWindow(): Promise { this.windowStates[Keys.mainWindowSize] = await this.getWindowState(Keys.mainWindowSize, this.defaultWidth, this.defaultHeight); From f76702bb44e7b9b504a74cc1535005aac1a0454a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 15 May 2019 22:47:58 -0400 Subject: [PATCH 0818/1626] optimize if blocks --- src/electron/tray.main.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/electron/tray.main.ts b/src/electron/tray.main.ts index 7645cf6af7..e7418e06d9 100644 --- a/src/electron/tray.main.ts +++ b/src/electron/tray.main.ts @@ -121,19 +121,19 @@ export class TrayMain { } private toggleWindow() { - if (this.windowMain.win === null) { + if (this.windowMain.win == null) { if (process.platform === 'darwin') { // On MacOS, closing the window via the red button destroys the BrowserWindow instance. this.windowMain.createWindow().then(() => { this.windowMain.win.show(); }); } + return; + } + if (this.windowMain.win.isVisible()) { + this.windowMain.win.hide(); } else { - if (this.windowMain.win.isVisible()) { - this.windowMain.win.hide(); - } else { - this.windowMain.win.show(); - } + this.windowMain.win.show(); } } From dff634d25e4f12dd9c9df5e077c9d9ce6f2a2f71 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 23 May 2019 20:03:58 -0400 Subject: [PATCH 0819/1626] dont toLower imported uris --- src/importers/baseImporter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index fcb33e796c..9c0ce1eef0 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -136,7 +136,7 @@ export abstract class BaseImporter { if (uri == null) { return null; } - uri = uri.toLowerCase().trim(); + uri = uri.trim(); if (uri.indexOf('://') === -1 && uri.indexOf('.') >= 0) { uri = 'http://' + uri; } From cd46f64993545a1cb772e2f6a2137a675554f3c3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 27 May 2019 10:29:09 -0400 Subject: [PATCH 0820/1626] implement AuthServiceAbstraction --- src/abstractions/auth.service.ts | 2 +- .../components/two-factor.component.ts | 18 ++++++------ src/services/auth.service.ts | 29 ++++++++++--------- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/abstractions/auth.service.ts b/src/abstractions/auth.service.ts index b654365b15..de7481ce60 100644 --- a/src/abstractions/auth.service.ts +++ b/src/abstractions/auth.service.ts @@ -6,7 +6,7 @@ import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; export abstract class AuthService { email: string; masterPasswordHash: string; - twoFactorProviders: Map; + twoFactorProvidersData: Map; selectedTwoFactorProviderType: TwoFactorProviderType; logIn: (email: string, masterPassword: string) => Promise; diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index e7e41f2f39..1e3ccd8904 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -48,7 +48,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { async ngOnInit() { if (this.authService.email == null || this.authService.masterPasswordHash == null || - this.authService.twoFactorProviders == null) { + this.authService.twoFactorProvidersData == null) { this.router.navigate([this.loginRoute]); return; } @@ -90,18 +90,18 @@ export class TwoFactorComponent implements OnInit, OnDestroy { this.cleanupU2f(); this.title = (TwoFactorProviders as any)[this.selectedProviderType].name; - const params = this.authService.twoFactorProviders.get(this.selectedProviderType); + const providerData = this.authService.twoFactorProvidersData.get(this.selectedProviderType); switch (this.selectedProviderType) { case TwoFactorProviderType.U2f: if (!this.u2fSupported || this.u2f == null) { break; } - if (params.Challenge != null) { - this.u2f.init(JSON.parse(params.Challenge)); + if (providerData.Challenge != null) { + this.u2f.init(JSON.parse(providerData.Challenge)); } else { // TODO: Deprecated. Remove in future version. - const challenges = JSON.parse(params.Challenges); + const challenges = JSON.parse(providerData.Challenges); if (challenges != null && challenges.length > 0) { this.u2f.init({ appId: challenges[0].appId, @@ -125,8 +125,8 @@ export class TwoFactorComponent implements OnInit, OnDestroy { setTimeout(() => { DuoWebSDK.init({ iframe: undefined, - host: params.Host, - sig_request: params.Signature, + host: providerData.Host, + sig_request: providerData.Signature, submit_callback: async (f: HTMLFormElement) => { const sig = f.querySelector('input[name="sig_response"]') as HTMLInputElement; if (sig != null) { @@ -138,8 +138,8 @@ export class TwoFactorComponent implements OnInit, OnDestroy { }, 0); break; case TwoFactorProviderType.Email: - this.twoFactorEmail = params.Email; - if (this.authService.twoFactorProviders.size > 1) { + this.twoFactorEmail = providerData.Email; + if (this.authService.twoFactorProvidersData.size > 1) { await this.sendEmail(false); } break; diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 0a4ab62e6f..ad379f29fd 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -15,6 +15,7 @@ import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorR import { ApiService } from '../abstractions/api.service'; import { AppIdService } from '../abstractions/appId.service'; +import { AuthService as AuthServiceAbstraction } from '../abstractions/auth.service'; import { CryptoService } from '../abstractions/crypto.service'; import { I18nService } from '../abstractions/i18n.service'; import { MessagingService } from '../abstractions/messaging.service'; @@ -73,10 +74,10 @@ export const TwoFactorProviders = { }, }; -export class AuthService { +export class AuthService implements AuthServiceAbstraction { email: string; masterPasswordHash: string; - twoFactorProviders: Map; + twoFactorProvidersData: Map; selectedTwoFactorProviderType: TwoFactorProviderType = null; private key: SymmetricCryptoKey; @@ -139,32 +140,32 @@ export class AuthService { getSupportedTwoFactorProviders(win: Window): any[] { const providers: any[] = []; - if (this.twoFactorProviders == null) { + if (this.twoFactorProvidersData == null) { return providers; } - if (this.twoFactorProviders.has(TwoFactorProviderType.OrganizationDuo) && + if (this.twoFactorProvidersData.has(TwoFactorProviderType.OrganizationDuo) && this.platformUtilsService.supportsDuo()) { providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]); } - if (this.twoFactorProviders.has(TwoFactorProviderType.Authenticator)) { + if (this.twoFactorProvidersData.has(TwoFactorProviderType.Authenticator)) { providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]); } - if (this.twoFactorProviders.has(TwoFactorProviderType.Yubikey)) { + if (this.twoFactorProvidersData.has(TwoFactorProviderType.Yubikey)) { providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]); } - if (this.twoFactorProviders.has(TwoFactorProviderType.Duo) && this.platformUtilsService.supportsDuo()) { + if (this.twoFactorProvidersData.has(TwoFactorProviderType.Duo) && this.platformUtilsService.supportsDuo()) { providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]); } - if (this.twoFactorProviders.has(TwoFactorProviderType.U2f) && this.platformUtilsService.supportsU2f(win)) { + if (this.twoFactorProvidersData.has(TwoFactorProviderType.U2f) && this.platformUtilsService.supportsU2f(win)) { providers.push(TwoFactorProviders[TwoFactorProviderType.U2f]); } - if (this.twoFactorProviders.has(TwoFactorProviderType.Email)) { + if (this.twoFactorProvidersData.has(TwoFactorProviderType.Email)) { providers.push(TwoFactorProviders[TwoFactorProviderType.Email]); } @@ -172,18 +173,18 @@ export class AuthService { } getDefaultTwoFactorProvider(u2fSupported: boolean): TwoFactorProviderType { - if (this.twoFactorProviders == null) { + if (this.twoFactorProvidersData == null) { return null; } if (this.selectedTwoFactorProviderType != null && - this.twoFactorProviders.has(this.selectedTwoFactorProviderType)) { + this.twoFactorProvidersData.has(this.selectedTwoFactorProviderType)) { return this.selectedTwoFactorProviderType; } let providerType: TwoFactorProviderType = null; let providerPriority = -1; - this.twoFactorProviders.forEach((value, type) => { + this.twoFactorProvidersData.forEach((value, type) => { const provider = (TwoFactorProviders as any)[type]; if (provider != null && provider.priority > providerPriority) { if (type === TwoFactorProviderType.U2f && !u2fSupported) { @@ -245,7 +246,7 @@ export class AuthService { this.email = email; this.masterPasswordHash = hashedPassword; this.key = this.setCryptoKeys ? key : null; - this.twoFactorProviders = twoFactorResponse.twoFactorProviders2; + this.twoFactorProvidersData = twoFactorResponse.twoFactorProviders2; result.twoFactorProviders = twoFactorResponse.twoFactorProviders2; return result; } @@ -285,7 +286,7 @@ export class AuthService { private clearState(): void { this.email = null; this.masterPasswordHash = null; - this.twoFactorProviders = null; + this.twoFactorProvidersData = null; this.selectedTwoFactorProviderType = null; } } From 38fc0432c3b352628b0114ac98b49ca69ee01675 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 30 May 2019 09:37:02 -0400 Subject: [PATCH 0821/1626] dont call clearclipboard in a loop --- .../services/electronPlatformUtils.service.ts | 13 +++++++++---- src/services/system.service.ts | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index b2b01750e0..4539b982e0 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -186,12 +186,17 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { copyToClipboard(text: string, options?: any): void { const type = options ? options.type : null; + const clearing = options ? !!options.clearing : false; const clearMs: number = options && options.clearMs ? options.clearMs : null; clipboard.writeText(text, type); - this.messagingService.send('copiedToClipboard', { - clipboardValue: text, - clearMs: clearMs, - }); + if (!clearing) { + this.messagingService.send('copiedToClipboard', { + clipboardValue: text, + clearMs: clearMs, + type: type, + clearing: clearing, + }); + } } readFromClipboard(options?: any): Promise { diff --git a/src/services/system.service.ts b/src/services/system.service.ts index 1e492c077f..a53b39800e 100644 --- a/src/services/system.service.ts +++ b/src/services/system.service.ts @@ -67,7 +67,7 @@ export class SystemService implements SystemServiceAbstraction { this.clearClipboardTimeoutFunction = async () => { const clipboardValueNow = await this.platformUtilsService.readFromClipboard(); if (clipboardValue === clipboardValueNow) { - this.platformUtilsService.copyToClipboard(''); + this.platformUtilsService.copyToClipboard('', { clearing: true }); } }; this.clearClipboardTimeout = setTimeout(async () => { From 697e7ef6327aab554289e6e5ebd8671075c71956 Mon Sep 17 00:00:00 2001 From: Tobirexy Date: Mon, 3 Jun 2019 14:29:29 +0200 Subject: [PATCH 0822/1626] add window option: always on top of other windows (#41) * add window option: always on Top * updated import path * changes requested --- src/electron/electronConstants.ts | 1 + src/electron/window.main.ts | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/electron/electronConstants.ts b/src/electron/electronConstants.ts index 3226486d34..b0086f8029 100644 --- a/src/electron/electronConstants.ts +++ b/src/electron/electronConstants.ts @@ -3,4 +3,5 @@ export class ElectronConstants { static readonly enableCloseToTrayKey: string = 'enableCloseToTray'; static readonly enableTrayKey: string = 'enableTray'; static readonly enableStartToTrayKey: string = 'enableStartToTrayKey'; + static readonly enableAlwaysOnTopKey: string = 'enableAlwaysOnTopKey'; } diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index 865ea81368..6faed6a00d 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -1,9 +1,10 @@ import { app, BrowserWindow, screen } from 'electron'; +import { ElectronConstants } from './electronConstants'; + import * as path from 'path'; import * as url from 'url'; import { isDev, isMacAppStore, isSnapStore } from './utils'; - import { StorageService } from '../abstractions/storage.service'; const WindowEventHandlingDelay = 100; @@ -14,6 +15,7 @@ const Keys = { export class WindowMain { win: BrowserWindow; isQuitting: boolean = false; + enableAlwaysOnTop: boolean = false; private windowStateChangeTimer: NodeJS.Timer; private windowStates: { [key: string]: any; } = {}; @@ -84,6 +86,7 @@ export class WindowMain { async createWindow(): Promise { this.windowStates[Keys.mainWindowSize] = await this.getWindowState(Keys.mainWindowSize, this.defaultWidth, this.defaultHeight); + this.enableAlwaysOnTop = await this.storageService.get(ElectronConstants.enableAlwaysOnTopKey); // Create the browser window. this.win = new BrowserWindow({ @@ -97,6 +100,7 @@ export class WindowMain { icon: process.platform === 'linux' ? path.join(__dirname, '/images/icon.png') : undefined, titleBarStyle: this.hideTitleBar && process.platform === 'darwin' ? 'hiddenInset' : undefined, show: false, + alwaysOnTop: this.enableAlwaysOnTop, }); if (this.windowStates[Keys.mainWindowSize].isMaximized) { @@ -147,6 +151,13 @@ export class WindowMain { this.win.on('move', () => { this.windowStateChangeHandler(Keys.mainWindowSize, this.win); }); + + } + + async toggleAlwaysOnTop(){ + this.enableAlwaysOnTop = !this.win.isAlwaysOnTop(); + this.win.setAlwaysOnTop(this.enableAlwaysOnTop); + await this.storageService.save(ElectronConstants.enableAlwaysOnTopKey, this.enableAlwaysOnTop); } private windowStateChangeHandler(configKey: string, win: BrowserWindow) { From 3d93696fb51b6a0c3d29c6df626e60fab9cca789 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 3 Jun 2019 08:33:37 -0400 Subject: [PATCH 0823/1626] formatting --- src/electron/window.main.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index 6faed6a00d..ab25cc689f 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -5,6 +5,7 @@ import * as path from 'path'; import * as url from 'url'; import { isDev, isMacAppStore, isSnapStore } from './utils'; + import { StorageService } from '../abstractions/storage.service'; const WindowEventHandlingDelay = 100; @@ -15,10 +16,10 @@ const Keys = { export class WindowMain { win: BrowserWindow; isQuitting: boolean = false; - enableAlwaysOnTop: boolean = false; private windowStateChangeTimer: NodeJS.Timer; private windowStates: { [key: string]: any; } = {}; + private enableAlwaysOnTop: boolean = false; constructor(private storageService: StorageService, private hideTitleBar = false, private defaultWidth = 950, private defaultHeight = 600) { } @@ -154,7 +155,7 @@ export class WindowMain { } - async toggleAlwaysOnTop(){ + async toggleAlwaysOnTop() { this.enableAlwaysOnTop = !this.win.isAlwaysOnTop(); this.win.setAlwaysOnTop(this.enableAlwaysOnTop); await this.storageService.save(ElectronConstants.enableAlwaysOnTopKey, this.enableAlwaysOnTop); From ac84a36206b6c3939b5fddc9d60a1599073308c3 Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 3 Jun 2019 15:46:54 +0200 Subject: [PATCH 0824/1626] Allow the lock screen to access the environmentService. (#42) --- src/angular/components/lock.component.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 8c88f6363f..2a0249ccea 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -2,6 +2,7 @@ import { OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { CryptoService } from '../../abstractions/crypto.service'; +import { EnvironmentService } from '../../abstractions/environment.service'; import { I18nService } from '../../abstractions/i18n.service'; import { LockService } from '../../abstractions/lock.service'; import { MessagingService } from '../../abstractions/messaging.service'; @@ -20,6 +21,7 @@ export class LockComponent implements OnInit { showPassword: boolean = false; email: string; pinLock: boolean = false; + webVaultHostname: string = ''; protected successRoute: string = 'vault'; protected onSuccessfulSubmit: () => void; @@ -30,13 +32,15 @@ export class LockComponent implements OnInit { constructor(protected router: Router, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected messagingService: MessagingService, protected userService: UserService, protected cryptoService: CryptoService, - protected storageService: StorageService, protected lockService: LockService) { } + protected storageService: StorageService, protected lockService: LockService, + protected environmentService: EnvironmentService) { } async ngOnInit() { this.pinSet = await this.lockService.isPinLockSet(); const hasKey = await this.cryptoService.hasKey(); this.pinLock = (this.pinSet[0] && hasKey) || this.pinSet[1]; this.email = await this.userService.getEmail(); + this.webVaultHostname = await new URL(this.environmentService.getWebVaultUrl()).hostname; } async submit() { From 802d38f52e9794567179b9bd9ffc475ceb31323f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 3 Jun 2019 09:53:18 -0400 Subject: [PATCH 0825/1626] webVaultHostname --- src/angular/components/lock.component.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 2a0249ccea..b8d238253d 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -15,6 +15,8 @@ import { ConstantsService } from '../../services/constants.service'; import { CipherString } from '../../models/domain/cipherString'; import { SymmetricCryptoKey } from '../../models/domain/symmetricCryptoKey'; +import { Utils } from '../../misc/utils'; + export class LockComponent implements OnInit { masterPassword: string = ''; pin: string = ''; @@ -33,14 +35,18 @@ export class LockComponent implements OnInit { protected platformUtilsService: PlatformUtilsService, protected messagingService: MessagingService, protected userService: UserService, protected cryptoService: CryptoService, protected storageService: StorageService, protected lockService: LockService, - protected environmentService: EnvironmentService) { } + protected environmentService: EnvironmentService) { } async ngOnInit() { this.pinSet = await this.lockService.isPinLockSet(); const hasKey = await this.cryptoService.hasKey(); this.pinLock = (this.pinSet[0] && hasKey) || this.pinSet[1]; this.email = await this.userService.getEmail(); - this.webVaultHostname = await new URL(this.environmentService.getWebVaultUrl()).hostname; + let vaultUrl = this.environmentService.getWebVaultUrl(); + if (vaultUrl == null) { + vaultUrl = 'https://bitwarden.com'; + } + this.webVaultHostname = Utils.getHostname(vaultUrl); } async submit() { From a1823f9931173b982fee10ba4cb84c0ee5078fc3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 4 Jun 2019 21:03:15 -0400 Subject: [PATCH 0826/1626] write failed responses to stderr --- src/cli/baseProgram.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/cli/baseProgram.ts b/src/cli/baseProgram.ts index b8b6248d05..546eb70147 100644 --- a/src/cli/baseProgram.ts +++ b/src/cli/baseProgram.ts @@ -10,15 +10,17 @@ import { UserService } from '../abstractions/user.service'; const chalk = chk.default; export abstract class BaseProgram { - constructor(private userService: UserService, private writeLn: (s: string, finalLine: boolean) => void) { } + constructor( + private userService: UserService, + private writeLn: (s: string, finalLine: boolean, error: boolean) => void) { } protected processResponse(response: Response, exitImmediately = false, dataProcessor: () => string = null) { if (!response.success) { if (process.env.BW_QUIET !== 'true') { if (process.env.BW_RESPONSE === 'true') { - this.writeLn(this.getJson(response), true); + this.writeLn(this.getJson(response), true, true); } else { - this.writeLn(chalk.redBright(response.message), true); + this.writeLn(chalk.redBright(response.message), true, true); } } if (exitImmediately) { @@ -30,7 +32,7 @@ export abstract class BaseProgram { } if (process.env.BW_RESPONSE === 'true') { - this.writeLn(this.getJson(response), true); + this.writeLn(this.getJson(response), true, false); } else if (response.data != null) { let out: string = dataProcessor != null ? dataProcessor() : null; if (out == null) { @@ -49,7 +51,7 @@ export abstract class BaseProgram { } if (out != null && process.env.BW_QUIET !== 'true') { - this.writeLn(out, true); + this.writeLn(out, true, false); } } if (exitImmediately) { From f4f90f83cd40c4ce6c4ceeedb9d359af463a02ed Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 5 Jun 2019 08:31:51 -0400 Subject: [PATCH 0827/1626] null check on parts name --- src/services/collection.service.ts | 4 ++-- src/services/folder.service.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index 842f5614cb..39115e20bb 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -107,8 +107,8 @@ export class CollectionService implements CollectionServiceAbstraction { const collectionCopy = new CollectionView(); collectionCopy.id = c.id; collectionCopy.organizationId = c.organizationId; - ServiceUtils.nestedTraverse(nodes, 0, c.name.replace(/^\/+|\/+$/g, '').split(NestingDelimiter), - collectionCopy, null, NestingDelimiter); + const parts = c.name != null ? c.name.replace(/^\/+|\/+$/g, '').split(NestingDelimiter) : []; + ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, null, NestingDelimiter); }); return nodes; } diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index ba64f0e53a..754fa30b42 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -105,8 +105,8 @@ export class FolderService implements FolderServiceAbstraction { const folderCopy = new FolderView(); folderCopy.id = f.id; folderCopy.revisionDate = f.revisionDate; - ServiceUtils.nestedTraverse(nodes, 0, f.name.replace(/^\/+|\/+$/g, '').split(NestingDelimiter), - folderCopy, null, NestingDelimiter); + const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, '').split(NestingDelimiter) : []; + ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, null, NestingDelimiter); }); return nodes; } From a017f725068a031284af9f500bc32bdfda1a3478 Mon Sep 17 00:00:00 2001 From: SvenvDam Date: Thu, 6 Jun 2019 20:06:38 +0200 Subject: [PATCH 0828/1626] Add common password requirements option to passphrase generator (#43) * Added commont password requirements option to passphrase generator * Processed PR comments --- src/services/passwordGeneration.service.ts | 33 ++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index b5c74822cd..6f76e05582 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -25,6 +25,8 @@ const DefaultOptions = { type: 'password', numWords: 3, wordSeparator: '-', + capitalize: false, + includeNumber: false, }; const Keys = { @@ -171,12 +173,29 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr o.wordSeparator = ' '; } + if (o.capitalize == null) { + o.addCommonRequirements = false; + } + + if (o.includeNumber == null) { + o.includeNumber = false; + } + const listLength = EEFLongWordList.length - 1; - const wordList = new Array(o.numWords); + let wordList = new Array(o.numWords); for (let i = 0; i < o.numWords; i++) { const wordIndex = await this.cryptoService.randomNumber(0, listLength); - wordList[i] = EEFLongWordList[wordIndex]; + if (o.capitalize) { + wordList[i] = this.capitalize(EEFLongWordList[wordIndex]); + } else { + wordList[i] = EEFLongWordList[wordIndex]; + } } + + if (o.includeNumber) { + return await this.insertNumber(wordList.join(o.wordSeparator)); + } + return wordList.join(o.wordSeparator); } @@ -256,6 +275,16 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return result; } + private capitalize(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); + } + + private async insertNumber(word: string) { + const charIndex = await this.cryptoService.randomNumber(0, word.length - 1); + const addedNumber = await this.cryptoService.randomNumber(0, 9); + return word.substring(0, charIndex) + addedNumber + word.substring(charIndex, word.length); + } + private async encryptHistory(history: GeneratedPasswordHistory[]): Promise { if (history == null || history.length === 0) { return Promise.resolve([]); From 8375f7381ad1d113fbf2fe2bd65e90ec4a1631b7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 6 Jun 2019 14:15:48 -0400 Subject: [PATCH 0829/1626] appendRandomNumberToRandomWord --- src/services/passwordGeneration.service.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 6f76e05582..372c9971b9 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -172,17 +172,15 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr if (o.wordSeparator == null || o.wordSeparator.length === 0 || o.wordSeparator.length > 1) { o.wordSeparator = ' '; } - if (o.capitalize == null) { o.addCommonRequirements = false; } - if (o.includeNumber == null) { o.includeNumber = false; } const listLength = EEFLongWordList.length - 1; - let wordList = new Array(o.numWords); + const wordList = new Array(o.numWords); for (let i = 0; i < o.numWords; i++) { const wordIndex = await this.cryptoService.randomNumber(0, listLength); if (o.capitalize) { @@ -193,9 +191,8 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr } if (o.includeNumber) { - return await this.insertNumber(wordList.join(o.wordSeparator)); + await this.appendRandomNumberToRandomWord(wordList); } - return wordList.join(o.wordSeparator); } @@ -279,10 +276,13 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return str.charAt(0).toUpperCase() + str.slice(1); } - private async insertNumber(word: string) { - const charIndex = await this.cryptoService.randomNumber(0, word.length - 1); - const addedNumber = await this.cryptoService.randomNumber(0, 9); - return word.substring(0, charIndex) + addedNumber + word.substring(charIndex, word.length); + private async appendRandomNumberToRandomWord(wordList: string[]) { + if (wordList == null || wordList.length < 0) { + return; + } + const index = await this.cryptoService.randomNumber(0, wordList.length - 1); + const num = await this.cryptoService.randomNumber(0, 9); + wordList[index] = wordList[index] + num; } private async encryptHistory(history: GeneratedPasswordHistory[]): Promise { From 740c01c33cb570764865ed270ad51571fe782cee Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 19 Jun 2019 16:43:08 -0400 Subject: [PATCH 0830/1626] anonymize user agent from desktop app --- src/electron/window.main.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index ab25cc689f..cdf9d68bb7 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -116,7 +116,9 @@ export class WindowMain { protocol: 'file:', pathname: path.join(__dirname, '/index.html'), slashes: true, - })); + }), { + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0', + }); // Open the DevTools. if (isDev()) { From 49a6b5046f955a0b1637b876a8ebe45486ec2ffc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 20 Jun 2019 08:56:45 -0400 Subject: [PATCH 0831/1626] setup for client event collection --- src/abstractions/api.service.ts | 3 ++ src/abstractions/environment.service.ts | 1 + src/enums/eventType.ts | 7 ++++ src/models/domain/environmentUrls.ts | 1 + src/models/request/eventRequest.ts | 6 ++++ src/services/api.service.ts | 47 ++++++++++++++++++------- src/services/environment.service.ts | 7 ++++ 7 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 src/models/request/eventRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 55a636a624..6732b8c04e 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -12,6 +12,7 @@ import { CollectionRequest } from '../models/request/collectionRequest'; import { DeleteRecoverRequest } from '../models/request/deleteRecoverRequest'; import { EmailRequest } from '../models/request/emailRequest'; import { EmailTokenRequest } from '../models/request/emailTokenRequest'; +import { EventRequest } from '../models/request/eventRequest'; import { FolderRequest } from '../models/request/folderRequest'; import { GroupRequest } from '../models/request/groupRequest'; import { ImportCiphersRequest } from '../models/request/importCiphersRequest'; @@ -98,6 +99,7 @@ export abstract class ApiService { urlsSet: boolean; apiBaseUrl: string; identityBaseUrl: string; + eventsBaseUrl: string; setUrls: (urls: EnvironmentUrls) => void; postIdentityToken: (request: TokenRequest) => Promise; @@ -254,6 +256,7 @@ export abstract class ApiService { token: string) => Promise>; getEventsOrganizationUser: (organizationId: string, id: string, start: string, end: string, token: string) => Promise>; + postEventsCollect: (request: EventRequest) => Promise; getUserPublicKey: (id: string) => Promise; diff --git a/src/abstractions/environment.service.ts b/src/abstractions/environment.service.ts index e561341d36..455447bbf6 100644 --- a/src/abstractions/environment.service.ts +++ b/src/abstractions/environment.service.ts @@ -5,6 +5,7 @@ export abstract class EnvironmentService { identityUrl: string; iconsUrl: string; notificationsUrl: string; + eventsUrl: string; getWebVaultUrl: () => string; setUrlsFromStorage: () => Promise; diff --git a/src/enums/eventType.ts b/src/enums/eventType.ts index 11b4c26aa9..f5b50d8fb5 100644 --- a/src/enums/eventType.ts +++ b/src/enums/eventType.ts @@ -6,6 +6,7 @@ export enum EventType { User_Recovered2fa = 1004, User_FailedLogIn = 1005, User_FailedLogIn2fa = 1006, + User_ExportedVault = 1007, Cipher_Created = 1100, Cipher_Updated = 1101, @@ -14,6 +15,12 @@ export enum EventType { Cipher_AttachmentDeleted = 1104, Cipher_Shared = 1105, Cipher_UpdatedCollections = 1106, + Cipher_ClientViewed = 1107, + Cipher_ClientToggledPasswordVisible = 1108, + Cipher_ClientToggledHiddenFieldVisible = 1109, + Cipher_ClientCopiedPassword = 1110, + Cipher_ClientCopedHiddenField = 1111, + Cipher_ClientAutofilled = 1112, Collection_Created = 1300, Collection_Updated = 1301, diff --git a/src/models/domain/environmentUrls.ts b/src/models/domain/environmentUrls.ts index 868cd43218..2241bd0a1a 100644 --- a/src/models/domain/environmentUrls.ts +++ b/src/models/domain/environmentUrls.ts @@ -2,4 +2,5 @@ export class EnvironmentUrls { base: string; api: string; identity: string; + events: string; } diff --git a/src/models/request/eventRequest.ts b/src/models/request/eventRequest.ts new file mode 100644 index 0000000000..f9f52d910e --- /dev/null +++ b/src/models/request/eventRequest.ts @@ -0,0 +1,6 @@ +import { EventType } from '../../enums/eventType'; + +export class EventRequest { + type: EventType; + cipherId: string; +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index fcff0bf77d..d3db7012e3 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -18,6 +18,7 @@ import { CollectionRequest } from '../models/request/collectionRequest'; import { DeleteRecoverRequest } from '../models/request/deleteRecoverRequest'; import { EmailRequest } from '../models/request/emailRequest'; import { EmailTokenRequest } from '../models/request/emailTokenRequest'; +import { EventRequest } from '../models/request/eventRequest'; import { FolderRequest } from '../models/request/folderRequest'; import { GroupRequest } from '../models/request/groupRequest'; import { ImportCiphersRequest } from '../models/request/importCiphersRequest'; @@ -105,6 +106,7 @@ export class ApiService implements ApiServiceAbstraction { urlsSet: boolean = false; apiBaseUrl: string; identityBaseUrl: string; + eventsBaseUrl: string; private deviceType: string; private isWebClient = false; @@ -130,24 +132,24 @@ export class ApiService implements ApiServiceAbstraction { this.usingBaseUrl = true; this.apiBaseUrl = urls.base + '/api'; this.identityBaseUrl = urls.base + '/identity'; + this.eventsBaseUrl = urls.base + '/events'; return; } - if (urls.api != null && urls.identity != null) { - this.apiBaseUrl = urls.api; - this.identityBaseUrl = urls.identity; - return; - } - - /* tslint:disable */ - // Local Dev - //this.apiBaseUrl = 'http://localhost:4000'; - //this.identityBaseUrl = 'http://localhost:33656'; + this.apiBaseUrl = urls.api; + this.identityBaseUrl = urls.identity; + this.eventsBaseUrl = urls.events; // Production - this.apiBaseUrl = 'https://api.bitwarden.com'; - this.identityBaseUrl = 'https://identity.bitwarden.com'; - /* tslint:enable */ + if (this.apiBaseUrl == null) { + this.apiBaseUrl = 'https://api.bitwarden.com'; + } + if (this.identityBaseUrl == null) { + this.identityBaseUrl = 'https://identity.bitwarden.com'; + } + if (this.eventsBaseUrl == null) { + this.eventsBaseUrl = 'https://events.bitwarden.com'; + } } // Auth APIs @@ -839,6 +841,25 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, EventResponse); } + async postEventsCollect(request: EventRequest): Promise { + const authHeader = await this.getActiveBearerToken(); + const headers = new Headers({ + 'Device-Type': this.deviceType, + 'Authorization': 'Bearer ' + authHeader, + 'Content-Type': 'application/json; charset=utf-8', + }); + const response = await this.fetch(new Request(this.eventsBaseUrl + '/collect', { + cache: 'no-cache', + credentials: this.getCredentials(), + method: 'POST', + body: JSON.stringify(request), + headers: headers, + })); + if (response.status !== 200) { + return Promise.reject('Event post failed.'); + } + } + // User APIs async getUserPublicKey(id: string): Promise { diff --git a/src/services/environment.service.ts b/src/services/environment.service.ts index 60a2dc8c62..a5bfea2679 100644 --- a/src/services/environment.service.ts +++ b/src/services/environment.service.ts @@ -14,6 +14,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { identityUrl: string; iconsUrl: string; notificationsUrl: string; + eventsUrl: string; constructor(private apiService: ApiService, private storageService: StorageService, private notificationsService: NotificationsService) { } @@ -35,6 +36,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { identity: null, icons: null, notifications: null, + events: null, webVault: null, }; @@ -51,6 +53,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { this.identityUrl = envUrls.identity = urls.identity; this.iconsUrl = urls.icons; this.notificationsUrl = urls.notifications; + this.eventsUrl = envUrls.events = urls.events; this.apiService.setUrls(envUrls); } @@ -61,6 +64,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { urls.identity = this.formatUrl(urls.identity); urls.icons = this.formatUrl(urls.icons); urls.notifications = this.formatUrl(urls.notifications); + urls.events = this.formatUrl(urls.events); await this.storageService.save(ConstantsService.environmentUrlsKey, { base: urls.base, @@ -69,6 +73,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { webVault: urls.webVault, icons: urls.icons, notifications: urls.notifications, + events: urls.events, }); this.baseUrl = urls.base; @@ -77,6 +82,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { this.identityUrl = urls.identity; this.iconsUrl = urls.icons; this.notificationsUrl = urls.notifications; + this.eventsUrl = urls.events; const envUrls = new EnvironmentUrls(); if (this.baseUrl) { @@ -84,6 +90,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { } else { envUrls.api = this.apiUrl; envUrls.identity = this.identityUrl; + envUrls.events = this.eventsUrl; } this.apiService.setUrls(envUrls); From 6d82fb5bbc5ab69968154047d3add4344ec9f624 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 21 Jun 2019 08:19:38 -0400 Subject: [PATCH 0832/1626] support old CSV format for password agent --- src/importers/passwordAgentCsvImporter.ts | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/importers/passwordAgentCsvImporter.ts b/src/importers/passwordAgentCsvImporter.ts index 444585661c..6095fcbb52 100644 --- a/src/importers/passwordAgentCsvImporter.ts +++ b/src/importers/passwordAgentCsvImporter.ts @@ -12,25 +12,32 @@ export class PasswordAgentCsvImporter extends BaseImporter implements Importer { return result; } + let newVersion = true; results.forEach((value) => { - if (value.length < 9) { + if (value.length !== 5 && value.length < 9) { return; } - const folder = this.getValueOrDefault(value[8], '(None)'); - const folderName = folder !== '(None)' ? folder.split('\\').join('/') : null; - this.processFolder(result, folderName); const cipher = this.initLoginCipher(); - cipher.notes = this.getValueOrDefault(value[3]); cipher.name = this.getValueOrDefault(value[0], '--'); cipher.login.username = this.getValueOrDefault(value[1]); cipher.login.password = this.getValueOrDefault(value[2]); - cipher.login.uris = this.makeUriArray(value[4]); + if (value.length === 5) { + newVersion = false; + cipher.notes = this.getValueOrDefault(value[4]); + cipher.login.uris = this.makeUriArray(value[3]); + } else { + const folder = this.getValueOrDefault(value[8], '(None)'); + const folderName = folder !== '(None)' ? folder.split('\\').join('/') : null; + this.processFolder(result, folderName); + cipher.notes = this.getValueOrDefault(value[3]); + cipher.login.uris = this.makeUriArray(value[4]); + } this.convertToNoteIfNeeded(cipher); this.cleanupCipher(cipher); result.ciphers.push(cipher); }); - if (this.organization) { + if (newVersion && this.organization) { this.moveFoldersToCollections(result); } From bc5a6e02c106dc87413f22b7c7cb0334c1fc6e88 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 24 Jun 2019 11:07:26 -0400 Subject: [PATCH 0833/1626] native fetch with proxy support on node api --- package-lock.json | 47 ++++++++++++++++++++++++--------- package.json | 1 + src/abstractions/api.service.ts | 1 + src/globals.d.ts | 25 ++++++++++++++++++ src/services/api.service.ts | 4 +++ src/services/audit.service.ts | 2 +- src/services/cipher.service.ts | 3 ++- src/services/nodeApi.service.ts | 9 +++++++ 8 files changed, 78 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7d91135c7d..f0ca38f54f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -541,6 +541,14 @@ "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", "dev": true }, + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "requires": { + "es6-promisify": "^5.0.0" + } + }, "ajv": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz", @@ -1522,8 +1530,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -1938,8 +1945,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -1995,7 +2001,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2039,14 +2044,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -2609,7 +2612,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, "requires": { "ms": "2.0.0" } @@ -3056,6 +3058,19 @@ "stackframe": "^1.0.4" } }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "requires": { + "es6-promise": "^4.0.3" + } + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -4412,6 +4427,15 @@ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true }, + "https-proxy-agent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", + "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", + "requires": { + "agent-base": "^4.1.0", + "debug": "^3.1.0" + } + }, "iconv-lite": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", @@ -5793,8 +5817,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "msgpack5": { "version": "4.2.1", diff --git a/package.json b/package.json index c7e9eec91c..8563973a2d 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "electron-store": "1.3.0", "electron-updater": "4.0.6", "form-data": "2.3.2", + "https-proxy-agent": "2.2.1", "inquirer": "6.2.0", "jsdom": "13.2.0", "keytar": "4.4.1", diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 6732b8c04e..1c471521f7 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -266,4 +266,5 @@ export abstract class ApiService { getActiveBearerToken: () => Promise; fetch: (request: Request) => Promise; + nativeFetch: (request: Request) => Promise; } diff --git a/src/globals.d.ts b/src/globals.d.ts index 8116ab173f..1da4c78936 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -1,3 +1,28 @@ declare function escape(s: string): string; declare function unescape(s: string): string; declare module 'duo_web_sdk'; + +// From: https://github.com/TooTallNate/node-https-proxy-agent/issues/27 +declare module 'https-proxy-agent' { + import * as https from 'https' + + namespace HttpsProxyAgent { + interface HttpsProxyAgentOptions { + host: string + port: number + secureProxy?: boolean + headers?: { + [key: string]: string + } + [key: string]: any + } + } + + // HttpsProxyAgent doesnt *actually* extend https.Agent, but for my purposes I want it to pretend that it does + class HttpsProxyAgent extends https.Agent { + constructor(opts: string) + constructor(opts: HttpsProxyAgent.HttpsProxyAgentOptions) + } + + export = HttpsProxyAgent +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index d3db7012e3..cd4e65f137 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -897,6 +897,10 @@ export class ApiService implements ApiServiceAbstraction { request.headers.set('Cache-Control', 'no-cache'); request.headers.set('Pragma', 'no-cache'); } + return this.nativeFetch(request); + } + + nativeFetch(request: Request): Promise { return fetch(request); } diff --git a/src/services/audit.service.ts b/src/services/audit.service.ts index 7b821afff4..1282ce2bd3 100644 --- a/src/services/audit.service.ts +++ b/src/services/audit.service.ts @@ -20,7 +20,7 @@ export class AuditService implements AuditServiceAbstraction { const hashStart = hash.substr(0, 5); const hashEnding = hash.substr(5); - const response = await fetch(PwnedPasswordsApi + hashStart); + const response = await this.apiService.nativeFetch(new Request(PwnedPasswordsApi + hashStart)); const leakedHashes = await response.text(); const match = leakedHashes.split(/\r?\n/).find((v) => { return v.split(':')[0] === hashEnding; diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 87bd575d27..c602c409ec 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -794,7 +794,8 @@ export class CipherService implements CipherServiceAbstraction { private async shareAttachmentWithServer(attachmentView: AttachmentView, cipherId: string, organizationId: string): Promise { - const attachmentResponse = await fetch(new Request(attachmentView.url, { cache: 'no-cache' })); + const attachmentResponse = await this.apiService.nativeFetch( + new Request(attachmentView.url, { cache: 'no-cache' })); if (attachmentResponse.status !== 200) { throw Error('Failed to download attachment: ' + attachmentResponse.status.toString()); } diff --git a/src/services/nodeApi.service.ts b/src/services/nodeApi.service.ts index 3815d46ec4..07559c89c2 100644 --- a/src/services/nodeApi.service.ts +++ b/src/services/nodeApi.service.ts @@ -1,4 +1,5 @@ import * as FormData from 'form-data'; +import * as HttpsProxyAgent from 'https-proxy-agent'; import * as fe from 'node-fetch'; import { ApiService } from './api.service'; @@ -17,4 +18,12 @@ export class NodeApiService extends ApiService { logoutCallback: (expired: boolean) => Promise) { super(tokenService, platformUtilsService, logoutCallback); } + + nativeFetch(request: Request): Promise { + const proxy = process.env.http_proxy || process.env.https_proxy; + if (proxy) { + (request as any).agent = new HttpsProxyAgent(proxy); + } + return fetch(request); + } } From 24ffb55ce0e781fb06cd04d318e9f70afe80d734 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 24 Jun 2019 21:12:34 -0400 Subject: [PATCH 0834/1626] addEditCipherInfo w/ collections from state --- src/angular/components/add-edit.component.ts | 27 ++++++++++++-------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index b04bedc1a2..dbfd6b3b39 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -162,8 +162,13 @@ export class AddEditComponent implements OnInit { this.title = this.i18nService.t('addItem'); } - this.cipher = await this.stateService.get('addEditCipher'); - await this.stateService.remove('addEditCipher'); + const addEditCipherInfo: any = await this.stateService.get('addEditCipherInfo'); + if (addEditCipherInfo != null) { + this.cipher = addEditCipherInfo.cipher; + this.collectionIds = addEditCipherInfo.collectionIds; + } + await this.stateService.remove('addEditCipherInfo'); + if (this.cipher == null) { if (this.editMode) { const cipher = await this.loadCipher(); @@ -179,15 +184,17 @@ export class AddEditComponent implements OnInit { this.cipher.identity = new IdentityView(); this.cipher.secureNote = new SecureNoteView(); this.cipher.secureNote.type = SecureNoteType.Generic; + } + } - await this.organizationChanged(); - if (this.collectionIds != null && this.collectionIds.length > 0 && this.collections.length > 0) { - this.collections.forEach((c) => { - if (this.collectionIds.indexOf(c.id) > -1) { - (c as any).checked = true; - } - }); - } + if (this.cipher != null && (!this.editMode || addEditCipherInfo != null)) { + await this.organizationChanged(); + if (this.collectionIds != null && this.collectionIds.length > 0 && this.collections.length > 0) { + this.collections.forEach((c) => { + if (this.collectionIds.indexOf(c.id) > -1) { + (c as any).checked = true; + } + }); } } From 86cee1a07c8e177c2b45d05c9d414b4c5128fa26 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 26 Jun 2019 17:42:48 -0400 Subject: [PATCH 0835/1626] update enum name --- src/enums/eventType.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/enums/eventType.ts b/src/enums/eventType.ts index f5b50d8fb5..60edeb06b7 100644 --- a/src/enums/eventType.ts +++ b/src/enums/eventType.ts @@ -6,7 +6,7 @@ export enum EventType { User_Recovered2fa = 1004, User_FailedLogIn = 1005, User_FailedLogIn2fa = 1006, - User_ExportedVault = 1007, + User_ClientExportedVault = 1007, Cipher_Created = 1100, Cipher_Updated = 1101, From 00f1aad65ea2e243713d409873dfb5d833296897 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 26 Jun 2019 17:43:03 -0400 Subject: [PATCH 0836/1626] dont allow select no collections --- src/angular/components/collections.component.ts | 8 +++++++- src/angular/components/share.component.ts | 11 +++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/angular/components/collections.component.ts b/src/angular/components/collections.component.ts index 379f5b5747..abd17fafad 100644 --- a/src/angular/components/collections.component.ts +++ b/src/angular/components/collections.component.ts @@ -17,6 +17,7 @@ import { Cipher } from '../../models/domain/cipher'; export class CollectionsComponent implements OnInit { @Input() cipherId: string; + @Input() allowSelectNone = false; @Output() onSavedCollections = new EventEmitter(); formPromise: Promise; @@ -48,9 +49,14 @@ export class CollectionsComponent implements OnInit { } async submit() { - this.cipherDomain.collectionIds = this.collections + const selectedCollectionIds = this.collections .filter((c) => !!(c as any).checked) .map((c) => c.id); + if (!this.allowSelectNone && selectedCollectionIds.length === 0) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('selectOneCollection')); + } + this.cipherDomain.collectionIds = selectedCollectionIds; try { this.formPromise = this.saveCollections(); await this.formPromise; diff --git a/src/angular/components/share.component.ts b/src/angular/components/share.component.ts index 4c656a5b13..3903709b08 100644 --- a/src/angular/components/share.component.ts +++ b/src/angular/components/share.component.ts @@ -64,13 +64,20 @@ export class ShareComponent implements OnInit { } async submit(): Promise { + const selectedCollectionIds = this.collections + .filter((c) => !!(c as any).checked) + .map((c) => c.id); + if (selectedCollectionIds.length === 0) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('selectOneCollection')); + } + const cipherDomain = await this.cipherService.get(this.cipherId); const cipherView = await cipherDomain.decrypt(); - const checkedCollectionIds = this.collections.filter((c) => (c as any).checked).map((c) => c.id); try { this.formPromise = this.cipherService.shareWithServer(cipherView, this.organizationId, - checkedCollectionIds).then(async () => { + selectedCollectionIds).then(async () => { this.onSharedCipher.emit(); this.platformUtilsService.eventTrack('Shared Cipher'); this.platformUtilsService.showToast('success', null, this.i18nService.t('sharedItem')); From 75514d79a6e202e652019fcd6f5bc1a612f46843 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 26 Jun 2019 17:50:37 -0400 Subject: [PATCH 0837/1626] return on error --- src/angular/components/collections.component.ts | 1 + src/angular/components/share.component.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/angular/components/collections.component.ts b/src/angular/components/collections.component.ts index abd17fafad..fcac85c010 100644 --- a/src/angular/components/collections.component.ts +++ b/src/angular/components/collections.component.ts @@ -55,6 +55,7 @@ export class CollectionsComponent implements OnInit { if (!this.allowSelectNone && selectedCollectionIds.length === 0) { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('selectOneCollection')); + return; } this.cipherDomain.collectionIds = selectedCollectionIds; try { diff --git a/src/angular/components/share.component.ts b/src/angular/components/share.component.ts index 3903709b08..956178fb71 100644 --- a/src/angular/components/share.component.ts +++ b/src/angular/components/share.component.ts @@ -70,6 +70,7 @@ export class ShareComponent implements OnInit { if (selectedCollectionIds.length === 0) { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('selectOneCollection')); + return; } const cipherDomain = await this.cipherService.get(this.cipherId); From 3238b819268d2155376a22b963887f59a3eeb7ca Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 28 Jun 2019 23:17:13 -0400 Subject: [PATCH 0838/1626] myki importer --- src/importers/mykiCsvImporter.ts | 76 ++++++++++++++++++++++++++++++++ src/services/import.service.ts | 4 ++ 2 files changed, 80 insertions(+) create mode 100644 src/importers/mykiCsvImporter.ts diff --git a/src/importers/mykiCsvImporter.ts b/src/importers/mykiCsvImporter.ts new file mode 100644 index 0000000000..47988cc797 --- /dev/null +++ b/src/importers/mykiCsvImporter.ts @@ -0,0 +1,76 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { CipherType } from '../enums/cipherType'; +import { SecureNoteType } from '../enums/secureNoteType'; + +import { CardView } from '../models/view/cardView'; +import { IdentityView } from '../models/view/identityView'; +import { SecureNoteView } from '../models/view/secureNoteView'; + +import { ImportResult } from '../models/domain/importResult'; + +export class MykiCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(value.nickname, '--'); + cipher.notes = this.getValueOrDefault(value.additionalInfo); + + if (value.url !== undefined) { + // Accounts + cipher.login.uris = this.makeUriArray(value.url); + cipher.login.username = this.getValueOrDefault(value.username); + cipher.login.password = this.getValueOrDefault(value.password); + cipher.login.totp = this.getValueOrDefault(value.twoFactAuthToken); + } else if (value.cardNumber !== undefined) { + // Cards + cipher.card = new CardView(); + cipher.type = CipherType.Card; + cipher.card.cardholderName = this.getValueOrDefault(value.cardName); + cipher.card.number = this.getValueOrDefault(value.cardNumber); + cipher.card.brand = this.getCardBrand(cipher.card.number); + cipher.card.expMonth = this.getValueOrDefault(value.exp_month); + cipher.card.expYear = this.getValueOrDefault(value.exp_year); + cipher.card.code = this.getValueOrDefault(value.cvv); + } else if (value.firstName !== undefined) { + // Identities + cipher.identity = new IdentityView(); + cipher.type = CipherType.Identity; + cipher.identity.title = this.getValueOrDefault(value.title); + cipher.identity.firstName = this.getValueOrDefault(value.firstName); + cipher.identity.middleName = this.getValueOrDefault(value.middleName); + cipher.identity.lastName = this.getValueOrDefault(value.lastName); + cipher.identity.phone = this.getValueOrDefault(value.number); + cipher.identity.email = this.getValueOrDefault(value.email); + cipher.identity.address1 = this.getValueOrDefault(value.firstAddressLine); + cipher.identity.address2 = this.getValueOrDefault(value.secondAddressLine); + cipher.identity.city = this.getValueOrDefault(value.city); + cipher.identity.country = this.getValueOrDefault(value.country); + cipher.identity.postalCode = this.getValueOrDefault(value.zipCode); + } else if (value.content !== undefined) { + // Notes + cipher.secureNote = new SecureNoteView(); + cipher.type = CipherType.SecureNote; + cipher.secureNote.type = SecureNoteType.Generic; + cipher.name = this.getValueOrDefault(value.title, '--'); + cipher.notes = this.getValueOrDefault(value.content); + } else { + return; + } + + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} diff --git a/src/services/import.service.ts b/src/services/import.service.ts index 5d69fde20a..bb8baae8d7 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -45,6 +45,7 @@ import { KeeperCsvImporter } from '../importers/keeperCsvImporter'; import { LastPassCsvImporter } from '../importers/lastpassCsvImporter'; import { MeldiumCsvImporter } from '../importers/meldiumCsvImporter'; import { MSecureCsvImporter } from '../importers/msecureCsvImporter'; +import { MykiCsvImporter } from '../importers/mykiCsvImporter'; import { OnePassword1PifImporter } from '../importers/onepassword1PifImporter'; import { OnePasswordWinCsvImporter } from '../importers/onepasswordWinCsvImporter'; import { PadlockCsvImporter } from '../importers/padlockCsvImporter'; @@ -115,6 +116,7 @@ export class ImportService implements ImportServiceAbstraction { { id: 'kasperskytxt', name: 'Kaspersky Password Manager (txt)' }, { id: 'remembearcsv', name: 'RememBear (csv)' }, { id: 'passwordwallettxt', name: 'PasswordWallet (txt)' }, + { id: 'mykicsv', name: 'Myki (csv)' }, ]; constructor(private cipherService: CipherService, private folderService: FolderService, @@ -247,6 +249,8 @@ export class ImportService implements ImportServiceAbstraction { return new RememBearCsvImporter(); case 'passwordwallettxt': return new PasswordWalletTxtImporter(); + case 'mykicsv': + return new MykiCsvImporter(); default: return null; } From 10c865b33db1df3ceb4b573022bdb4976ff89d82 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 1 Jul 2019 15:17:17 -0400 Subject: [PATCH 0839/1626] fix capitalize bug --- src/services/passwordGeneration.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 372c9971b9..40cddedcb1 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -173,7 +173,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr o.wordSeparator = ' '; } if (o.capitalize == null) { - o.addCommonRequirements = false; + o.capitalize = false; } if (o.includeNumber == null) { o.includeNumber = false; From 53bf68de49fb0f7ee1fc2146a058e314f6f34d61 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 1 Jul 2019 15:36:55 -0400 Subject: [PATCH 0840/1626] <= --- src/services/passwordGeneration.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 40cddedcb1..161d3249a1 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -277,7 +277,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr } private async appendRandomNumberToRandomWord(wordList: string[]) { - if (wordList == null || wordList.length < 0) { + if (wordList == null || wordList.length <= 0) { return; } const index = await this.cryptoService.randomNumber(0, wordList.length - 1); From 2aa71f98a1f623470b3bd71306e0ef8a1ba97758 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 2 Jul 2019 08:13:33 -0400 Subject: [PATCH 0841/1626] re-set favicon state after unlock/login --- src/angular/components/lock.component.ts | 7 +++++-- src/angular/components/login.component.ts | 7 ++++++- src/angular/components/two-factor.component.ts | 8 +++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index b8d238253d..a5357f897f 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -7,6 +7,7 @@ import { I18nService } from '../../abstractions/i18n.service'; import { LockService } from '../../abstractions/lock.service'; import { MessagingService } from '../../abstractions/messaging.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { StateService } from '../../abstractions/state.service'; import { StorageService } from '../../abstractions/storage.service'; import { UserService } from '../../abstractions/user.service'; @@ -35,7 +36,7 @@ export class LockComponent implements OnInit { protected platformUtilsService: PlatformUtilsService, protected messagingService: MessagingService, protected userService: UserService, protected cryptoService: CryptoService, protected storageService: StorageService, protected lockService: LockService, - protected environmentService: EnvironmentService) { } + protected environmentService: EnvironmentService, protected stateService: StateService) { } async ngOnInit() { this.pinSet = await this.lockService.isPinLockSet(); @@ -126,7 +127,9 @@ export class LockComponent implements OnInit { this.doContinue(); } - private doContinue() { + private async doContinue() { + const disableFavicon = await this.storageService.get(ConstantsService.disableFaviconKey); + await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon); this.messagingService.send('unlocked'); if (this.onSuccessfulSubmit != null) { this.onSuccessfulSubmit(); diff --git a/src/angular/components/login.component.ts b/src/angular/components/login.component.ts index 86e0e66a7c..2c41e01533 100644 --- a/src/angular/components/login.component.ts +++ b/src/angular/components/login.component.ts @@ -9,8 +9,11 @@ import { AuthResult } from '../../models/domain/authResult'; import { AuthService } from '../../abstractions/auth.service'; import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { StateService } from '../../abstractions/state.service'; import { StorageService } from '../../abstractions/storage.service'; +import { ConstantsService } from '../../services/constants.service'; + import { Utils } from '../../misc/utils'; const Keys = { @@ -34,7 +37,7 @@ export class LoginComponent implements OnInit { constructor(protected authService: AuthService, protected router: Router, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, - private storageService: StorageService) { } + private storageService: StorageService, private stateService: StorageService) { } async ngOnInit() { if (this.email == null || this.email === '') { @@ -86,6 +89,8 @@ export class LoginComponent implements OnInit { this.router.navigate([this.twoFactorRoute]); } } else { + const disableFavicon = await this.storageService.get(ConstantsService.disableFaviconKey); + await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon); if (this.onSuccessfulLogin != null) { this.onSuccessfulLogin(); } diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 1e3ccd8904..dd460a16c5 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -14,8 +14,11 @@ import { AuthService } from '../../abstractions/auth.service'; import { EnvironmentService } from '../../abstractions/environment.service'; import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { StateService } from '../../abstractions/state.service'; +import { StorageService } from '../../abstractions/storage.service'; import { TwoFactorProviders } from '../../services/auth.service'; +import { ConstantsService } from '../../services/constants.service'; import * as DuoWebSDK from 'duo_web_sdk'; import { U2f } from '../../misc/u2f'; @@ -42,7 +45,8 @@ export class TwoFactorComponent implements OnInit, OnDestroy { constructor(protected authService: AuthService, protected router: Router, protected i18nService: I18nService, protected apiService: ApiService, protected platformUtilsService: PlatformUtilsService, protected win: Window, - protected environmentService: EnvironmentService) { + protected environmentService: EnvironmentService, protected stateService: StateService, + protected storageService: StorageService) { this.u2fSupported = this.platformUtilsService.supportsU2f(win); } @@ -169,6 +173,8 @@ export class TwoFactorComponent implements OnInit, OnDestroy { try { this.formPromise = this.authService.logInTwoFactor(this.selectedProviderType, this.token, this.remember); await this.formPromise; + const disableFavicon = await this.storageService.get(ConstantsService.disableFaviconKey); + await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon); if (this.onSuccessfulLogin != null) { this.onSuccessfulLogin(); } From 74c30198587e1c799dfa16c2eb2771ff12c2eebe Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 2 Jul 2019 08:44:39 -0400 Subject: [PATCH 0842/1626] make protected --- src/angular/components/login.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/login.component.ts b/src/angular/components/login.component.ts index 2c41e01533..75fea94b45 100644 --- a/src/angular/components/login.component.ts +++ b/src/angular/components/login.component.ts @@ -37,7 +37,7 @@ export class LoginComponent implements OnInit { constructor(protected authService: AuthService, protected router: Router, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, - private storageService: StorageService, private stateService: StorageService) { } + private storageService: StorageService, protected stateService: StorageService) { } async ngOnInit() { if (this.email == null || this.email === '') { From e15e0eebbda6090f801d4adf0b9c8289e72a8ede Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 3 Jul 2019 09:00:13 -0400 Subject: [PATCH 0843/1626] collect many events --- src/abstractions/api.service.ts | 2 +- src/services/api.service.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 1c471521f7..de665c8bb6 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -256,7 +256,7 @@ export abstract class ApiService { token: string) => Promise>; getEventsOrganizationUser: (organizationId: string, id: string, start: string, end: string, token: string) => Promise>; - postEventsCollect: (request: EventRequest) => Promise; + postEventsCollectMany: (request: EventRequest[]) => Promise; getUserPublicKey: (id: string) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index cd4e65f137..2b21da77d7 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -841,14 +841,14 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, EventResponse); } - async postEventsCollect(request: EventRequest): Promise { + async postEventsCollectMany(request: EventRequest[]): Promise { const authHeader = await this.getActiveBearerToken(); const headers = new Headers({ 'Device-Type': this.deviceType, 'Authorization': 'Bearer ' + authHeader, 'Content-Type': 'application/json; charset=utf-8', }); - const response = await this.fetch(new Request(this.eventsBaseUrl + '/collect', { + const response = await this.fetch(new Request(this.eventsBaseUrl + '/collect/many', { cache: 'no-cache', credentials: this.getCredentials(), method: 'POST', From cfec7c48153236d7ece357deb3933612b834b7f8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 3 Jul 2019 09:53:55 -0400 Subject: [PATCH 0844/1626] event service --- src/abstractions/event.service.ts | 6 +++ src/models/data/eventData.ts | 6 +++ src/services/constants.service.ts | 2 + src/services/eventService.ts | 78 +++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 src/abstractions/event.service.ts create mode 100644 src/models/data/eventData.ts create mode 100644 src/services/eventService.ts diff --git a/src/abstractions/event.service.ts b/src/abstractions/event.service.ts new file mode 100644 index 0000000000..d677722512 --- /dev/null +++ b/src/abstractions/event.service.ts @@ -0,0 +1,6 @@ +import { EventType } from '../enums/eventType'; + +export abstract class EventService { + collect: (eventType: EventType, cipherId?: string, uploadImmediately?: boolean) => Promise; + uploadEvents: () => Promise; +} diff --git a/src/models/data/eventData.ts b/src/models/data/eventData.ts new file mode 100644 index 0000000000..232fb8235c --- /dev/null +++ b/src/models/data/eventData.ts @@ -0,0 +1,6 @@ +import { EventType } from '../../enums/eventType'; + +export class EventData { + type: EventType; + cipherId: string; +} diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index 4ec0fc0e08..648c85ff54 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -21,6 +21,7 @@ export class ConstantsService { static readonly pinProtectedKey: string = 'pinProtectedKey'; static readonly protectedPin: string = 'protectedPin'; static readonly clearClipboardKey: string = 'clearClipboardKey'; + static readonly eventCollectionKey: string = 'eventCollection'; readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; readonly disableGaKey: string = ConstantsService.disableGaKey; @@ -43,4 +44,5 @@ export class ConstantsService { readonly pinProtectedKey: string = ConstantsService.pinProtectedKey; readonly protectedPin: string = ConstantsService.protectedPin; readonly clearClipboardKey: string = ConstantsService.clearClipboardKey; + readonly eventCollectionKey: string = ConstantsService.eventCollectionKey; } diff --git a/src/services/eventService.ts b/src/services/eventService.ts new file mode 100644 index 0000000000..86ce3f94db --- /dev/null +++ b/src/services/eventService.ts @@ -0,0 +1,78 @@ +import { EventType } from '../enums/eventType'; + +import { EventData } from '../models/data/eventData'; + +import { EventRequest } from '../models/request/eventRequest'; + +import { ApiService } from '../abstractions/api.service'; +import { CipherService } from '../abstractions/cipher.service'; +import { EventService as EventServiceAbstraction } from '../abstractions/event.service'; +import { StorageService } from '../abstractions/storage.service'; +import { UserService } from '../abstractions/user.service'; + +import { ConstantsService } from './constants.service'; + +export class EventService implements EventServiceAbstraction { + private inited = false; + + constructor(private storageService: StorageService, private apiService: ApiService, + private userService: UserService, private cipherService: CipherService) { } + + init(checkOnInterval: boolean) { + if (this.inited) { + return; + } + + this.inited = true; + if (checkOnInterval) { + this.uploadEvents(); + setInterval(() => this.uploadEvents(), 60 * 1000); // check every 60 seconds + } + } + + async collect(eventType: EventType, cipherId: string = null, uploadImmediately = false): Promise { + const organizations = await this.userService.getAllOrganizations(); + if (organizations == null) { + return; + } + const orgIds = new Set(organizations.filter((o) => o.useEvents).map((o) => o.id)); + if (orgIds.size === 0) { + return; + } + if (cipherId != null) { + const cipher = await this.cipherService.get(cipherId); + if (cipher == null || cipher.organizationId == null || !orgIds.has(cipher.organizationId)) { + return; + } + } + let eventCollection = await this.storageService.get(ConstantsService.eventCollectionKey); + if (eventCollection == null) { + eventCollection = []; + } + const event = new EventData(); + event.type = eventType; + event.cipherId = cipherId; + eventCollection.push(event); + await this.storageService.save(ConstantsService.eventCollectionKey, eventCollection); + if (uploadImmediately) { + await this.uploadEvents(); + } + } + + async uploadEvents(): Promise { + const eventCollection = await this.storageService.get(ConstantsService.eventCollectionKey); + if (eventCollection == null || eventCollection.length === 0) { + return; + } + const request = eventCollection.map((e) => { + const req = new EventRequest(); + req.type = e.type; + req.cipherId = e.cipherId; + return req; + }); + try { + await this.apiService.postEventsCollectMany(request); + await this.storageService.remove(ConstantsService.eventCollectionKey); + } catch { } + } +} From 05859a1df34dfd07392e7c88cc64a724044e74a5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 3 Jul 2019 10:37:26 -0400 Subject: [PATCH 0845/1626] init u2f param --- src/angular/components/two-factor.component.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index dd460a16c5..66eb59f9eb 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -27,6 +27,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { token: string = ''; remember: boolean = false; u2fReady: boolean = false; + initU2f: boolean = true; providers = TwoFactorProviders; providerType = TwoFactorProviderType; selectedProviderType: TwoFactorProviderType = TwoFactorProviderType.Authenticator; @@ -57,7 +58,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { return; } - if (this.win != null && this.u2fSupported) { + if (this.initU2f && this.win != null && this.u2fSupported) { let customWebVaultUrl: string = null; if (this.environmentService.baseUrl != null) { customWebVaultUrl = this.environmentService.baseUrl; @@ -102,7 +103,9 @@ export class TwoFactorComponent implements OnInit, OnDestroy { } if (providerData.Challenge != null) { - this.u2f.init(JSON.parse(providerData.Challenge)); + setTimeout(() => { + this.u2f.init(JSON.parse(providerData.Challenge)); + }, 500); } else { // TODO: Deprecated. Remove in future version. const challenges = JSON.parse(providerData.Challenges); From 2830121471fb7c6dca6de934c5f28520294fbffb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 3 Jul 2019 12:13:20 -0400 Subject: [PATCH 0846/1626] event post is always many --- src/abstractions/api.service.ts | 2 +- src/services/api.service.ts | 4 ++-- src/services/{eventService.ts => event.service.ts} | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/services/{eventService.ts => event.service.ts} (97%) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index de665c8bb6..13b424dea4 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -256,7 +256,7 @@ export abstract class ApiService { token: string) => Promise>; getEventsOrganizationUser: (organizationId: string, id: string, start: string, end: string, token: string) => Promise>; - postEventsCollectMany: (request: EventRequest[]) => Promise; + postEventsCollect: (request: EventRequest[]) => Promise; getUserPublicKey: (id: string) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 2b21da77d7..ab840ca5a6 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -841,14 +841,14 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, EventResponse); } - async postEventsCollectMany(request: EventRequest[]): Promise { + async postEventsCollect(request: EventRequest[]): Promise { const authHeader = await this.getActiveBearerToken(); const headers = new Headers({ 'Device-Type': this.deviceType, 'Authorization': 'Bearer ' + authHeader, 'Content-Type': 'application/json; charset=utf-8', }); - const response = await this.fetch(new Request(this.eventsBaseUrl + '/collect/many', { + const response = await this.fetch(new Request(this.eventsBaseUrl + '/collect', { cache: 'no-cache', credentials: this.getCredentials(), method: 'POST', diff --git a/src/services/eventService.ts b/src/services/event.service.ts similarity index 97% rename from src/services/eventService.ts rename to src/services/event.service.ts index 86ce3f94db..b393546554 100644 --- a/src/services/eventService.ts +++ b/src/services/event.service.ts @@ -71,7 +71,7 @@ export class EventService implements EventServiceAbstraction { return req; }); try { - await this.apiService.postEventsCollectMany(request); + await this.apiService.postEventsCollect(request); await this.storageService.remove(ConstantsService.eventCollectionKey); } catch { } } From b01759924c112c3d3d33538e782af70e282bbb5b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 5 Jul 2019 13:07:52 -0400 Subject: [PATCH 0847/1626] support ms and s on datetime conversion --- src/importers/onepassword1PifImporter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/importers/onepassword1PifImporter.ts b/src/importers/onepassword1PifImporter.ts index 3f2420a185..56a7a76652 100644 --- a/src/importers/onepassword1PifImporter.ts +++ b/src/importers/onepassword1PifImporter.ts @@ -147,7 +147,7 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { .map((h: any) => { const ph = new PasswordHistoryView(); ph.password = h.value; - ph.lastUsedDate = new Date(h.time * 1000); + ph.lastUsedDate = new Date(('' + h.time).length >= 13 ? h.time : h.time * 1000); return ph; }); } From a631bd990affc7c5f3062b896753a77544557c8c Mon Sep 17 00:00:00 2001 From: Tobirexy Date: Tue, 9 Jul 2019 14:30:26 +0200 Subject: [PATCH 0848/1626] fixes App freezing when NOT opening from tray (#46) --- src/electron/window.main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index cdf9d68bb7..36f26f82d2 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -37,7 +37,7 @@ export class WindowMain { // Someone tried to run a second instance, we should focus our window. if (this.win != null) { if (this.win.isMinimized()) { - this.win.restore(); + this.win.show(); } this.win.focus(); } From 6789b8c8a2b2197e73a74ddd1afb27f89e873743 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jul 2019 08:53:38 -0400 Subject: [PATCH 0849/1626] also show window if not visible --- src/electron/window.main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index 36f26f82d2..6c7faae4a3 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -36,7 +36,7 @@ export class WindowMain { app.on('second-instance', (event, commandLine, workingDirectory) => { // Someone tried to run a second instance, we should focus our window. if (this.win != null) { - if (this.win.isMinimized()) { + if (this.win.isMinimized() || !this.win.isVisible()) { this.win.show(); } this.win.focus(); From aee0ad53dc3510ff55ea5d705d1c2bafc67ddb21 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jul 2019 10:51:40 -0400 Subject: [PATCH 0850/1626] update event log types --- src/enums/eventType.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/enums/eventType.ts b/src/enums/eventType.ts index 60edeb06b7..f133148742 100644 --- a/src/enums/eventType.ts +++ b/src/enums/eventType.ts @@ -18,9 +18,11 @@ export enum EventType { Cipher_ClientViewed = 1107, Cipher_ClientToggledPasswordVisible = 1108, Cipher_ClientToggledHiddenFieldVisible = 1109, - Cipher_ClientCopiedPassword = 1110, - Cipher_ClientCopedHiddenField = 1111, - Cipher_ClientAutofilled = 1112, + Cipher_ClientToggledCardCodeVisible = 1110, + Cipher_ClientCopiedPassword = 1111, + Cipher_ClientCopiedHiddenField = 1112, + Cipher_ClientCopiedCardCode = 1113, + Cipher_ClientAutofilled = 1114, Collection_Created = 1300, Collection_Updated = 1301, From ff9c7bfa6a0eb87558170c2c06b3d38b9c6a7313 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jul 2019 10:51:53 -0400 Subject: [PATCH 0851/1626] add events to view page --- src/angular/components/view.component.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index 1fd26d1015..f6985b977e 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -9,11 +9,13 @@ import { } from '@angular/core'; import { CipherType } from '../../enums/cipherType'; +import { EventType } from '../../enums/eventType'; import { FieldType } from '../../enums/fieldType'; import { AuditService } from '../../abstractions/audit.service'; import { CipherService } from '../../abstractions/cipher.service'; import { CryptoService } from '../../abstractions/crypto.service'; +import { EventService } from '../../abstractions/event.service'; import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { TokenService } from '../../abstractions/token.service'; @@ -51,9 +53,11 @@ export class ViewComponent implements OnDestroy, OnInit { protected cryptoService: CryptoService, protected platformUtilsService: PlatformUtilsService, protected auditService: AuditService, protected win: Window, protected broadcasterService: BroadcasterService, protected ngZone: NgZone, - protected changeDetectorRef: ChangeDetectorRef, protected userService: UserService) { } + protected changeDetectorRef: ChangeDetectorRef, protected userService: UserService, + protected eventService: EventService) { } ngOnInit() { + this.eventService.collect(EventType.Cipher_ClientViewed); this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { this.ngZone.run(async () => { switch (message.command) { @@ -99,11 +103,17 @@ export class ViewComponent implements OnDestroy, OnInit { togglePassword() { this.platformUtilsService.eventTrack('Toggled Password'); this.showPassword = !this.showPassword; + if (this.showPassword) { + this.eventService.collect(EventType.Cipher_ClientToggledPasswordVisible); + } } toggleCardCode() { this.platformUtilsService.eventTrack('Toggled Card Code'); this.showCardCode = !this.showCardCode; + if (this.showCardCode) { + this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible); + } } async checkPassword() { @@ -126,6 +136,9 @@ export class ViewComponent implements OnDestroy, OnInit { toggleFieldValue(field: FieldView) { const f = (field as any); f.showValue = !f.showValue; + if (f.showValue) { + this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible); + } } launch(uri: LoginUriView) { @@ -147,6 +160,14 @@ export class ViewComponent implements OnDestroy, OnInit { this.platformUtilsService.copyToClipboard(value, copyOptions); this.platformUtilsService.showToast('info', null, this.i18nService.t('valueCopied', this.i18nService.t(typeI18nKey))); + + if (typeI18nKey === 'password') { + this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible); + } else if (typeI18nKey === 'securityCode') { + this.eventService.collect(EventType.Cipher_ClientCopiedCardCode); + } else if (aType === 'H_Field') { + this.eventService.collect(EventType.Cipher_ClientCopiedHiddenField); + } } async downloadAttachment(attachment: AttachmentView) { From 7bdca0dcb4bbe8983157d555740db5ed229d5948 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Jul 2019 13:08:36 -0400 Subject: [PATCH 0852/1626] event logging on view page fixes --- src/abstractions/event.service.ts | 1 + src/angular/components/view.component.ts | 19 ++++++++++++------- src/models/data/eventData.ts | 1 + src/models/request/eventRequest.ts | 1 + src/services/event.service.ts | 16 +++++++++++++++- 5 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/abstractions/event.service.ts b/src/abstractions/event.service.ts index d677722512..40c080275f 100644 --- a/src/abstractions/event.service.ts +++ b/src/abstractions/event.service.ts @@ -3,4 +3,5 @@ import { EventType } from '../enums/eventType'; export abstract class EventService { collect: (eventType: EventType, cipherId?: string, uploadImmediately?: boolean) => Promise; uploadEvents: () => Promise; + clearEvents: () => Promise; } diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index f6985b977e..79591889b8 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -47,6 +47,7 @@ export class ViewComponent implements OnDestroy, OnInit { checkPasswordPromise: Promise; private totpInterval: any; + private previousCipherId: string; constructor(protected cipherService: CipherService, protected totpService: TotpService, protected tokenService: TokenService, protected i18nService: I18nService, @@ -57,7 +58,6 @@ export class ViewComponent implements OnDestroy, OnInit { protected eventService: EventService) { } ngOnInit() { - this.eventService.collect(EventType.Cipher_ClientViewed); this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { this.ngZone.run(async () => { switch (message.command) { @@ -94,6 +94,11 @@ export class ViewComponent implements OnDestroy, OnInit { await this.totpTick(interval); }, 1000); } + + if (this.previousCipherId !== this.cipherId) { + this.eventService.collect(EventType.Cipher_ClientViewed, this.cipherId); + } + this.previousCipherId = this.cipherId; } edit() { @@ -104,7 +109,7 @@ export class ViewComponent implements OnDestroy, OnInit { this.platformUtilsService.eventTrack('Toggled Password'); this.showPassword = !this.showPassword; if (this.showPassword) { - this.eventService.collect(EventType.Cipher_ClientToggledPasswordVisible); + this.eventService.collect(EventType.Cipher_ClientToggledPasswordVisible, this.cipherId); } } @@ -112,7 +117,7 @@ export class ViewComponent implements OnDestroy, OnInit { this.platformUtilsService.eventTrack('Toggled Card Code'); this.showCardCode = !this.showCardCode; if (this.showCardCode) { - this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible); + this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible, this.cipherId); } } @@ -137,7 +142,7 @@ export class ViewComponent implements OnDestroy, OnInit { const f = (field as any); f.showValue = !f.showValue; if (f.showValue) { - this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible); + this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, this.cipherId); } } @@ -162,11 +167,11 @@ export class ViewComponent implements OnDestroy, OnInit { this.i18nService.t('valueCopied', this.i18nService.t(typeI18nKey))); if (typeI18nKey === 'password') { - this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible); + this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, this.cipherId); } else if (typeI18nKey === 'securityCode') { - this.eventService.collect(EventType.Cipher_ClientCopiedCardCode); + this.eventService.collect(EventType.Cipher_ClientCopiedCardCode, this.cipherId); } else if (aType === 'H_Field') { - this.eventService.collect(EventType.Cipher_ClientCopiedHiddenField); + this.eventService.collect(EventType.Cipher_ClientCopiedHiddenField, this.cipherId); } } diff --git a/src/models/data/eventData.ts b/src/models/data/eventData.ts index 232fb8235c..f8639ad89c 100644 --- a/src/models/data/eventData.ts +++ b/src/models/data/eventData.ts @@ -3,4 +3,5 @@ import { EventType } from '../../enums/eventType'; export class EventData { type: EventType; cipherId: string; + date: string; } diff --git a/src/models/request/eventRequest.ts b/src/models/request/eventRequest.ts index f9f52d910e..83bfa3902a 100644 --- a/src/models/request/eventRequest.ts +++ b/src/models/request/eventRequest.ts @@ -3,4 +3,5 @@ import { EventType } from '../../enums/eventType'; export class EventRequest { type: EventType; cipherId: string; + date: string; } diff --git a/src/services/event.service.ts b/src/services/event.service.ts index b393546554..316073d3e5 100644 --- a/src/services/event.service.ts +++ b/src/services/event.service.ts @@ -31,6 +31,10 @@ export class EventService implements EventServiceAbstraction { } async collect(eventType: EventType, cipherId: string = null, uploadImmediately = false): Promise { + const authed = await this.userService.isAuthenticated(); + if (!authed) { + return; + } const organizations = await this.userService.getAllOrganizations(); if (organizations == null) { return; @@ -52,6 +56,7 @@ export class EventService implements EventServiceAbstraction { const event = new EventData(); event.type = eventType; event.cipherId = cipherId; + event.date = new Date().toISOString(); eventCollection.push(event); await this.storageService.save(ConstantsService.eventCollectionKey, eventCollection); if (uploadImmediately) { @@ -60,6 +65,10 @@ export class EventService implements EventServiceAbstraction { } async uploadEvents(): Promise { + const authed = await this.userService.isAuthenticated(); + if (!authed) { + return; + } const eventCollection = await this.storageService.get(ConstantsService.eventCollectionKey); if (eventCollection == null || eventCollection.length === 0) { return; @@ -68,11 +77,16 @@ export class EventService implements EventServiceAbstraction { const req = new EventRequest(); req.type = e.type; req.cipherId = e.cipherId; + req.date = e.date; return req; }); try { await this.apiService.postEventsCollect(request); - await this.storageService.remove(ConstantsService.eventCollectionKey); + this.clearEvents(); } catch { } } + + async clearEvents(): Promise { + await this.storageService.remove(ConstantsService.eventCollectionKey); + } } From 21e3026f048d4d834be9a53b1ecf31e890dcfaac Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 11 Jul 2019 23:05:38 -0400 Subject: [PATCH 0853/1626] heartbeat --- src/services/notifications.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index d88d9124a4..29f1ed5d19 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -46,6 +46,7 @@ export class NotificationsService implements NotificationsServiceAbstraction { if (this.signalrConnection != null) { this.signalrConnection.off('ReceiveMessage'); + this.signalrConnection.off('Heartbeat'); await this.signalrConnection.stop(); this.connected = false; this.signalrConnection = null; @@ -61,6 +62,8 @@ export class NotificationsService implements NotificationsServiceAbstraction { this.signalrConnection.on('ReceiveMessage', (data: any) => this.processNotification(new NotificationResponse(data))); + this.signalrConnection.on('Heartbeat', + (data: any) => { /*console.log('Heartbeat!');*/ }); this.signalrConnection.onclose(() => { this.connected = false; this.reconnect(true); From 803dec26e7dbc1a31906349e7b2c9ddd67ef6c8a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 12 Jul 2019 10:41:09 -0400 Subject: [PATCH 0854/1626] client events for edit page --- src/angular/components/add-edit.component.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index dbfd6b3b39..451abce825 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -10,6 +10,7 @@ import { } from '@angular/core'; import { CipherType } from '../../enums/cipherType'; +import { EventType } from '../../enums/eventType'; import { FieldType } from '../../enums/fieldType'; import { OrganizationUserStatusType } from '../../enums/organizationUserStatusType'; import { SecureNoteType } from '../../enums/secureNoteType'; @@ -18,6 +19,7 @@ import { UriMatchType } from '../../enums/uriMatchType'; import { AuditService } from '../../abstractions/audit.service'; import { CipherService } from '../../abstractions/cipher.service'; import { CollectionService } from '../../abstractions/collection.service'; +import { EventService } from '../../abstractions/event.service'; import { FolderService } from '../../abstractions/folder.service'; import { I18nService } from '../../abstractions/i18n.service'; import { MessagingService } from '../../abstractions/messaging.service'; @@ -75,12 +77,13 @@ export class AddEditComponent implements OnInit { ownershipOptions: any[] = []; protected writeableCollections: CollectionView[]; + private previousCipherId: string; constructor(protected cipherService: CipherService, protected folderService: FolderService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected auditService: AuditService, protected stateService: StateService, protected userService: UserService, protected collectionService: CollectionService, - protected messagingService: MessagingService) { + protected messagingService: MessagingService, protected eventService: EventService) { this.typeOptions = [ { name: i18nService.t('typeLogin'), value: CipherType.Login }, { name: i18nService.t('typeCard'), value: CipherType.Card }, @@ -199,6 +202,11 @@ export class AddEditComponent implements OnInit { } this.folders = await this.folderService.getAllDecrypted(); + + if (this.editMode && this.previousCipherId !== this.cipherId) { + this.eventService.collect(EventType.Cipher_ClientViewed, this.cipherId); + } + this.previousCipherId = this.cipherId; } async submit(): Promise { @@ -333,17 +341,26 @@ export class AddEditComponent implements OnInit { this.platformUtilsService.eventTrack('Toggled Password on Edit'); this.showPassword = !this.showPassword; document.getElementById('loginPassword').focus(); + if (this.editMode && this.showPassword) { + this.eventService.collect(EventType.Cipher_ClientToggledPasswordVisible, this.cipherId); + } } toggleCardCode() { this.platformUtilsService.eventTrack('Toggled CardCode on Edit'); this.showCardCode = !this.showCardCode; document.getElementById('cardCode').focus(); + if (this.editMode && this.showCardCode) { + this.eventService.collect(EventType.Cipher_ClientToggledCardCodeVisible, this.cipherId); + } } toggleFieldValue(field: FieldView) { const f = (field as any); f.showValue = !f.showValue; + if (this.editMode && f.showValue) { + this.eventService.collect(EventType.Cipher_ClientToggledHiddenFieldVisible, this.cipherId); + } } toggleUriOptions(uri: LoginUriView) { From 84aab0cb24b8418c625c28c79e8c190861531c5f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 12 Jul 2019 17:11:36 -0400 Subject: [PATCH 0855/1626] export vault event --- src/angular/components/export.component.ts | 9 ++++++++- src/enums/eventType.ts | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/angular/components/export.component.ts b/src/angular/components/export.component.ts index f85d4ef018..f7d3a0d31d 100644 --- a/src/angular/components/export.component.ts +++ b/src/angular/components/export.component.ts @@ -4,9 +4,11 @@ import { } from '@angular/core'; import { CryptoService } from '../../abstractions/crypto.service'; +import { EventService } from '../../abstractions/event.service'; import { ExportService } from '../../abstractions/export.service'; import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { EventType } from '../../enums/eventType'; export class ExportComponent { @Output() onSaved = new EventEmitter(); @@ -18,7 +20,7 @@ export class ExportComponent { constructor(protected cryptoService: CryptoService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected exportService: ExportService, - protected win: Window) { } + protected eventService: EventService, protected win: Window) { } async submit() { if (this.masterPassword == null || this.masterPassword === '') { @@ -36,6 +38,7 @@ export class ExportComponent { this.platformUtilsService.eventTrack('Exported Data'); this.downloadFile(data); this.saved(); + await this.collectEvent(); } catch { } } else { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), @@ -61,6 +64,10 @@ export class ExportComponent { return this.exportService.getFileName(prefix, this.format); } + protected async collectEvent(): Promise { + await this.eventService.collect(EventType.User_ClientExportedVault); + } + private downloadFile(csv: string): void { const fileName = this.getFileName(); this.platformUtilsService.saveFile(this.win, csv, { type: 'text/plain' }, fileName); diff --git a/src/enums/eventType.ts b/src/enums/eventType.ts index f133148742..3df5b693b7 100644 --- a/src/enums/eventType.ts +++ b/src/enums/eventType.ts @@ -40,4 +40,5 @@ export enum EventType { Organization_Updated = 1600, Organization_PurgedVault = 1601, + Organization_ClientExportedVault = 1602, } From d61794265a12076b765876b52cef597862727b1f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 12 Jul 2019 17:13:17 -0400 Subject: [PATCH 0856/1626] comment out Organization_ClientExportedVault --- src/enums/eventType.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/enums/eventType.ts b/src/enums/eventType.ts index 3df5b693b7..eb3b55e67b 100644 --- a/src/enums/eventType.ts +++ b/src/enums/eventType.ts @@ -40,5 +40,5 @@ export enum EventType { Organization_Updated = 1600, Organization_PurgedVault = 1601, - Organization_ClientExportedVault = 1602, + // Organization_ClientExportedVault = 1602, } From a3c9c7d41ea454438b3c3fcf1887e9997fc2727a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sun, 14 Jul 2019 11:25:26 -0400 Subject: [PATCH 0857/1626] disable event service for now --- src/services/event.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/event.service.ts b/src/services/event.service.ts index 316073d3e5..85f79f5afe 100644 --- a/src/services/event.service.ts +++ b/src/services/event.service.ts @@ -31,6 +31,7 @@ export class EventService implements EventServiceAbstraction { } async collect(eventType: EventType, cipherId: string = null, uploadImmediately = false): Promise { + return; const authed = await this.userService.isAuthenticated(); if (!authed) { return; @@ -65,6 +66,7 @@ export class EventService implements EventServiceAbstraction { } async uploadEvents(): Promise { + return; const authed = await this.userService.isAuthenticated(); if (!authed) { return; From f406a01900db90e80d70d47dd1d84961c000341f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 22 Jul 2019 14:15:03 -0400 Subject: [PATCH 0858/1626] support otp from safe in cloud import --- src/importers/safeInCloudXmlImporter.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/importers/safeInCloudXmlImporter.ts b/src/importers/safeInCloudXmlImporter.ts index 4029a2c979..7027ec1bff 100644 --- a/src/importers/safeInCloudXmlImporter.ts +++ b/src/importers/safeInCloudXmlImporter.ts @@ -71,6 +71,8 @@ export class SafeInCloudXmlImporter extends BaseImporter implements Importer { cipher.login.username = text; } else if (fieldType === 'password') { cipher.login.password = text; + } else if (fieldType === 'one_time_password') { + cipher.login.totp = text; } else if (fieldType === 'notes') { cipher.notes += (text + '\n'); } else if (fieldType === 'weblogin' || fieldType === 'website') { From 15cf7b433a9d2f51c4ff35fec7c8e1a8027f1e3a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 23 Jul 2019 08:28:53 -0400 Subject: [PATCH 0859/1626] delay search if indexing --- src/services/search.service.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 550a6ab933..1977924d5c 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -92,6 +92,13 @@ export class SearchService implements SearchServiceAbstraction { return ciphers; } + if (this.indexing) { + await new Promise((r) => setTimeout(r, 250)); + if (this.indexing) { + await new Promise((r) => setTimeout(r, 500)); + } + } + const index = this.getIndexForSearch(); if (index == null) { // Fall back to basic search if index is not available From e89f295e1d59040bf48ba599a900e3b42497008e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 24 Jul 2019 14:32:16 -0400 Subject: [PATCH 0860/1626] upgrade to electron 5 --- package-lock.json | 42 +++++++++++++------------------------ package.json | 2 +- src/electron/window.main.ts | 4 ++++ 3 files changed, 19 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index f0ca38f54f..3c580ed319 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1551,14 +1551,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1573,20 +1571,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -1703,8 +1698,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -1716,7 +1710,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -1731,7 +1724,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -1739,14 +1731,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -1765,7 +1755,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -1846,8 +1835,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -1859,7 +1847,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -1981,7 +1968,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2851,20 +2837,20 @@ "dev": true }, "electron": { - "version": "3.0.14", - "resolved": "https://registry.npmjs.org/electron/-/electron-3.0.14.tgz", - "integrity": "sha512-1fG9bE0LzL5QXeEq2MC0dHdVO0pbZOnNlVAIyOyJaCFAu/TjLhxQfWj38bFUEojzuVlaR87tZz0iy2qlVZj3sw==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/electron/-/electron-5.0.8.tgz", + "integrity": "sha512-wkUVE2GaYCsqQTsISSHWkIkcdpwLwZ1jhzAXSFFoSzsTgugmzhX60rJjIccotUmZ0iPzw+u4ahfcaJ0eslrPNQ==", "dev": true, "requires": { - "@types/node": "^8.0.24", + "@types/node": "^10.12.18", "electron-download": "^4.1.0", "extract-zip": "^1.0.3" }, "dependencies": { "@types/node": { - "version": "8.10.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.39.tgz", - "integrity": "sha512-rE7fktr02J8ybFf6eysife+WF+L4sAHWzw09DgdCebEu+qDwMvv4zl6Bc+825ttGZP73kCKxa3dhJOoGJ8+5mA==", + "version": "10.14.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.13.tgz", + "integrity": "sha512-yN/FNNW1UYsRR1wwAoyOwqvDuLDtVXnaJTZ898XIw/Q5cCaeVAlVwvsmXLX5PuiScBYwZsZU4JYSHB3TvfdwvQ==", "dev": true } } diff --git a/package.json b/package.json index 8563973a2d..927a120aa3 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@types/zxcvbn": "^4.4.0", "concurrently": "3.5.1", "cssstyle": "1.2.1", - "electron": "3.0.14", + "electron": "5.0.8", "jasmine": "^3.3.1", "jasmine-core": "^3.3.0", "jasmine-spec-reporter": "^4.2.1", diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index 6c7faae4a3..f57dc4154b 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -102,6 +102,10 @@ export class WindowMain { titleBarStyle: this.hideTitleBar && process.platform === 'darwin' ? 'hiddenInset' : undefined, show: false, alwaysOnTop: this.enableAlwaysOnTop, + webPreferences: { + nodeIntegration: true, + webviewTag: true, + }, }); if (this.windowStates[Keys.mainWindowSize].isMaximized) { From 0aae22fc00dc1d9ec96480d2262c5f35d0fd0d67 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 24 Jul 2019 14:40:57 -0400 Subject: [PATCH 0861/1626] enable events --- src/services/event.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/services/event.service.ts b/src/services/event.service.ts index 85f79f5afe..316073d3e5 100644 --- a/src/services/event.service.ts +++ b/src/services/event.service.ts @@ -31,7 +31,6 @@ export class EventService implements EventServiceAbstraction { } async collect(eventType: EventType, cipherId: string = null, uploadImmediately = false): Promise { - return; const authed = await this.userService.isAuthenticated(); if (!authed) { return; @@ -66,7 +65,6 @@ export class EventService implements EventServiceAbstraction { } async uploadEvents(): Promise { - return; const authed = await this.userService.isAuthenticated(); if (!authed) { return; From 4bf44fa5b88f33f3a34492292dd02e00a588009e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 24 Jul 2019 15:09:51 -0400 Subject: [PATCH 0862/1626] update electron updater --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 927a120aa3..a6a9ff668d 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", "electron-log": "2.2.17", "electron-store": "1.3.0", - "electron-updater": "4.0.6", + "electron-updater": "4.1.2", "form-data": "2.3.2", "https-proxy-agent": "2.2.1", "inquirer": "6.2.0", From ff13cb283896c68128fccf28ae65b89c090be522 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 25 Jul 2019 12:22:22 -0400 Subject: [PATCH 0863/1626] setComponentParameters from modal --- src/angular/components/modal.component.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/angular/components/modal.component.ts b/src/angular/components/modal.component.ts index d939cac1ee..d851710f24 100644 --- a/src/angular/components/modal.component.ts +++ b/src/angular/components/modal.component.ts @@ -32,7 +32,8 @@ export class ModalComponent implements OnDestroy { document.body.removeChild(document.querySelector('.modal-backdrop')); } - show(type: Type, parentContainer: ViewContainerRef, fade: boolean = true): T { + show(type: Type, parentContainer: ViewContainerRef, fade: boolean = true, + setComponentParameters: (component: T) => void = null): T { this.onShow.emit(); this.messagingService.send('modalShow'); this.parentContainer = parentContainer; @@ -45,6 +46,9 @@ export class ModalComponent implements OnDestroy { const factory = this.componentFactoryResolver.resolveComponentFactory(type); const componentRef = this.container.createComponent(factory); + if (setComponentParameters != null) { + setComponentParameters(componentRef.instance); + } document.querySelector('.modal-dialog').addEventListener('click', (e: Event) => { e.stopPropagation(); From 692d1ec20163edaa404b52888af2ea45b13b59cd Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 25 Jul 2019 20:41:28 -0400 Subject: [PATCH 0864/1626] upgrade signalr libs --- package-lock.json | 188 +++++++++++++++++++++++++++------------------- package.json | 4 +- 2 files changed, 114 insertions(+), 78 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3c580ed319..53bc303b3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,14 +94,29 @@ } }, "@aspnet/signalr": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@aspnet/signalr/-/signalr-1.0.4.tgz", - "integrity": "sha512-q7HMlTZPkZCa/0UclsXvEyqNirpjRfRuwhjEeADD1i6pqe0Yx5OwuCO7+Xsc6MNKR8vE1C9MyxnSj0SecvUbTA==" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@aspnet/signalr/-/signalr-1.1.4.tgz", + "integrity": "sha512-Jp9nPc8hmmhbG9OKiHe2fOKskBHfg+3Y9foSKHxjgGtyI743hXjGFv3uFlUg503K9f8Ilu63gQt3fDkLICBRyg==", + "requires": { + "eventsource": "^1.0.7", + "request": "^2.88.0", + "ws": "^6.0.0" + }, + "dependencies": { + "ws": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } }, "@aspnet/signalr-protocol-msgpack": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@aspnet/signalr-protocol-msgpack/-/signalr-protocol-msgpack-1.0.4.tgz", - "integrity": "sha512-nwGwkroojOLmjvV4nybfBqw3GS488Mx9hYoY5pMDUKNSeNOPCejNeCmdFPf1c/xAnV24eC4O1XKolA7IGI7Fzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@aspnet/signalr-protocol-msgpack/-/signalr-protocol-msgpack-1.1.0.tgz", + "integrity": "sha512-AQv5AavWvoFz2iLSIDK1DAIXMNhQ1Jt1qRDouXxLKAKP13u8iFq7i3/MwJ30ShOBGBoL5/zn6pBlNjAzTmAsMA==", "requires": { "msgpack5": "^4.0.2" } @@ -460,6 +475,11 @@ "@types/rx-lite": "*" } }, + "@types/semver": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.0.1.tgz", + "integrity": "sha512-ffCdcrEE5h8DqVxinQjo+2d1q+FV5z7iNtPofw3JsrltSoSVlOGaW0rY8XxtO9XukdTn8TaCGWmk2VFGhI70mg==" + }, "@types/through": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.29.tgz", @@ -1046,21 +1066,6 @@ "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==", "dev": true }, - "bluebird-lst": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.6.tgz", - "integrity": "sha512-CBWFoPuUPpcvMUxfyr8DKdI5d4kjxFl1h39+VbKxP3KJWJHEsLtuT4pPLkjpxCGU6Ask21tvbnftWXdqIxYldQ==", - "requires": { - "bluebird": "^3.5.2" - }, - "dependencies": { - "bluebird": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", - "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==" - } - } - }, "bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", @@ -1344,7 +1349,8 @@ "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true }, "buffer-xor": { "version": "1.0.3", @@ -1353,13 +1359,11 @@ "dev": true }, "builder-util-runtime": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.1.1.tgz", - "integrity": "sha512-+ieS4PMB33vVE2S3ZNWBEQJ1zKmAs/agrBdh7XadE1lKLjrH4aXYuOh9OOGdxqIRldhlhNBaF+yKMMEFOdNVig==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.3.0.tgz", + "integrity": "sha512-CSOdsYqf4RXIHh1HANPbrZHlZ9JQJXSuDDloblZPcWQVN62inyYoTQuSmY3KrgefME2Sv3Kn2MxHvbGQHRf8Iw==", "requires": { - "bluebird-lst": "^1.0.6", "debug": "^4.1.1", - "fs-extra-p": "^7.0.0", "sax": "^1.2.4" }, "dependencies": { @@ -1372,9 +1376,9 @@ } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -2905,30 +2909,48 @@ } }, "electron-updater": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.0.6.tgz", - "integrity": "sha512-JPGLME6fxJcHG8hX7HWFl6Aew6iVm0DkcrENreKa5SUJCHG+uUaAhxDGDt+YGcNkyx1uJ6eBGMvFxDTLUv67pg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.1.2.tgz", + "integrity": "sha512-4Sk8IW0LfOilDz+WAB/gEDmX7+FUFRbKHGN1zGjehPilnd6H9cmjgBHK6Xzq/FLq/uOHGJ6GX/9tsF+jr7CvnA==", "requires": { - "bluebird-lst": "^1.0.6", - "builder-util-runtime": "~8.1.0", - "fs-extra-p": "^7.0.0", - "js-yaml": "^3.12.0", - "lazy-val": "^1.0.3", + "@types/semver": "^6.0.1", + "builder-util-runtime": "8.3.0", + "fs-extra": "^8.1.0", + "js-yaml": "^3.13.1", + "lazy-val": "^1.0.4", "lodash.isequal": "^4.5.0", - "pako": "^1.0.7", - "semver": "^5.6.0", - "source-map-support": "^0.5.9" + "pako": "^1.0.10", + "semver": "^6.2.0" }, "dependencies": { - "pako": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.8.tgz", - "integrity": "sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA==" + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "graceful-fs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz", + "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==" + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } }, "semver": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, @@ -3142,6 +3164,14 @@ "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", "dev": true }, + "eventsource": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", + "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", + "requires": { + "original": "^1.0.0" + } + }, "evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -3513,21 +3543,13 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, "requires": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, - "fs-extra-p": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-7.0.0.tgz", - "integrity": "sha512-5tg5jBOd0xIXjwj4PDnafOXL5TyPVzjxLby4DPKev53wurEXp7IsojBaD4Lj5M5w7jxw0pbkEU0fFEPmcKoMnA==", - "requires": { - "bluebird-lst": "^1.0.6", - "fs-extra": "^7.0.0" - } - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3582,6 +3604,7 @@ "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3750,6 +3773,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5028,6 +5052,7 @@ "version": "3.13.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.0.tgz", "integrity": "sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==", + "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -5431,9 +5456,9 @@ } }, "lazy-val": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.3.tgz", - "integrity": "sha512-pjCf3BYk+uv3ZcPzEVM0BFvO9Uw58TmlrU0oG5tTrr9Kcid3+kdKxapH8CjdYmVa2nO5wOoZn2rdvZx2PKj/xg==" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.4.tgz", + "integrity": "sha512-u93kb2fPbIrfzBuLjZE+w+fJbUUMhNDXxNmMfaqNgpfQf1CO5ZSe2LfsnBqVAk7i/2NF48OSoRj+Xe2VT+lE8Q==" }, "levn": { "version": "0.3.0", @@ -5817,9 +5842,9 @@ }, "dependencies": { "bl": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bl/-/bl-2.1.1.tgz", - "integrity": "sha512-YTmzlmPyCuKGFSTLL3P7nlZHk+CDC3ddehCT+/ZwcI35jUjnpfSXlrAAr3hIEoD/+4TvKy27vMp7yc/q8aa6tA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.0.tgz", + "integrity": "sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==", "requires": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" @@ -6719,6 +6744,14 @@ } } }, + "original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "requires": { + "url-parse": "^1.4.3" + } + }, "os-browserify": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", @@ -6786,8 +6819,7 @@ "pako": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", - "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", - "dev": true + "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==" }, "papaparse": { "version": "4.6.0", @@ -7180,6 +7212,11 @@ "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", "dev": true }, + "querystringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", + "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -7440,8 +7477,7 @@ "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, "resolve": { "version": "1.8.1", @@ -7868,15 +7904,6 @@ "urix": "^0.1.0" } }, - "source-map-support": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.10.tgz", - "integrity": "sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ==", - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "source-map-url": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", @@ -8849,6 +8876,15 @@ } } }, + "url-parse": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "url-parse-lax": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", diff --git a/package.json b/package.json index a6a9ff668d..40abfc0471 100644 --- a/package.json +++ b/package.json @@ -73,8 +73,8 @@ "@angular/platform-browser-dynamic": "7.2.1", "@angular/router": "7.2.1", "@angular/upgrade": "7.2.1", - "@aspnet/signalr": "1.0.4", - "@aspnet/signalr-protocol-msgpack": "1.0.4", + "@aspnet/signalr": "1.1.4", + "@aspnet/signalr-protocol-msgpack": "1.1.0", "big-integer": "1.6.36", "chalk": "2.4.1", "commander": "2.18.0", From 6372d2410424e5ef04acd962598d242d2bce905e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 31 Jul 2019 23:41:13 -0400 Subject: [PATCH 0865/1626] update keytar --- package-lock.json | 39 ++++++++++++++++++++++++--------------- package.json | 2 +- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 53bc303b3f..03f1566ed8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2069,9 +2069,9 @@ } }, "chownr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", + "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==" }, "ci-info": { "version": "1.1.3", @@ -5429,12 +5429,19 @@ } }, "keytar": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-4.4.1.tgz", - "integrity": "sha512-6xEe7ybXSR5EZC+z0GI2yqLYZjV1tyPQY2xSZ8rGsBxrrLEh8VR/Lfqv59uGX+I+W+OZxH0jCXN1dU1++ify4g==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-4.13.0.tgz", + "integrity": "sha512-qdyZ3XDuv11ANDXJ+shsmc+j/h5BHPDSn33MwkUMDg2EA++xEBleNkghr3Jg95cqVx5WgDYD8V/m3Q0y7kwQ2w==", "requires": { - "nan": "2.12.1", - "prebuild-install": "5.2.4" + "nan": "2.14.0", + "prebuild-install": "5.3.0" + }, + "dependencies": { + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + } } }, "kind-of": { @@ -5860,7 +5867,9 @@ "nan": { "version": "2.12.1", "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz", - "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==" + "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==", + "dev": true, + "optional": true }, "nanomatch": { "version": "1.2.13", @@ -5946,9 +5955,9 @@ } }, "node-abi": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.7.1.tgz", - "integrity": "sha512-OV8Bq1OrPh6z+Y4dqwo05HqrRL9YNF7QVMRfq1/pguwKLG+q9UB/Lk0x5qXjO23JjJg+/jqCHSTaG1P3tfKfuw==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.10.0.tgz", + "integrity": "sha512-OT0WepUvYHXdki6DU8LWhEkuo3M44i2paWBYtH9qXtPb9YiKlYEKa5WUII20XEcOv7UJPzfB0kZfPZdW46zdkw==", "requires": { "semver": "^5.4.1" } @@ -7068,9 +7077,9 @@ "dev": true }, "prebuild-install": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.2.4.tgz", - "integrity": "sha512-CG3JnpTZXdmr92GW4zbcba4jkDha6uHraJ7hW4Fn8j0mExxwOKK20hqho8ZuBDCKYCHYIkFM1P2jhtG+KpP4fg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.0.tgz", + "integrity": "sha512-aaLVANlj4HgZweKttFNUVNRxDukytuIuxeK2boIMHjagNJCiVKWFsKF4tCE3ql3GbrD2tExPQ7/pwtEJcHNZeg==", "requires": { "detect-libc": "^1.0.3", "expand-template": "^2.0.3", diff --git a/package.json b/package.json index 40abfc0471..4dd8100042 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "https-proxy-agent": "2.2.1", "inquirer": "6.2.0", "jsdom": "13.2.0", - "keytar": "4.4.1", + "keytar": "4.13.0", "lowdb": "1.0.0", "lunr": "2.3.3", "ngx-infinite-scroll": "7.0.1", From ee91cfc2dfda974ab1ae8e0077e58998bbc6e832 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 2 Aug 2019 09:51:00 -0400 Subject: [PATCH 0866/1626] password agent import updates for alt format --- src/importers/passwordAgentCsvImporter.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/importers/passwordAgentCsvImporter.ts b/src/importers/passwordAgentCsvImporter.ts index 6095fcbb52..e1d1018368 100644 --- a/src/importers/passwordAgentCsvImporter.ts +++ b/src/importers/passwordAgentCsvImporter.ts @@ -17,19 +17,24 @@ export class PasswordAgentCsvImporter extends BaseImporter implements Importer { if (value.length !== 5 && value.length < 9) { return; } + const altFormat = value.length === 10 && value[0] === '0'; const cipher = this.initLoginCipher(); - cipher.name = this.getValueOrDefault(value[0], '--'); - cipher.login.username = this.getValueOrDefault(value[1]); - cipher.login.password = this.getValueOrDefault(value[2]); + cipher.name = this.getValueOrDefault(value[altFormat ? 1 : 0], '--'); + cipher.login.username = this.getValueOrDefault(value[altFormat ? 2 : 1]); + cipher.login.password = this.getValueOrDefault(value[altFormat ? 3 : 2]); if (value.length === 5) { newVersion = false; cipher.notes = this.getValueOrDefault(value[4]); cipher.login.uris = this.makeUriArray(value[3]); } else { - const folder = this.getValueOrDefault(value[8], '(None)'); - const folderName = folder !== '(None)' ? folder.split('\\').join('/') : null; + const folder = this.getValueOrDefault(value[altFormat ? 9 : 8], '(None)'); + let folderName = folder !== '(None)' ? folder.split('\\').join('/') : null; + if (folderName != null) { + folderName = folder.split(' > ').join('/'); + folderName = folder.split('>').join('/'); + } this.processFolder(result, folderName); - cipher.notes = this.getValueOrDefault(value[3]); + cipher.notes = this.getValueOrDefault(value[altFormat ? 5 : 3]); cipher.login.uris = this.makeUriArray(value[4]); } this.convertToNoteIfNeeded(cipher); From 5c1b80ee87f74f01ea86ac9dd600875ad7b07e0f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 3 Aug 2019 19:59:01 -0400 Subject: [PATCH 0867/1626] roboform convertToNoteIfNeeded --- src/importers/roboformCsvImporter.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/importers/roboformCsvImporter.ts b/src/importers/roboformCsvImporter.ts index 056479db83..09a97997b3 100644 --- a/src/importers/roboformCsvImporter.ts +++ b/src/importers/roboformCsvImporter.ts @@ -42,6 +42,7 @@ export class RoboFormCsvImporter extends BaseImporter implements Importer { }); } + this.convertToNoteIfNeeded(cipher); this.cleanupCipher(cipher); if (i === results.length && cipher.name === '--' && this.isNullOrWhitespace(cipher.login.password)) { From 393f6c9c20fb7eded5008b65242ea44e69bc349c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 5 Aug 2019 08:37:20 -0400 Subject: [PATCH 0868/1626] set 100ms timeout on removing tray to fix crash --- src/electron/tray.main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/electron/tray.main.ts b/src/electron/tray.main.ts index e7418e06d9..7e31b632fd 100644 --- a/src/electron/tray.main.ts +++ b/src/electron/tray.main.ts @@ -80,7 +80,7 @@ export class TrayMain { this.windowMain.win.on('show', async (e: Event) => { const enableTray = await this.storageService.get(ElectronConstants.enableTrayKey); if (!enableTray) { - this.removeTray(false); + setTimeout(() => this.removeTray(false), 100); } }); } From e28e820286870df0111bd4feb8a219a3ae360ba7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 9 Aug 2019 23:56:55 -0400 Subject: [PATCH 0869/1626] new payment api changes --- src/abstractions/api.service.ts | 6 ++++-- src/models/response/paymentResponse.ts | 18 ++++++++++++++++++ src/services/api.service.ts | 16 ++++++++++++---- 3 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 src/models/response/paymentResponse.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 13b424dea4..c00fb72b5c 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -78,6 +78,7 @@ import { OrganizationUserDetailsResponse, OrganizationUserUserDetailsResponse, } from '../models/response/organizationUserResponse'; +import { PaymentResponse } from '../models/response/paymentResponse'; import { PreloginResponse } from '../models/response/preloginResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SelectionReadOnlyResponse } from '../models/response/selectionReadOnlyResponse'; @@ -118,7 +119,7 @@ export abstract class ApiService { getAccountRevisionDate: () => Promise; postPasswordHint: (request: PasswordHintRequest) => Promise; postRegister: (request: RegisterRequest) => Promise; - postPremium: (data: FormData) => Promise; + postPremium: (data: FormData) => Promise; postReinstatePremium: () => Promise; postCancelPremium: () => Promise; postAccountStorage: (request: StorageRequest) => Promise; @@ -241,7 +242,7 @@ export abstract class ApiService { postOrganizationLicenseUpdate: (id: string, data: FormData) => Promise; postOrganizationApiKey: (id: string, request: PasswordVerificationRequest) => Promise; postOrganizationRotateApiKey: (id: string, request: PasswordVerificationRequest) => Promise; - postOrganizationUpgrade: (id: string, request: OrganizationUpgradeRequest) => Promise; + postOrganizationUpgrade: (id: string, request: OrganizationUpgradeRequest) => Promise; postOrganizationSeat: (id: string, request: SeatRequest) => Promise; postOrganizationStorage: (id: string, request: StorageRequest) => Promise; postOrganizationPayment: (id: string, request: PaymentRequest) => Promise; @@ -263,6 +264,7 @@ export abstract class ApiService { getHibpBreach: (username: string) => Promise; postBitPayInvoice: (request: BitPayInvoiceRequest) => Promise; + postSetupPayment: () => Promise; getActiveBearerToken: () => Promise; fetch: (request: Request) => Promise; diff --git a/src/models/response/paymentResponse.ts b/src/models/response/paymentResponse.ts new file mode 100644 index 0000000000..f4cc67452a --- /dev/null +++ b/src/models/response/paymentResponse.ts @@ -0,0 +1,18 @@ +import { BaseResponse } from './baseResponse'; +import { ProfileResponse } from './profileResponse'; + +export class PaymentResponse extends BaseResponse { + userProfile: ProfileResponse; + paymentIntentClientSecret: string; + success: boolean; + + constructor(response: any) { + super(response); + const userProfile = this.getResponseProperty('UserProfile'); + if (userProfile != null) { + this.userProfile = new ProfileResponse(userProfile); + } + this.paymentIntentClientSecret = this.getResponseProperty('PaymentIntentClientSecret'); + this.success = this.getResponseProperty('Success'); + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index ab840ca5a6..911af209b4 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -85,6 +85,7 @@ import { OrganizationUserDetailsResponse, OrganizationUserUserDetailsResponse, } from '../models/response/organizationUserResponse'; +import { PaymentResponse } from '../models/response/paymentResponse'; import { PreloginResponse } from '../models/response/preloginResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SelectionReadOnlyResponse } from '../models/response/selectionReadOnlyResponse'; @@ -253,8 +254,9 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/accounts/register', request, false, false); } - postPremium(data: FormData): Promise { - return this.send('POST', '/accounts/premium', data, true, false); + async postPremium(data: FormData): Promise { + const r = await this.send('POST', '/accounts/premium', data, true, true); + return new PaymentResponse(r); } postReinstatePremium(): Promise { @@ -780,8 +782,9 @@ export class ApiService implements ApiServiceAbstraction { return new ApiKeyResponse(r); } - postOrganizationUpgrade(id: string, request: OrganizationUpgradeRequest): Promise { - return this.send('POST', '/organizations/' + id + '/upgrade', request, true, false); + async postOrganizationUpgrade(id: string, request: OrganizationUpgradeRequest): Promise { + const r = await this.send('POST', '/organizations/' + id + '/upgrade', request, true, true); + return new PaymentResponse(r); } postOrganizationSeat(id: string, request: SeatRequest): Promise { @@ -881,6 +884,11 @@ export class ApiService implements ApiServiceAbstraction { return r as string; } + async postSetupPayment(): Promise { + const r = await this.send('POST', '/setup-payment', null, true, true); + return r as string; + } + // Helpers async getActiveBearerToken(): Promise { From de9bcac0ec9f4429c17af4b952f58a2054aad02e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 10 Aug 2019 12:59:47 -0400 Subject: [PATCH 0870/1626] adjust storage with payment intent/method handling --- src/abstractions/api.service.ts | 2 +- src/services/api.service.ts | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index c00fb72b5c..89ba3abbd2 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -122,7 +122,7 @@ export abstract class ApiService { postPremium: (data: FormData) => Promise; postReinstatePremium: () => Promise; postCancelPremium: () => Promise; - postAccountStorage: (request: StorageRequest) => Promise; + postAccountStorage: (request: StorageRequest) => Promise; postAccountPayment: (request: PaymentRequest) => Promise; postAccountLicense: (data: FormData) => Promise; postAccountKey: (request: UpdateKeyRequest) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 911af209b4..8ca3c1875b 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -267,8 +267,9 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/accounts/cancel-premium', null, true, false); } - postAccountStorage(request: StorageRequest): Promise { - return this.send('POST', '/accounts/storage', request, true, false); + async postAccountStorage(request: StorageRequest): Promise { + const r = await this.send('POST', '/accounts/storage', request, true, true); + return new PaymentResponse(r); } postAccountPayment(request: PaymentRequest): Promise { @@ -791,8 +792,9 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/organizations/' + id + '/seat', request, true, false); } - postOrganizationStorage(id: string, request: StorageRequest): Promise { - return this.send('POST', '/organizations/' + id + '/storage', request, true, false); + async postOrganizationStorage(id: string, request: StorageRequest): Promise { + const r = await this.send('POST', '/organizations/' + id + '/storage', request, true, true); + return new PaymentResponse(r); } postOrganizationPayment(id: string, request: PaymentRequest): Promise { From 4f876fc222a0b6b5156139f43a4623f6fa083465 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 10 Aug 2019 13:14:53 -0400 Subject: [PATCH 0871/1626] payment response for adjust seats --- src/abstractions/api.service.ts | 2 +- src/services/api.service.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 89ba3abbd2..f627deb074 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -243,7 +243,7 @@ export abstract class ApiService { postOrganizationApiKey: (id: string, request: PasswordVerificationRequest) => Promise; postOrganizationRotateApiKey: (id: string, request: PasswordVerificationRequest) => Promise; postOrganizationUpgrade: (id: string, request: OrganizationUpgradeRequest) => Promise; - postOrganizationSeat: (id: string, request: SeatRequest) => Promise; + postOrganizationSeat: (id: string, request: SeatRequest) => Promise; postOrganizationStorage: (id: string, request: StorageRequest) => Promise; postOrganizationPayment: (id: string, request: PaymentRequest) => Promise; postOrganizationVerifyBank: (id: string, request: VerifyBankRequest) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 8ca3c1875b..97e6e216fa 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -788,8 +788,9 @@ export class ApiService implements ApiServiceAbstraction { return new PaymentResponse(r); } - postOrganizationSeat(id: string, request: SeatRequest): Promise { - return this.send('POST', '/organizations/' + id + '/seat', request, true, false); + async postOrganizationSeat(id: string, request: SeatRequest): Promise { + const r = await this.send('POST', '/organizations/' + id + '/seat', request, true, true); + return new PaymentResponse(r); } async postOrganizationStorage(id: string, request: StorageRequest): Promise { From e96fa17b61404adab571e38f47c3612cc02df3ce Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 15 Aug 2019 14:58:17 -0400 Subject: [PATCH 0872/1626] dont use credentials for safari --- src/services/api.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 97e6e216fa..a6184b86a3 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -113,6 +113,7 @@ export class ApiService implements ApiServiceAbstraction { private isWebClient = false; private isDesktopClient = false; private usingBaseUrl = false; + private useCredentials = true; constructor(private tokenService: TokenService, private platformUtilsService: PlatformUtilsService, private logoutCallback: (expired: boolean) => Promise) { @@ -124,6 +125,7 @@ export class ApiService implements ApiServiceAbstraction { device === DeviceType.UnknownBrowser || device === DeviceType.VivaldiBrowser; this.isDesktopClient = device === DeviceType.WindowsDesktop || device === DeviceType.MacOsDesktop || device === DeviceType.LinuxDesktop; + this.useCredentials = device !== DeviceType.SafariBrowser; } setUrls(urls: EnvironmentUrls): void { @@ -1015,7 +1017,7 @@ export class ApiService implements ApiServiceAbstraction { } private getCredentials(): RequestCredentials { - if (!this.isWebClient || this.usingBaseUrl) { + if (this.useCredentials && (!this.isWebClient || this.usingBaseUrl)) { return 'include'; } return undefined; From 640c44820a2e26b6e34591b59bc168a6eb455e83 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 15 Aug 2019 15:17:48 -0400 Subject: [PATCH 0873/1626] SafariExtension not browser --- src/services/api.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/api.service.ts b/src/services/api.service.ts index a6184b86a3..e186f605b1 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -125,7 +125,7 @@ export class ApiService implements ApiServiceAbstraction { device === DeviceType.UnknownBrowser || device === DeviceType.VivaldiBrowser; this.isDesktopClient = device === DeviceType.WindowsDesktop || device === DeviceType.MacOsDesktop || device === DeviceType.LinuxDesktop; - this.useCredentials = device !== DeviceType.SafariBrowser; + this.useCredentials = device !== DeviceType.SafariExtension; } setUrls(urls: EnvironmentUrls): void { From 1f98a2ebcb1f08dca39047fe77d8dea4bb1a67dc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 16 Aug 2019 20:54:21 -0400 Subject: [PATCH 0874/1626] always set on subscribe --- src/services/broadcaster.service.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/services/broadcaster.service.ts b/src/services/broadcaster.service.ts index 9f46ccc6f3..beda9c1c55 100644 --- a/src/services/broadcaster.service.ts +++ b/src/services/broadcaster.service.ts @@ -17,10 +17,6 @@ export class BroadcasterService implements BroadcasterServiceAbstraction { } subscribe(id: string, messageCallback: (message: any) => any) { - if (this.subscribers.has(id)) { - return; - } - this.subscribers.set(id, messageCallback); } From ae37c2198bd2cb4799f76592c7fa49be154135d3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 20 Aug 2019 13:47:15 -0400 Subject: [PATCH 0875/1626] isViewOpen returns promise --- src/abstractions/platformUtils.service.ts | 2 +- src/cli/services/cliPlatformUtils.service.ts | 2 +- src/electron/services/electronPlatformUtils.service.ts | 4 ++-- src/services/lock.service.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index 38204ab899..188bd80090 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -13,7 +13,7 @@ export abstract class PlatformUtilsService { isIE: () => boolean; isMacAppStore: () => boolean; analyticsId: () => string; - isViewOpen: () => boolean; + isViewOpen: () => Promise; lockTimeout: () => number; launchUri: (uri: string, options?: any) => void; saveFile: (win: Window, blobData: any, blobOptions: any, fileName: string) => void; diff --git a/src/cli/services/cliPlatformUtils.service.ts b/src/cli/services/cliPlatformUtils.service.ts index ad4f69b6fc..d7564c9b84 100644 --- a/src/cli/services/cliPlatformUtils.service.ts +++ b/src/cli/services/cliPlatformUtils.service.ts @@ -73,7 +73,7 @@ export class CliPlatformUtilsService implements PlatformUtilsService { } isViewOpen() { - return false; + return Promise.resolve(false); } lockTimeout(): number { diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index 4539b982e0..877cbfcf4f 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -98,8 +98,8 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { return this.analyticsIdCache; } - isViewOpen(): boolean { - return false; + isViewOpen(): Promise { + return Promise.resolve(false); } lockTimeout(): number { diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts index 0c091d9fc0..851838b806 100644 --- a/src/services/lock.service.ts +++ b/src/services/lock.service.ts @@ -44,7 +44,7 @@ export class LockService implements LockServiceAbstraction { } async checkLock(): Promise { - if (this.platformUtilsService.isViewOpen()) { + if (await this.platformUtilsService.isViewOpen()) { // Do not lock return; } From 2ab6b9f330144ef028c85ca3bbe210a28d012383 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 21 Aug 2019 09:50:56 -0400 Subject: [PATCH 0876/1626] allow duo for safari extension --- src/angular/components/two-factor.component.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 66eb59f9eb..63f2ac0257 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -125,9 +125,11 @@ export class TwoFactorComponent implements OnInit, OnDestroy { break; case TwoFactorProviderType.Duo: case TwoFactorProviderType.OrganizationDuo: + /* if (this.platformUtilsService.getDevice() === DeviceType.SafariExtension) { break; } + */ setTimeout(() => { DuoWebSDK.init({ From 94a12f76448fd21cb10b9eb2c5ff8086f043baf0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 21 Aug 2019 10:05:00 -0400 Subject: [PATCH 0877/1626] remove safari specific code from 2fa page --- src/angular/components/two-factor.component.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 63f2ac0257..9d3379405c 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -125,12 +125,6 @@ export class TwoFactorComponent implements OnInit, OnDestroy { break; case TwoFactorProviderType.Duo: case TwoFactorProviderType.OrganizationDuo: - /* - if (this.platformUtilsService.getDevice() === DeviceType.SafariExtension) { - break; - } - */ - setTimeout(() => { DuoWebSDK.init({ iframe: undefined, From 8a0d371d2029622a005d2eddc14de44b1a091da3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 21 Aug 2019 23:13:08 -0400 Subject: [PATCH 0878/1626] securesafe csv importer --- src/importers/secureSafeCsvImporter.ts | 29 ++++++++++++++++++++++++++ src/services/import.service.ts | 4 ++++ 2 files changed, 33 insertions(+) create mode 100644 src/importers/secureSafeCsvImporter.ts diff --git a/src/importers/secureSafeCsvImporter.ts b/src/importers/secureSafeCsvImporter.ts new file mode 100644 index 0000000000..a0cabb5e21 --- /dev/null +++ b/src/importers/secureSafeCsvImporter.ts @@ -0,0 +1,29 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +export class SecureSafeCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(value.Title); + cipher.notes = this.getValueOrDefault(value.Comment); + cipher.login.uris = this.makeUriArray(value.Url); + cipher.login.password = this.getValueOrDefault(value.Password); + cipher.login.username = this.getValueOrDefault(value.Username); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} diff --git a/src/services/import.service.ts b/src/services/import.service.ts index bb8baae8d7..47ad31a9be 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -61,6 +61,7 @@ import { RememBearCsvImporter } from '../importers/rememBearCsvImporter'; import { RoboFormCsvImporter } from '../importers/roboformCsvImporter'; import { SafeInCloudXmlImporter } from '../importers/safeInCloudXmlImporter'; import { SaferPassCsvImporter } from '../importers/saferpassCsvImport'; +import { SecureSafeCsvImporter } from '../importers/secureSafeCsvImporter'; import { SplashIdCsvImporter } from '../importers/splashIdCsvImporter'; import { StickyPasswordXmlImporter } from '../importers/stickyPasswordXmlImporter'; import { TrueKeyCsvImporter } from '../importers/truekeyCsvImporter'; @@ -117,6 +118,7 @@ export class ImportService implements ImportServiceAbstraction { { id: 'remembearcsv', name: 'RememBear (csv)' }, { id: 'passwordwallettxt', name: 'PasswordWallet (txt)' }, { id: 'mykicsv', name: 'Myki (csv)' }, + { id: 'securesafecsv', name: 'SecureSafe (csv)' }, ]; constructor(private cipherService: CipherService, private folderService: FolderService, @@ -251,6 +253,8 @@ export class ImportService implements ImportServiceAbstraction { return new PasswordWalletTxtImporter(); case 'mykicsv': return new MykiCsvImporter(); + case 'securesafecsv': + return new SecureSafeCsvImporter(); default: return null; } From fbc7d6c2bcd3db2833ce6cfb11adea1960867cba Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 22 Aug 2019 12:04:15 -0400 Subject: [PATCH 0879/1626] get rid of useCredentials variable --- src/services/api.service.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/services/api.service.ts b/src/services/api.service.ts index e186f605b1..7d9d77eeb3 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -109,23 +109,22 @@ export class ApiService implements ApiServiceAbstraction { identityBaseUrl: string; eventsBaseUrl: string; + private device: DeviceType; private deviceType: string; private isWebClient = false; private isDesktopClient = false; private usingBaseUrl = false; - private useCredentials = true; constructor(private tokenService: TokenService, private platformUtilsService: PlatformUtilsService, private logoutCallback: (expired: boolean) => Promise) { - const device = platformUtilsService.getDevice(); - this.deviceType = device.toString(); - this.isWebClient = device === DeviceType.IEBrowser || device === DeviceType.ChromeBrowser || - device === DeviceType.EdgeBrowser || device === DeviceType.FirefoxBrowser || - device === DeviceType.OperaBrowser || device === DeviceType.SafariBrowser || - device === DeviceType.UnknownBrowser || device === DeviceType.VivaldiBrowser; - this.isDesktopClient = device === DeviceType.WindowsDesktop || device === DeviceType.MacOsDesktop || - device === DeviceType.LinuxDesktop; - this.useCredentials = device !== DeviceType.SafariExtension; + this.device = platformUtilsService.getDevice(); + this.deviceType = this.device.toString(); + this.isWebClient = this.device === DeviceType.IEBrowser || this.device === DeviceType.ChromeBrowser || + this.device === DeviceType.EdgeBrowser || this.device === DeviceType.FirefoxBrowser || + this.device === DeviceType.OperaBrowser || this.device === DeviceType.SafariBrowser || + this.device === DeviceType.UnknownBrowser || this.device === DeviceType.VivaldiBrowser; + this.isDesktopClient = this.device === DeviceType.WindowsDesktop || this.device === DeviceType.MacOsDesktop || + this.device === DeviceType.LinuxDesktop; } setUrls(urls: EnvironmentUrls): void { @@ -1017,7 +1016,7 @@ export class ApiService implements ApiServiceAbstraction { } private getCredentials(): RequestCredentials { - if (this.useCredentials && (!this.isWebClient || this.usingBaseUrl)) { + if (this.device !== DeviceType.SafariExtension && (!this.isWebClient || this.usingBaseUrl)) { return 'include'; } return undefined; From 99d56d936fdebc99994ce5089e8f063729a2e0ec Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 26 Aug 2019 10:06:20 -0400 Subject: [PATCH 0880/1626] logmeonce csv importer --- src/importers/logMeOnceCsvImporter.ts | 31 +++++++++++++++++++++++++++ src/services/import.service.ts | 4 ++++ 2 files changed, 35 insertions(+) create mode 100644 src/importers/logMeOnceCsvImporter.ts diff --git a/src/importers/logMeOnceCsvImporter.ts b/src/importers/logMeOnceCsvImporter.ts new file mode 100644 index 0000000000..d7ff04e8a0 --- /dev/null +++ b/src/importers/logMeOnceCsvImporter.ts @@ -0,0 +1,31 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +export class LogMeOnceCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, false); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + if (value.length < 4) { + return; + } + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(value[0], '--'); + cipher.login.username = this.getValueOrDefault(value[2]); + cipher.login.password = this.getValueOrDefault(value[3]); + cipher.login.uris = this.makeUriArray(value[1]); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} diff --git a/src/services/import.service.ts b/src/services/import.service.ts index 47ad31a9be..1a1ac5a92d 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -43,6 +43,7 @@ import { KeePass2XmlImporter } from '../importers/keepass2XmlImporter'; import { KeePassXCsvImporter } from '../importers/keepassxCsvImporter'; import { KeeperCsvImporter } from '../importers/keeperCsvImporter'; import { LastPassCsvImporter } from '../importers/lastpassCsvImporter'; +import { LogMeOnceCsvImporter } from '../importers/logMeOnceCsvImporter'; import { MeldiumCsvImporter } from '../importers/meldiumCsvImporter'; import { MSecureCsvImporter } from '../importers/msecureCsvImporter'; import { MykiCsvImporter } from '../importers/mykiCsvImporter'; @@ -119,6 +120,7 @@ export class ImportService implements ImportServiceAbstraction { { id: 'passwordwallettxt', name: 'PasswordWallet (txt)' }, { id: 'mykicsv', name: 'Myki (csv)' }, { id: 'securesafecsv', name: 'SecureSafe (csv)' }, + { id: 'logmeoncecsv', name: 'LogMeOnce (csv)' }, ]; constructor(private cipherService: CipherService, private folderService: FolderService, @@ -255,6 +257,8 @@ export class ImportService implements ImportServiceAbstraction { return new MykiCsvImporter(); case 'securesafecsv': return new SecureSafeCsvImporter(); + case 'logmeoncecsv': + return new LogMeOnceCsvImporter(); default: return null; } From b74ee7b3ee823a820854623cc32c8522a09e9f4d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 29 Aug 2019 09:40:50 -0400 Subject: [PATCH 0881/1626] memory stored pinProtectedKey --- src/abstractions/crypto.service.ts | 5 +++-- src/abstractions/lock.service.ts | 4 +++- src/angular/components/lock.component.ts | 18 +++++++++++++----- src/services/crypto.service.ts | 19 ++++++++++++------- src/services/lock.service.ts | 20 ++++---------------- src/services/system.service.ts | 2 +- 6 files changed, 36 insertions(+), 32 deletions(-) diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 3ee45f1bc1..2805e1e983 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -13,7 +13,7 @@ export abstract class CryptoService { setOrgKeys: (orgs: ProfileOrganizationResponse[]) => Promise<{}>; getKey: () => Promise; getKeyHash: () => Promise; - getEncKey: () => Promise; + getEncKey: (key?: SymmetricCryptoKey) => Promise; getPublicKey: () => Promise; getPrivateKey: () => Promise; getFingerprint: (userId: string, publicKey?: ArrayBuffer) => Promise; @@ -30,7 +30,8 @@ export abstract class CryptoService { clearKeys: () => Promise; toggleKey: () => Promise; makeKey: (password: string, salt: string, kdf: KdfType, kdfIterations: number) => Promise; - makeKeyFromPin: (pin: string, salt: string, kdf: KdfType, kdfIterations: number) => Promise; + makeKeyFromPin: (pin: string, salt: string, kdf: KdfType, kdfIterations: number, + protectedKeyCs?: CipherString) => Promise; makeShareKey: () => Promise<[CipherString, SymmetricCryptoKey]>; makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, CipherString]>; makePinKey: (pin: string, salt: string, kdf: KdfType, kdfIterations: number) => Promise; diff --git a/src/abstractions/lock.service.ts b/src/abstractions/lock.service.ts index ede31db0b1..f43938bb46 100644 --- a/src/abstractions/lock.service.ts +++ b/src/abstractions/lock.service.ts @@ -1,5 +1,7 @@ +import { CipherString } from '../models/domain/cipherString'; + export abstract class LockService { - pinLocked: boolean; + pinProtectedKey: CipherString; isLocked: () => Promise; checkLock: () => Promise; lock: (allowSoftLock?: boolean) => Promise; diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index a5357f897f..1258b545d5 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -40,8 +40,7 @@ export class LockComponent implements OnInit { async ngOnInit() { this.pinSet = await this.lockService.isPinLockSet(); - const hasKey = await this.cryptoService.hasKey(); - this.pinLock = (this.pinSet[0] && hasKey) || this.pinSet[1]; + this.pinLock = (this.pinSet[0] && this.lockService.pinProtectedKey != null) || this.pinSet[1]; this.email = await this.userService.getEmail(); let vaultUrl = this.environmentService.getWebVaultUrl(); if (vaultUrl == null) { @@ -69,12 +68,14 @@ export class LockComponent implements OnInit { let failed = true; try { if (this.pinSet[0]) { + const key = await this.cryptoService.makeKeyFromPin(this.pin, this.email, kdf, kdfIterations, + this.lockService.pinProtectedKey); + const encKey = await this.cryptoService.getEncKey(key); const protectedPin = await this.storageService.get(ConstantsService.protectedPin); - const decPin = await this.cryptoService.decryptToUtf8(new CipherString(protectedPin)); + const decPin = await this.cryptoService.decryptToUtf8(new CipherString(protectedPin), encKey); failed = decPin !== this.pin; - this.lockService.pinLocked = failed; if (!failed) { - this.doContinue(); + await this.setKeyAndContinue(key); } } else { const key = await this.cryptoService.makeKeyFromPin(this.pin, this.email, kdf, kdfIterations); @@ -100,6 +101,13 @@ export class LockComponent implements OnInit { const storedKeyHash = await this.cryptoService.getKeyHash(); if (storedKeyHash != null && keyHash != null && storedKeyHash === keyHash) { + if (this.pinSet[0]) { + const protectedPin = await this.storageService.get(ConstantsService.protectedPin); + const encKey = await this.cryptoService.getEncKey(key); + const decPin = await this.cryptoService.decryptToUtf8(new CipherString(protectedPin), encKey); + const pinKey = await this.cryptoService.makePinKey(decPin, this.email, kdf, kdfIterations); + this.lockService.pinProtectedKey = await this.cryptoService.encrypt(key.key, pinKey); + } this.setKeyAndContinue(key); } else { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index dae82f5be9..b0a38f6d23 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -110,7 +110,7 @@ export class CryptoService implements CryptoServiceAbstraction { } @sequentialize(() => 'getEncKey') - async getEncKey(): Promise { + async getEncKey(key: SymmetricCryptoKey = null): Promise { if (this.encKey != null) { return this.encKey; } @@ -120,7 +120,9 @@ export class CryptoService implements CryptoServiceAbstraction { return null; } - const key = await this.getKey(); + if (key == null) { + key = await this.getKey(); + } if (key == null) { return null; } @@ -315,13 +317,16 @@ export class CryptoService implements CryptoServiceAbstraction { return new SymmetricCryptoKey(key); } - async makeKeyFromPin(pin: string, salt: string, kdf: KdfType, kdfIterations: number): + async makeKeyFromPin(pin: string, salt: string, kdf: KdfType, kdfIterations: number, + protectedKeyCs: CipherString = null): Promise { - const pinProtectedKey = await this.storageService.get(ConstantsService.pinProtectedKey); - if (pinProtectedKey == null) { - throw new Error('No PIN protected key found.'); + if (protectedKeyCs == null) { + const pinProtectedKey = await this.storageService.get(ConstantsService.pinProtectedKey); + if (pinProtectedKey == null) { + throw new Error('No PIN protected key found.'); + } + protectedKeyCs = new CipherString(pinProtectedKey); } - const protectedKeyCs = new CipherString(pinProtectedKey); const pinKey = await this.makePinKey(pin, salt, kdf, kdfIterations); const decKey = await this.decryptToBytes(protectedKeyCs, pinKey); return new SymmetricCryptoKey(decKey); diff --git a/src/services/lock.service.ts b/src/services/lock.service.ts index 851838b806..b82c87661e 100644 --- a/src/services/lock.service.ts +++ b/src/services/lock.service.ts @@ -11,8 +11,10 @@ import { SearchService } from '../abstractions/search.service'; import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; +import { CipherString } from '../models/domain/cipherString'; + export class LockService implements LockServiceAbstraction { - pinLocked = false; + pinProtectedKey: CipherString = null; private inited = false; @@ -37,9 +39,6 @@ export class LockService implements LockServiceAbstraction { async isLocked(): Promise { const hasKey = await this.cryptoService.hasKey(); - if (hasKey && this.pinLocked) { - return true; - } return !hasKey; } @@ -85,18 +84,6 @@ export class LockService implements LockServiceAbstraction { return; } - if (allowSoftLock) { - const pinSet = await this.isPinLockSet(); - if (pinSet[0]) { - this.pinLocked = true; - this.messagingService.send('locked'); - if (this.lockedCallback != null) { - await this.lockedCallback(); - } - return; - } - } - await Promise.all([ this.cryptoService.clearKey(), this.cryptoService.clearOrgKeys(true), @@ -126,6 +113,7 @@ export class LockService implements LockServiceAbstraction { } clear(): Promise { + this.pinProtectedKey = null; return this.storageService.remove(ConstantsService.protectedPin); } } diff --git a/src/services/system.service.ts b/src/services/system.service.ts index a53b39800e..aa77cb3d82 100644 --- a/src/services/system.service.ts +++ b/src/services/system.service.ts @@ -19,7 +19,7 @@ export class SystemService implements SystemServiceAbstraction { } startProcessReload(): void { - if (this.lockService.pinLocked || this.reloadInterval != null) { + if (this.lockService.pinProtectedKey != null || this.reloadInterval != null) { return; } this.cancelProcessReload(); From cc614e68d9f1f8110a39f2747e79748e7b40b644 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 6 Sep 2019 09:06:01 -0400 Subject: [PATCH 0882/1626] added isAppleMobileBrowser util --- src/misc/utils.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/misc/utils.ts b/src/misc/utils.ts index c7f3eeccca..585cdeff83 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -11,6 +11,7 @@ export class Utils { static isNode = false; static isBrowser = true; static isMobileBrowser = false; + static isAppleMobileBrowser = false; static global: any = null; static tldEndingRegex = /.*\.(com|net|org|edu|uk|gov|ca|de|jp|fr|au|ru|ch|io|es|us|co|xyz|info|ly|mil)$/; @@ -25,6 +26,7 @@ export class Utils { Utils.isBrowser = typeof window !== 'undefined'; Utils.isNativeScript = !Utils.isNode && !Utils.isBrowser; Utils.isMobileBrowser = Utils.isBrowser && this.isMobile(window); + Utils.isAppleMobileBrowser = Utils.isBrowser && this.isAppleMobile(window); Utils.global = Utils.isNativeScript ? global : (Utils.isNode && !Utils.isBrowser ? global : window); } @@ -257,6 +259,10 @@ export class Utils { return mobile || win.navigator.userAgent.match(/iPad/i) != null; } + private static isAppleMobile(win: Window) { + return win.navigator.userAgent.match(/iPhone/i) != null || win.navigator.userAgent.match(/iPad/i) != null; + } + private static getUrl(uriString: string): URL { if (uriString == null) { return null; From 255bd3962d268bec8f66411676049803bb29e0d3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 6 Sep 2019 09:33:09 -0400 Subject: [PATCH 0883/1626] localeNames mapped to iso codes --- src/abstractions/i18n.service.ts | 1 + src/services/i18n.service.ts | 40 ++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/abstractions/i18n.service.ts b/src/abstractions/i18n.service.ts index fa2019202c..d53494dd0e 100644 --- a/src/abstractions/i18n.service.ts +++ b/src/abstractions/i18n.service.ts @@ -3,6 +3,7 @@ export abstract class I18nService { supportedTranslationLocales: string[]; translationLocale: string; collator: Intl.Collator; + localeNames: Map; t: (id: string, p1?: string, p2?: string, p3?: string) => string; translate: (id: string, p1?: string, p2?: string, p3?: string) => string; } diff --git a/src/services/i18n.service.ts b/src/services/i18n.service.ts index 345bf4b5ac..efb845ee11 100644 --- a/src/services/i18n.service.ts +++ b/src/services/i18n.service.ts @@ -6,6 +6,46 @@ export class I18nService implements I18nServiceAbstraction { supportedTranslationLocales: string[] = ['en']; translationLocale: string; collator: Intl.Collator; + localeNames = new Map([ + ['af', 'Afrikaans'], + ['bg', 'български'], + ['ca', 'català'], + ['cs', 'čeština'], + ['da', 'dansk'], + ['de', 'Deutsch'], + ['el', 'Ελληνικά'], + ['en', 'English'], + ['en-GB', 'English (British)'], + ['eo', 'Esperanto'], + ['es', 'español'], + ['et', 'eesti'], + ['fa', 'فارسی'], + ['fi', 'suomi'], + ['fr', 'français'], + ['he', 'עברית'], + ['hi', 'हिन्दी'], + ['hr', 'hrvatski'], + ['hu', 'magyar'], + ['id', 'Bahasa Indonesia'], + ['it', 'italiano'], + ['ja', '日本語'], + ['ko', '한국어'], + ['nb', 'norsk (bokmål)'], + ['nl', 'Nederlands'], + ['pl', 'polski'], + ['pt-BR', 'português do Brasil'], + ['pt-PT', 'português'], + ['ro', 'română'], + ['ru', 'русский'], + ['sk', 'slovenčina'], + ['sv', 'svenska'], + ['th', 'ไทย'], + ['tr', 'Türkçe'], + ['uk', 'українська'], + ['vi', 'Tiếng Việt'], + ['zh-CN', '中文(中国大陆)'], + ['zh-TW', '中文(台灣)'], + ]); protected inited: boolean; protected defaultMessages: any = {}; From ec012c99341ee65c0d5a7a7fdf153e79bfb274be Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 11 Sep 2019 17:05:29 -0400 Subject: [PATCH 0884/1626] blackberry csv importer --- src/importers/blackBerryCsvImporter.ts | 36 ++++++++++++++++++++++++++ src/services/import.service.ts | 4 +++ 2 files changed, 40 insertions(+) create mode 100644 src/importers/blackBerryCsvImporter.ts diff --git a/src/importers/blackBerryCsvImporter.ts b/src/importers/blackBerryCsvImporter.ts new file mode 100644 index 0000000000..286603f04b --- /dev/null +++ b/src/importers/blackBerryCsvImporter.ts @@ -0,0 +1,36 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +export class BlackBerryCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + if (value.grouping === 'list') { + return; + } + const cipher = this.initLoginCipher(); + cipher.favorite = value.fav === '1'; + cipher.name = this.getValueOrDefault(value.name); + cipher.notes = this.getValueOrDefault(value.extra); + if (value.grouping !== 'note') { + cipher.login.uris = this.makeUriArray(value.url); + cipher.login.password = this.getValueOrDefault(value.password); + cipher.login.username = this.getValueOrDefault(value.username); + } + this.convertToNoteIfNeeded(cipher); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} diff --git a/src/services/import.service.ts b/src/services/import.service.ts index 1a1ac5a92d..8e2ce4e429 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -28,6 +28,7 @@ import { AvastCsvImporter } from '../importers/avastCsvImporter'; import { AviraCsvImporter } from '../importers/aviraCsvImporter'; import { BitwardenCsvImporter } from '../importers/bitwardenCsvImporter'; import { BitwardenJsonImporter } from '../importers/bitwardenJsonImporter'; +import { BlackBerryCsvImporter } from '../importers/blackBerryCsvImporter'; import { BlurCsvImporter } from '../importers/blurCsvImporter'; import { ChromeCsvImporter } from '../importers/chromeCsvImporter'; import { ClipperzHtmlImporter } from '../importers/clipperzHtmlImporter'; @@ -121,6 +122,7 @@ export class ImportService implements ImportServiceAbstraction { { id: 'mykicsv', name: 'Myki (csv)' }, { id: 'securesafecsv', name: 'SecureSafe (csv)' }, { id: 'logmeoncecsv', name: 'LogMeOnce (csv)' }, + { id: 'blackberrycsv', name: 'BlackBerry Password Keeper (csv)' }, ]; constructor(private cipherService: CipherService, private folderService: FolderService, @@ -259,6 +261,8 @@ export class ImportService implements ImportServiceAbstraction { return new SecureSafeCsvImporter(); case 'logmeoncecsv': return new LogMeOnceCsvImporter(); + case 'blackberrycsv': + return new BlackBerryCsvImporter(); default: return null; } From 00b248155a55db9d3da6b19634bc6635e7dcc326 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 18 Sep 2019 20:23:06 -0400 Subject: [PATCH 0885/1626] added new payment method enums --- src/enums/paymentMethodType.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/enums/paymentMethodType.ts b/src/enums/paymentMethodType.ts index cd8dedf661..428eb74f0d 100644 --- a/src/enums/paymentMethodType.ts +++ b/src/enums/paymentMethodType.ts @@ -5,4 +5,6 @@ export enum PaymentMethodType { BitPay = 3, Credit = 4, WireTransfer = 5, + AppleInApp = 6, + GoogleInApp = 7, } From 575a28e25fd27969d0335f06b1778027f1214c3e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 19 Sep 2019 08:52:57 -0400 Subject: [PATCH 0886/1626] in-app purchase pre-check api --- src/abstractions/api.service.ts | 2 ++ src/models/request/iapCheckRequest.ts | 5 +++++ src/services/api.service.ts | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 src/models/request/iapCheckRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index f627deb074..977a893862 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -15,6 +15,7 @@ import { EmailTokenRequest } from '../models/request/emailTokenRequest'; import { EventRequest } from '../models/request/eventRequest'; import { FolderRequest } from '../models/request/folderRequest'; import { GroupRequest } from '../models/request/groupRequest'; +import { IapCheckRequest } from '../models/request/iapCheckRequest'; import { ImportCiphersRequest } from '../models/request/importCiphersRequest'; import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest'; @@ -120,6 +121,7 @@ export abstract class ApiService { postPasswordHint: (request: PasswordHintRequest) => Promise; postRegister: (request: RegisterRequest) => Promise; postPremium: (data: FormData) => Promise; + postIapCheck: (request: IapCheckRequest) => Promise; postReinstatePremium: () => Promise; postCancelPremium: () => Promise; postAccountStorage: (request: StorageRequest) => Promise; diff --git a/src/models/request/iapCheckRequest.ts b/src/models/request/iapCheckRequest.ts new file mode 100644 index 0000000000..75ad723c61 --- /dev/null +++ b/src/models/request/iapCheckRequest.ts @@ -0,0 +1,5 @@ +import { PaymentMethodType } from '../../enums/paymentMethodType'; + +export class IapCheckRequest { + paymentMethodType: PaymentMethodType; +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 7d9d77eeb3..eb431926ea 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -21,6 +21,7 @@ import { EmailTokenRequest } from '../models/request/emailTokenRequest'; import { EventRequest } from '../models/request/eventRequest'; import { FolderRequest } from '../models/request/folderRequest'; import { GroupRequest } from '../models/request/groupRequest'; +import { IapCheckRequest } from '../models/request/iapCheckRequest'; import { ImportCiphersRequest } from '../models/request/importCiphersRequest'; import { ImportDirectoryRequest } from '../models/request/importDirectoryRequest'; import { ImportOrganizationCiphersRequest } from '../models/request/importOrganizationCiphersRequest'; @@ -260,6 +261,10 @@ export class ApiService implements ApiServiceAbstraction { return new PaymentResponse(r); } + async postIapCheck(request: IapCheckRequest): Promise { + return this.send('POST', '/accounts/iap-check', request, true, false); + } + postReinstatePremium(): Promise { return this.send('POST', '/accounts/reinstate-premium', null, true, false); } From 929dd8415dcd69032e545b20f75c1c8abaad0e3a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 19 Sep 2019 16:30:29 -0400 Subject: [PATCH 0887/1626] usingInAppPurchase added to model --- src/models/response/subscriptionResponse.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/models/response/subscriptionResponse.ts b/src/models/response/subscriptionResponse.ts index 13f1b30aaa..d7915932a1 100644 --- a/src/models/response/subscriptionResponse.ts +++ b/src/models/response/subscriptionResponse.ts @@ -8,6 +8,7 @@ export class SubscriptionResponse extends BaseResponse { upcomingInvoice: BillingSubscriptionUpcomingInvoiceResponse; license: any; expiration: string; + usingInAppPurchase: boolean; constructor(response: any) { super(response); @@ -16,6 +17,7 @@ export class SubscriptionResponse extends BaseResponse { this.maxStorageGb = this.getResponseProperty('MaxStorageGb'); this.license = this.getResponseProperty('License'); this.expiration = this.getResponseProperty('Expiration'); + this.usingInAppPurchase = this.getResponseProperty('UpcomingInvoice'); const subscription = this.getResponseProperty('Subscription'); const upcomingInvoice = this.getResponseProperty('UpcomingInvoice'); this.subscription = subscription == null ? null : new BillingSubscriptionResponse(subscription); From 6b82cd0380d33c83e8b242713eb173d94e16e9f4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 20 Sep 2019 23:58:24 -0400 Subject: [PATCH 0888/1626] workaround for process.windowsStore bug --- src/electron/utils.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/electron/utils.ts b/src/electron/utils.ts index cb19144bc4..0bb34ca49e 100644 --- a/src/electron/utils.ts +++ b/src/electron/utils.ts @@ -15,7 +15,12 @@ export function isMacAppStore() { } export function isWindowsStore() { - return process.platform === 'win32' && process.windowsStore && process.windowsStore === true; + const isWindows = process.platform === 'win32'; + if (isWindows && !process.windowsStore && + process.resourcesPath.indexOf('8bitSolutionsLLC.bitwardendesktop_') > -1) { + process.windowsStore = true; + } + return isWindows && process.windowsStore && process.windowsStore === true; } export function isSnapStore() { From 971e19335f66ac64845b65a7bcf688811f60eaff Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 24 Sep 2019 16:03:25 -0400 Subject: [PATCH 0889/1626] fix UsingInAppPurchase mapping --- src/models/response/subscriptionResponse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/response/subscriptionResponse.ts b/src/models/response/subscriptionResponse.ts index d7915932a1..df2581ceab 100644 --- a/src/models/response/subscriptionResponse.ts +++ b/src/models/response/subscriptionResponse.ts @@ -17,7 +17,7 @@ export class SubscriptionResponse extends BaseResponse { this.maxStorageGb = this.getResponseProperty('MaxStorageGb'); this.license = this.getResponseProperty('License'); this.expiration = this.getResponseProperty('Expiration'); - this.usingInAppPurchase = this.getResponseProperty('UpcomingInvoice'); + this.usingInAppPurchase = this.getResponseProperty('UsingInAppPurchase'); const subscription = this.getResponseProperty('Subscription'); const upcomingInvoice = this.getResponseProperty('UpcomingInvoice'); this.subscription = subscription == null ? null : new BillingSubscriptionResponse(subscription); From 53d08067df953e7a66ebf0b972e6443e1f275d86 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 25 Sep 2019 17:12:13 -0400 Subject: [PATCH 0890/1626] extend ctor for collection view --- src/models/view/collectionView.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/models/view/collectionView.ts b/src/models/view/collectionView.ts index 760992ef89..622a9e4cf0 100644 --- a/src/models/view/collectionView.ts +++ b/src/models/view/collectionView.ts @@ -3,6 +3,8 @@ import { View } from './view'; import { Collection } from '../domain/collection'; import { ITreeNodeObject } from '../domain/treeNode'; +import { CollectionGroupDetailsResponse } from '../response/collectionResponse'; + export class CollectionView implements View, ITreeNodeObject { id: string = null; organizationId: string = null; @@ -10,14 +12,16 @@ export class CollectionView implements View, ITreeNodeObject { externalId: string = null; readOnly: boolean = null; - constructor(c?: Collection) { + constructor(c?: Collection | CollectionGroupDetailsResponse) { if (!c) { return; } this.id = c.id; this.organizationId = c.organizationId; - this.readOnly = c.readOnly; this.externalId = c.externalId; + if (c instanceof Collection) { + this.readOnly = c.readOnly; + } } } From 034aefa652459c9ed5a660fe13593e314ee368dc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 1 Oct 2019 11:03:51 -0400 Subject: [PATCH 0891/1626] isGuid util --- src/misc/utils.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 585cdeff83..6c1764c205 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -150,6 +150,10 @@ export class Utils { }); } + static isGuid(id: string) { + return RegExp(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, 'i').test(id); + } + static getHostname(uriString: string): string { const url = Utils.getUrl(uriString); try { From b2a804117358f121fdfe15a8f7e912dedb06b295 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 4 Oct 2019 09:45:37 -0400 Subject: [PATCH 0892/1626] fix twoFactorEnabled type --- src/models/response/organizationUserResponse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/response/organizationUserResponse.ts b/src/models/response/organizationUserResponse.ts index 333baa838a..444e27906b 100644 --- a/src/models/response/organizationUserResponse.ts +++ b/src/models/response/organizationUserResponse.ts @@ -24,7 +24,7 @@ export class OrganizationUserResponse extends BaseResponse { export class OrganizationUserUserDetailsResponse extends OrganizationUserResponse { name: string; email: string; - twoFactorEnabled: string; + twoFactorEnabled: boolean; constructor(response: any) { super(response); From e8130e79345581a7ddd80cdba31ba1f7960471eb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 4 Oct 2019 10:11:26 -0400 Subject: [PATCH 0893/1626] buttercup csv importer --- src/importers/buttercupCsvImporter.ts | 51 +++++++++++++++++++++++++++ src/services/import.service.ts | 4 +++ 2 files changed, 55 insertions(+) create mode 100644 src/importers/buttercupCsvImporter.ts diff --git a/src/importers/buttercupCsvImporter.ts b/src/importers/buttercupCsvImporter.ts new file mode 100644 index 0000000000..9173dc6569 --- /dev/null +++ b/src/importers/buttercupCsvImporter.ts @@ -0,0 +1,51 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +const OfficialProps = [ + '!group_id', '!group_name', 'title', 'username', 'password', 'URL', 'id', +]; + +export class ButtercupCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + this.processFolder(result, this.getValueOrDefault(value['!group_name'])); + + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(value.title, '--'); + cipher.login.username = this.getValueOrDefault(value.username); + cipher.login.password = this.getValueOrDefault(value.password); + cipher.login.uris = this.makeUriArray(value.URL); + + let processingCustomFields = false; + for (const prop in value) { + if (value.hasOwnProperty(prop)) { + if (!processingCustomFields && OfficialProps.indexOf(prop) === -1) { + processingCustomFields = true; + } + if (processingCustomFields) { + this.processKvp(cipher, prop, value[prop]); + } + } + } + + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + if (this.organization) { + this.moveFoldersToCollections(result); + } + + result.success = true; + return result; + } +} diff --git a/src/services/import.service.ts b/src/services/import.service.ts index 8e2ce4e429..2526485bc9 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -30,6 +30,7 @@ import { BitwardenCsvImporter } from '../importers/bitwardenCsvImporter'; import { BitwardenJsonImporter } from '../importers/bitwardenJsonImporter'; import { BlackBerryCsvImporter } from '../importers/blackBerryCsvImporter'; import { BlurCsvImporter } from '../importers/blurCsvImporter'; +import { ButtercupCsvImporter } from '../importers/buttercupCsvImporter'; import { ChromeCsvImporter } from '../importers/chromeCsvImporter'; import { ClipperzHtmlImporter } from '../importers/clipperzHtmlImporter'; import { DashlaneJsonImporter } from '../importers/dashlaneJsonImporter'; @@ -123,6 +124,7 @@ export class ImportService implements ImportServiceAbstraction { { id: 'securesafecsv', name: 'SecureSafe (csv)' }, { id: 'logmeoncecsv', name: 'LogMeOnce (csv)' }, { id: 'blackberrycsv', name: 'BlackBerry Password Keeper (csv)' }, + { id: 'buttercupcsv', name: 'Buttercup (csv)' }, ]; constructor(private cipherService: CipherService, private folderService: FolderService, @@ -263,6 +265,8 @@ export class ImportService implements ImportServiceAbstraction { return new LogMeOnceCsvImporter(); case 'blackberrycsv': return new BlackBerryCsvImporter(); + case 'buttercupcsv': + return new ButtercupCsvImporter(); default: return null; } From 83d6b2449cba8390912b692d882f400723c01dc1 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 5 Oct 2019 20:39:46 -0400 Subject: [PATCH 0894/1626] fix min character assignments for pw gen --- src/services/passwordGeneration.service.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 161d3249a1..7270fdaf69 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -53,15 +53,26 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr // sanitize if (o.uppercase && o.minUppercase <= 0) { o.minUppercase = 1; + } else if (!o.uppercase) { + o.minUppercase = 0; } + if (o.lowercase && o.minLowercase <= 0) { o.minLowercase = 1; + } else if (!o.lowercase) { + o.minLowercase = 0; } + if (o.number && o.minNumber <= 0) { o.minNumber = 1; + } else if (!o.number) { + o.minNumber = 0; } + if (o.special && o.minSpecial <= 0) { o.minSpecial = 1; + } else if (!o.special) { + o.minSpecial = 0; } if (!o.length || o.length < 1) { From 9f2d9c0a91d470a693502e6e74d7154fc84581f5 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 7 Oct 2019 10:02:18 -0400 Subject: [PATCH 0895/1626] allow custom user agent string --- src/services/api.service.ts | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/services/api.service.ts b/src/services/api.service.ts index eb431926ea..d62f2a2abc 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -117,7 +117,7 @@ export class ApiService implements ApiServiceAbstraction { private usingBaseUrl = false; constructor(private tokenService: TokenService, private platformUtilsService: PlatformUtilsService, - private logoutCallback: (expired: boolean) => Promise) { + private logoutCallback: (expired: boolean) => Promise, private customUserAgent: string = null) { this.device = platformUtilsService.getDevice(); this.deviceType = this.device.toString(); this.isWebClient = this.device === DeviceType.IEBrowser || this.device === DeviceType.ChromeBrowser || @@ -158,15 +158,19 @@ export class ApiService implements ApiServiceAbstraction { // Auth APIs async postIdentityToken(request: TokenRequest): Promise { + const headers = new Headers({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', + 'Accept': 'application/json', + 'Device-Type': this.deviceType, + }); + if (this.customUserAgent != null) { + headers.set('User-Agent', this.customUserAgent); + } const response = await this.fetch(new Request(this.identityBaseUrl + '/connect/token', { body: this.qsStringify(request.toIdentityToken(this.platformUtilsService.identityClientId)), credentials: this.getCredentials(), cache: 'no-cache', - headers: new Headers({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', - 'Accept': 'application/json', - 'Device-Type': this.deviceType, - }), + headers: headers, method: 'POST', })); @@ -860,6 +864,9 @@ export class ApiService implements ApiServiceAbstraction { 'Authorization': 'Bearer ' + authHeader, 'Content-Type': 'application/json; charset=utf-8', }); + if (this.customUserAgent != null) { + headers.set('User-Agent', this.customUserAgent); + } const response = await this.fetch(new Request(this.eventsBaseUrl + '/collect', { cache: 'no-cache', credentials: this.getCredentials(), @@ -926,6 +933,9 @@ export class ApiService implements ApiServiceAbstraction { const headers = new Headers({ 'Device-Type': this.deviceType, }); + if (this.customUserAgent != null) { + headers.set('User-Agent', this.customUserAgent); + } const requestInit: RequestInit = { cache: 'no-cache', @@ -985,6 +995,14 @@ export class ApiService implements ApiServiceAbstraction { if (refreshToken == null || refreshToken === '') { throw new Error(); } + const headers = new Headers({ + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', + 'Accept': 'application/json', + 'Device-Type': this.deviceType, + }); + if (this.customUserAgent != null) { + headers.set('User-Agent', this.customUserAgent); + } const decodedToken = this.tokenService.decodeToken(); const response = await this.fetch(new Request(this.identityBaseUrl + '/connect/token', { @@ -995,11 +1013,7 @@ export class ApiService implements ApiServiceAbstraction { }), cache: 'no-cache', credentials: this.getCredentials(), - headers: new Headers({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', - 'Accept': 'application/json', - 'Device-Type': this.deviceType, - }), + headers: headers, method: 'POST', })); From e16cb9b801bec1cf1744d8b48f39421ad37e1644 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 7 Oct 2019 10:11:32 -0400 Subject: [PATCH 0896/1626] allow custom user agent on node api service --- src/services/nodeApi.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/nodeApi.service.ts b/src/services/nodeApi.service.ts index 07559c89c2..b60b2128f4 100644 --- a/src/services/nodeApi.service.ts +++ b/src/services/nodeApi.service.ts @@ -15,8 +15,8 @@ import { TokenService } from '../abstractions/token.service'; export class NodeApiService extends ApiService { constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService, - logoutCallback: (expired: boolean) => Promise) { - super(tokenService, platformUtilsService, logoutCallback); + logoutCallback: (expired: boolean) => Promise, customUserAgent: string = null) { + super(tokenService, platformUtilsService, logoutCallback, customUserAgent); } nativeFetch(request: Request): Promise { From dc4d2e131dafe3a3578508fb84e3e4590b7bf187 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 7 Oct 2019 14:45:24 -0400 Subject: [PATCH 0897/1626] npm audit fix --- package-lock.json | 1585 +++++++++++++++++---------------------------- 1 file changed, 596 insertions(+), 989 deletions(-) diff --git a/package-lock.json b/package-lock.json index 03f1566ed8..a0797c728b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1555,12 +1555,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1575,17 +1577,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -1702,7 +1707,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -1714,6 +1720,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -1728,6 +1735,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -1735,12 +1743,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -1759,6 +1769,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -1839,7 +1850,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -1851,6 +1863,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -1972,6 +1985,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3556,546 +3570,6 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "^2.1.0" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "^5.1.1", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "nan": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz", - "integrity": "sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw==", - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.0", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.1.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.5.1", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.0.5" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true, - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.0.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.2.4", - "minizlib": "^1.1.0", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.1", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true, - "dev": true - } - } - }, "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", @@ -4217,9 +3691,9 @@ "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" }, "handlebars": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.1.tgz", - "integrity": "sha512-3Zhi6C0euYZL5sM0Zcy7lInLXKQ+YLcF/olbN010mzGQ4XVm50JeyBnMqofHh696GrciGruC7kCcApPDJvVgwA==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.2.tgz", + "integrity": "sha512-cIv17+GhL8pHHnRJzGu2wwcthL5sb8uDKBHvZ2Dtu5s1YNt0ljbzKbamnc+gr69y7bzwQiBdr5+hOpRd5pnOdg==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -5049,9 +4523,9 @@ "dev": true }, "js-yaml": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.0.tgz", - "integrity": "sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -5514,15 +4988,9 @@ } }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" - }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", - "dev": true + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "lodash.isequal": { "version": "4.5.0", @@ -5797,9 +5265,9 @@ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "dev": true, "requires": { "for-in": "^1.0.2", @@ -5941,9 +5409,9 @@ "dev": true }, "neo-async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz", - "integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", "dev": true }, "ngx-infinite-scroll": { @@ -5990,277 +5458,577 @@ "update-notifier": "^2.3.0" }, "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, "chokidar": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", - "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", "dev": true, "requires": { "anymatch": "^2.0.0", - "async-each": "^1.0.0", - "braces": "^2.3.0", - "fsevents": "^1.2.2", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", "glob-parent": "^3.1.0", - "inherits": "^2.0.1", + "inherits": "^2.0.3", "is-binary-path": "^1.0.0", "is-glob": "^4.0.0", - "lodash.debounce": "^4.0.8", - "normalize-path": "^2.1.1", + "normalize-path": "^3.0.0", "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0", - "upath": "^1.0.5" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "readdirp": "^2.2.1", + "upath": "^1.1.1" }, "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", "dev": true, + "optional": true, "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" }, "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, "dev": true, "requires": { - "is-buffer": "^1.1.5" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.7.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true } } }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true - } - } - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", "dev": true, "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" } }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true } } }, @@ -6270,103 +6038,6 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, "supports-color": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", @@ -7309,18 +6980,6 @@ "util-deprecate": "~1.0.1" } }, - "readdirp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", - "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "minimatch": "^3.0.2", - "readable-stream": "^2.0.2", - "set-immediate-shim": "^1.0.1" - } - }, "redent": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", @@ -7616,33 +7275,16 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", - "dev": true - }, "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", "dev": true, "requires": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", "is-plain-object": "^2.0.3", "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } } }, "setimmediate": { @@ -8627,20 +8269,20 @@ "dev": true }, "uglify-js": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.2.tgz", - "integrity": "sha512-imog1WIsi9Yb56yRt5TfYVxGmnWs3WSGU73ieSOlMVFwhJCA9W8fqFFMMj4kgDqiS/80LGdsYnWL7O9UcjEBlg==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", + "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", "dev": true, "optional": true, "requires": { - "commander": "~2.19.0", + "commander": "~2.20.0", "source-map": "~0.6.1" }, "dependencies": { "commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz", + "integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==", "dev": true, "optional": true } @@ -8673,44 +8315,15 @@ } }, "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", "dev": true, "requires": { "arr-union": "^3.1.0", "get-value": "^2.0.6", "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } + "set-value": "^2.0.1" } }, "unique-string": { @@ -8785,12 +8398,6 @@ "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", "dev": true }, - "upath": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", - "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", - "dev": true - }, "update-notifier": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", From e35431f3742bb9ab0820204b7823a5f4f6dd35b7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 11 Oct 2019 13:38:44 -0400 Subject: [PATCH 0898/1626] process enpass json folders --- src/importers/enpassJsonImporter.ts | 54 +++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/importers/enpassJsonImporter.ts b/src/importers/enpassJsonImporter.ts index 79310220b4..e0a4634a99 100644 --- a/src/importers/enpassJsonImporter.ts +++ b/src/importers/enpassJsonImporter.ts @@ -5,6 +5,7 @@ import { ImportResult } from '../models/domain/importResult'; import { CardView } from '../models/view/cardView'; import { CipherView } from '../models/view/cipherView'; +import { FolderView } from '../models/view/folderView'; import { CipherType } from '../enums/cipherType'; @@ -17,7 +18,22 @@ export class EnpassJsonImporter extends BaseImporter implements Importer { return result; } + const foldersMap = new Map(); + const foldersIndexMap = new Map(); + const folderTree = this.buildFolderTree(results.folders); + this.flattenFolderTree(null, folderTree, foldersMap); + foldersMap.forEach((val, key) => { + foldersIndexMap.set(key, result.folders.length); + const f = new FolderView(); + f.name = val; + result.folders.push(f); + }); + results.items.forEach((item: any) => { + if (item.folders != null && item.folders.length > 0 && foldersIndexMap.has(item.folders[0])) { + result.folderRelationships.push([result.ciphers.length, foldersIndexMap.get(item.folders[0])]); + } + const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(item.title); cipher.favorite = item.favorite > 0; @@ -101,4 +117,42 @@ export class EnpassJsonImporter extends BaseImporter implements Importer { this.processKvp(cipher, field.label, field.value); }); } + + private buildFolderTree(folders: any[]): any[] { + if (folders == null) { + return []; + } + const folderTree: any[] = []; + const map = new Map([]); + folders.forEach((obj: any) => { + map.set(obj.uuid, obj); + obj.children = []; + }); + folders.forEach((obj: any) => { + if (obj.parent_uuid != null && obj.parent_uuid !== '' && map.has(obj.parent_uuid)) { + map.get(obj.parent_uuid).children.push(obj); + } else { + folderTree.push(obj); + } + }); + return folderTree; + } + + private flattenFolderTree(titlePrefix: string, tree: any[], map: Map) { + if (tree == null) { + return; + } + tree.forEach((f: any) => { + if (f.title != null && f.title.trim() !== '') { + let title = f.title.trim(); + if (titlePrefix != null && titlePrefix.trim() !== '') { + title = titlePrefix + '/' + title; + } + map.set(f.uuid, title); + if (f.children != null && f.children.length !== 0) { + this.flattenFolderTree(title, f.children, map); + } + } + }); + } } From 669f6ddf93bbfe8acd18a4834fff5e1c7f9c91ba Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 15 Oct 2019 11:06:55 -0400 Subject: [PATCH 0899/1626] option to allow sync to throw error --- src/abstractions/sync.service.ts | 2 +- src/services/sync.service.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/abstractions/sync.service.ts b/src/abstractions/sync.service.ts index 264e6ecbf9..ad6eb498e8 100644 --- a/src/abstractions/sync.service.ts +++ b/src/abstractions/sync.service.ts @@ -8,7 +8,7 @@ export abstract class SyncService { getLastSync: () => Promise; setLastSync: (date: Date) => Promise; - fullSync: (forceSync: boolean) => Promise; + fullSync: (forceSync: boolean, allowThrowOnError?: boolean) => Promise; syncUpsertFolder: (notification: SyncFolderNotification, isEdit: boolean) => Promise; syncDeleteFolder: (notification: SyncFolderNotification) => Promise; syncUpsertCipher: (notification: SyncCipherNotification, isEdit: boolean) => Promise; diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index 6db32a6e6f..4e98d785ee 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -61,7 +61,7 @@ export class SyncService implements SyncServiceAbstraction { await this.storageService.save(Keys.lastSyncPrefix + userId, date.toJSON()); } - async fullSync(forceSync: boolean): Promise { + async fullSync(forceSync: boolean, allowThrowOnError = false): Promise { this.syncStarted(); const isAuthenticated = await this.userService.isAuthenticated(); if (!isAuthenticated) { @@ -95,7 +95,11 @@ export class SyncService implements SyncServiceAbstraction { await this.setLastSync(now); return this.syncCompleted(true); } catch (e) { - return this.syncCompleted(false); + if (allowThrowOnError) { + throw e; + } else { + return this.syncCompleted(false); + } } } From 2c4597828630b080469f5eb2bcdc215c44046309 Mon Sep 17 00:00:00 2001 From: Veit-Hendrik Schlenker Date: Sun, 20 Oct 2019 03:14:53 +0200 Subject: [PATCH 0900/1626] add function to copy data to drag transfer event (#51) --- src/angular/components/view.component.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index 79591889b8..365055f87f 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -175,6 +175,10 @@ export class ViewComponent implements OnDestroy, OnInit { } } + setTextDataOnDrag(event: DragEvent, data: string) { + event.dataTransfer.setData('text', data); + } + async downloadAttachment(attachment: AttachmentView) { const a = (attachment as any); if (a.downloading) { From b3d1e9d233aa7ed235d6ec0b26cdba41afa065ae Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 21 Oct 2019 08:41:16 -0400 Subject: [PATCH 0901/1626] npm i --- package-lock.json | 54 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index a0797c728b..4e63ecbe2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1534,7 +1534,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -1949,7 +1950,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -2005,6 +2007,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2048,12 +2051,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -5498,7 +5503,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -5519,12 +5525,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5539,17 +5547,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -5666,7 +5677,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -5678,6 +5690,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5692,6 +5705,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5699,12 +5713,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5723,6 +5739,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -5803,7 +5820,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -5815,6 +5833,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -5900,7 +5919,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -5936,6 +5956,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5955,6 +5976,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5998,12 +6020,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, From 8ab36db5c6d07724326853207690338a27589f43 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 21 Oct 2019 13:51:48 -0400 Subject: [PATCH 0902/1626] add ids to data when erroring with multipleResults --- src/cli/baseProgram.ts | 2 +- src/cli/models/response.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/cli/baseProgram.ts b/src/cli/baseProgram.ts index 546eb70147..cc58ac1844 100644 --- a/src/cli/baseProgram.ts +++ b/src/cli/baseProgram.ts @@ -69,7 +69,7 @@ export abstract class BaseProgram { } } - protected getMessage(response: Response) { + protected getMessage(response: Response): string { const message = (response.data as MessageResponse); if (process.env.BW_RAW === 'true') { return message.raw; diff --git a/src/cli/models/response.ts b/src/cli/models/response.ts index d361c3a2b6..24ca7d3c36 100644 --- a/src/cli/models/response.ts +++ b/src/cli/models/response.ts @@ -1,7 +1,7 @@ import { BaseResponse } from './response/baseResponse'; export class Response { - static error(error: any): Response { + static error(error: any, data?: any): Response { const res = new Response(); res.success = false; if (typeof (error) === 'string') { @@ -9,6 +9,7 @@ export class Response { } else { res.message = error.message != null ? error.message : error.toString(); } + res.data = data; return res; } @@ -26,7 +27,7 @@ export class Response { ids.forEach((id) => { msg += '\n' + id; }); - return Response.error(msg); + return Response.error(msg, ids); } static success(data?: BaseResponse): Response { From 57e49207e9ad57c71576fc487a38513a4d0fe120 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 21 Oct 2019 16:04:33 -0400 Subject: [PATCH 0903/1626] NOINTERACTION option checks --- src/cli/commands/login.command.ts | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/cli/commands/login.command.ts b/src/cli/commands/login.command.ts index 91d2e294fd..6801cf2413 100644 --- a/src/cli/commands/login.command.ts +++ b/src/cli/commands/login.command.ts @@ -22,7 +22,8 @@ export class LoginCommand { protected i18nService: I18nService) { } async run(email: string, password: string, cmd: program.Command) { - if (email == null || email === '') { + const canInteract = process.stdout.isTTY && process.env.BW_NOINTERACTION !== 'true'; + if ((email == null || email === '') && canInteract) { const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ type: 'input', name: 'email', @@ -37,7 +38,7 @@ export class LoginCommand { return Response.badRequest('Email address is invalid.'); } - if (password == null || password === '') { + if ((password == null || password === '') && canInteract) { const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ type: 'password', name: 'password', @@ -88,7 +89,7 @@ export class LoginCommand { if (selectedProvider == null) { if (twoFactorProviders.length === 1) { selectedProvider = twoFactorProviders[0]; - } else { + } else if (canInteract) { const options = twoFactorProviders.map((p) => p.name); options.push(new inquirer.Separator()); options.push('Cancel'); @@ -105,6 +106,9 @@ export class LoginCommand { } selectedProvider = twoFactorProviders[i]; } + if (selectedProvider == null) { + return Response.error('Login failed. No provider selected.'); + } } if (twoFactorToken == null && response.twoFactorProviders.size > 1 && @@ -115,13 +119,15 @@ export class LoginCommand { } if (twoFactorToken == null) { - const answer: inquirer.Answers = - await inquirer.createPromptModule({ output: process.stderr })({ - type: 'input', - name: 'token', - message: 'Two-step login code:', - }); - twoFactorToken = answer.token; + if (canInteract) { + const answer: inquirer.Answers = + await inquirer.createPromptModule({ output: process.stderr })({ + type: 'input', + name: 'token', + message: 'Two-step login code:', + }); + twoFactorToken = answer.token; + } if (twoFactorToken == null || twoFactorToken === '') { return Response.badRequest('Code is required.'); } From 05c36b300db460925e834204b8e7c668b57e3948 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 25 Nov 2019 16:10:51 -0500 Subject: [PATCH 0904/1626] codebook csv importer --- src/importers/codebookCsvImporter.ts | 46 ++++++++++++++++++++++++++++ src/services/import.service.ts | 4 +++ 2 files changed, 50 insertions(+) create mode 100644 src/importers/codebookCsvImporter.ts diff --git a/src/importers/codebookCsvImporter.ts b/src/importers/codebookCsvImporter.ts new file mode 100644 index 0000000000..59ebdd4a8f --- /dev/null +++ b/src/importers/codebookCsvImporter.ts @@ -0,0 +1,46 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +export class CodebookCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + this.processFolder(result, this.getValueOrDefault(value.Category)); + + const cipher = this.initLoginCipher(); + cipher.favorite = this.getValueOrDefault(value.Favorite) === 'True'; + cipher.name = this.getValueOrDefault(value.Entry, '--'); + cipher.notes = this.getValueOrDefault(value.Note); + cipher.login.username = this.getValueOrDefault(value.Username, value.Email); + cipher.login.password = this.getValueOrDefault(value.Password); + cipher.login.totp = this.getValueOrDefault(value.TOTP); + cipher.login.uris = this.makeUriArray(value.Website); + + if (!this.isNullOrWhitespace(value.Username)) { + this.processKvp(cipher, 'Email', value.Email); + } + this.processKvp(cipher, 'Phone', value.Phone); + this.processKvp(cipher, 'PIN', value.PIN); + this.processKvp(cipher, 'Account', value.Account); + this.processKvp(cipher, 'Date', value.Date); + + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + if (this.organization) { + this.moveFoldersToCollections(result); + } + + result.success = true; + return result; + } +} diff --git a/src/services/import.service.ts b/src/services/import.service.ts index 2526485bc9..df6735bd47 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -33,6 +33,7 @@ import { BlurCsvImporter } from '../importers/blurCsvImporter'; import { ButtercupCsvImporter } from '../importers/buttercupCsvImporter'; import { ChromeCsvImporter } from '../importers/chromeCsvImporter'; import { ClipperzHtmlImporter } from '../importers/clipperzHtmlImporter'; +import { CodebookCsvImporter } from '../importers/codebookCsvImporter'; import { DashlaneJsonImporter } from '../importers/dashlaneJsonImporter'; import { EnpassCsvImporter } from '../importers/enpassCsvImporter'; import { EnpassJsonImporter } from '../importers/enpassJsonImporter'; @@ -125,6 +126,7 @@ export class ImportService implements ImportServiceAbstraction { { id: 'logmeoncecsv', name: 'LogMeOnce (csv)' }, { id: 'blackberrycsv', name: 'BlackBerry Password Keeper (csv)' }, { id: 'buttercupcsv', name: 'Buttercup (csv)' }, + { id: 'codebookcsv', name: 'Codebook (csv)' }, ]; constructor(private cipherService: CipherService, private folderService: FolderService, @@ -267,6 +269,8 @@ export class ImportService implements ImportServiceAbstraction { return new BlackBerryCsvImporter(); case 'buttercupcsv': return new ButtercupCsvImporter(); + case 'codebookcsv': + return new CodebookCsvImporter(); default: return null; } From d1c0776330145ce173eae23d6dd5f0cbc825af8a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 25 Nov 2019 16:29:33 -0500 Subject: [PATCH 0905/1626] ProtectInMemory support on keepass import --- src/importers/keepass2XmlImporter.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/importers/keepass2XmlImporter.ts b/src/importers/keepass2XmlImporter.ts index 0bced5a80d..a959baeadd 100644 --- a/src/importers/keepass2XmlImporter.ts +++ b/src/importers/keepass2XmlImporter.ts @@ -1,6 +1,8 @@ import { BaseImporter } from './baseImporter'; import { Importer } from './importer'; +import { FieldType } from '../enums/fieldType'; + import { ImportResult } from '../models/domain/importResult'; import { FolderView } from '../models/view/folderView'; @@ -71,7 +73,12 @@ export class KeePass2XmlImporter extends BaseImporter implements Importer { } else if (key === 'Notes') { cipher.notes += (value + '\n'); } else { - this.processKvp(cipher, key, value); + let type = FieldType.Text; + if (valueEl.attributes.length > 0 && valueEl.attributes['ProtectInMemory'] != null && + valueEl.attributes['ProtectInMemory'].value === 'True') { + type = FieldType.Hidden; + } + this.processKvp(cipher, key, value, type); } }); From 68bbc8434f66f744512f756a618f2da66e70d3a4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 26 Nov 2019 08:34:45 -0500 Subject: [PATCH 0906/1626] importer fixes --- src/importers/codebookCsvImporter.ts | 1 + src/importers/keepass2XmlImporter.ts | 5 +++-- src/importers/onepassword1PifImporter.ts | 19 ++++++++++++------- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/importers/codebookCsvImporter.ts b/src/importers/codebookCsvImporter.ts index 59ebdd4a8f..7ee56c205b 100644 --- a/src/importers/codebookCsvImporter.ts +++ b/src/importers/codebookCsvImporter.ts @@ -32,6 +32,7 @@ export class CodebookCsvImporter extends BaseImporter implements Importer { this.processKvp(cipher, 'Account', value.Account); this.processKvp(cipher, 'Date', value.Date); + this.convertToNoteIfNeeded(cipher); this.cleanupCipher(cipher); result.ciphers.push(cipher); }); diff --git a/src/importers/keepass2XmlImporter.ts b/src/importers/keepass2XmlImporter.ts index a959baeadd..1ad213e238 100644 --- a/src/importers/keepass2XmlImporter.ts +++ b/src/importers/keepass2XmlImporter.ts @@ -74,8 +74,9 @@ export class KeePass2XmlImporter extends BaseImporter implements Importer { cipher.notes += (value + '\n'); } else { let type = FieldType.Text; - if (valueEl.attributes.length > 0 && valueEl.attributes['ProtectInMemory'] != null && - valueEl.attributes['ProtectInMemory'].value === 'True') { + const attrs = (valueEl.attributes as any); + if (attrs.length > 0 && attrs.ProtectInMemory != null && + attrs.ProtectInMemory.value === 'True') { type = FieldType.Hidden; } this.processKvp(cipher, key, value, type); diff --git a/src/importers/onepassword1PifImporter.ts b/src/importers/onepassword1PifImporter.ts index 56a7a76652..b3df8f9d54 100644 --- a/src/importers/onepassword1PifImporter.ts +++ b/src/importers/onepassword1PifImporter.ts @@ -54,6 +54,9 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { } if (item.details != null) { + if (item.details.passwordHistory != null) { + this.parsePasswordHistory(item.details.passwordHistory, cipher); + } if (!this.isNullOrWhitespace(item.details.ccnum) || !this.isNullOrWhitespace(item.details.cvv)) { cipher.type = CipherType.Card; cipher.card = new CardView(); @@ -78,9 +81,6 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { } }); } - if (item.details.passwordHistory != null) { - this.parsePasswordHistory(item.details.passwordHistory, cipher); - } } } @@ -103,6 +103,9 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { } if (item.secureContents != null) { + if (item.secureContents.passwordHistory != null) { + this.parsePasswordHistory(item.secureContents.passwordHistory, cipher); + } if (!this.isNullOrWhitespace(item.secureContents.notesPlain)) { cipher.notes = item.secureContents.notesPlain.split(this.newLineRegex).join('\n') + '\n'; } @@ -132,9 +135,6 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { } }); } - if (item.secureContents.passwordHistory != null) { - this.parsePasswordHistory(item.secureContents.passwordHistory, cipher); - } } } @@ -232,8 +232,13 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { } } - const fieldType = field.k === 'concealed' ? FieldType.Hidden : FieldType.Text; const fieldName = this.isNullOrWhitespace(field[nameKey]) ? 'no_name' : field[nameKey]; + if (fieldName === 'password' && cipher.passwordHistory != null && + cipher.passwordHistory.some((h) => h.password === fieldValue)) { + return; + } + + const fieldType = field.k === 'concealed' ? FieldType.Hidden : FieldType.Text; this.processKvp(cipher, fieldName, fieldValue, fieldType); }); } From 77282e7b0fe82aa4332304d9be037627be511bcb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 13 Dec 2019 22:48:18 -0500 Subject: [PATCH 0907/1626] update user agent --- src/electron/window.main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index f57dc4154b..96d59ddf7e 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -121,7 +121,7 @@ export class WindowMain { pathname: path.join(__dirname, '/index.html'), slashes: true, }), { - userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0', + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0', }); // Open the DevTools. From a7517a3621804489f2c4fbc2c3532519fdfd13bd Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 20 Dec 2019 13:05:22 -0500 Subject: [PATCH 0908/1626] update password boss importer --- src/importers/passwordBossJsonImporter.ts | 34 +++++++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/importers/passwordBossJsonImporter.ts b/src/importers/passwordBossJsonImporter.ts index 2f6cf8f416..65c0875570 100644 --- a/src/importers/passwordBossJsonImporter.ts +++ b/src/importers/passwordBossJsonImporter.ts @@ -4,6 +4,7 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; import { CardView } from '../models/view/cardView'; +import { FolderView } from '../models/view/folderView'; import { CipherType } from '../enums/cipherType'; @@ -11,16 +12,32 @@ export class PasswordBossJsonImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { const result = new ImportResult(); const results = JSON.parse(data); - if (results == null) { + if (results == null || results.items == null) { result.success = false; return result; } - results.forEach((value: any) => { + const foldersMap = new Map(); + results.folders.forEach((value: any) => { + foldersMap.set(value.id, value.name); + }); + const foldersIndexMap = new Map(); + foldersMap.forEach((val, key) => { + foldersIndexMap.set(key, result.folders.length); + const f = new FolderView(); + f.name = val; + result.folders.push(f); + }); + + results.items.forEach((value: any) => { const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(value.name, '--'); cipher.login.uris = this.makeUriArray(value.login_url); + if (value.folder != null && foldersIndexMap.has(value.folder)) { + result.folderRelationships.push([result.ciphers.length, foldersIndexMap.get(value.folder)]); + } + if (value.identifiers == null) { return; } @@ -44,6 +61,13 @@ export class PasswordBossJsonImporter extends BaseImporter implements Importer { continue; } + if (property === 'custom_fields') { + valObj.forEach((cf: any) => { + this.processKvp(cipher, cf.name, cf.value); + }); + continue; + } + if (cipher.type === CipherType.Card) { if (property === 'cardNumber') { cipher.card.number = val; @@ -66,12 +90,16 @@ export class PasswordBossJsonImporter extends BaseImporter implements Importer { continue; } } else { - if (property === 'username') { + if ((property === 'username' || property === 'email') && + this.isNullOrWhitespace(cipher.login.username)) { cipher.login.username = val; continue; } else if (property === 'password') { cipher.login.password = val; continue; + } else if (property === 'totp') { + cipher.login.totp = val; + continue; } else if ((cipher.login.uris == null || cipher.login.uris.length === 0) && this.uriFieldNames.indexOf(property) > -1) { cipher.login.uris = this.makeUriArray(val); From 98c7dc162628129d0bddbf20359d389dacb661d3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 20 Dec 2019 13:29:56 -0500 Subject: [PATCH 0909/1626] avast json importer --- src/importers/avastJsonImporter.ts | 69 ++++++++++++++++++++++++++++++ src/services/import.service.ts | 4 ++ 2 files changed, 73 insertions(+) create mode 100644 src/importers/avastJsonImporter.ts diff --git a/src/importers/avastJsonImporter.ts b/src/importers/avastJsonImporter.ts new file mode 100644 index 0000000000..b4981983a6 --- /dev/null +++ b/src/importers/avastJsonImporter.ts @@ -0,0 +1,69 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CipherType } from '../enums/cipherType'; +import { SecureNoteType } from '../enums/secureNoteType'; + +export class AvastJsonImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = JSON.parse(data); + if (results == null) { + result.success = false; + return result; + } + + if (results.logins != null) { + results.logins.forEach((value: any) => { + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(value.custName); + cipher.notes = this.getValueOrDefault(value.note); + cipher.login.uris = this.makeUriArray(value.url); + cipher.login.password = this.getValueOrDefault(value.pwd); + cipher.login.username = this.getValueOrDefault(value.loginName); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + } + + if (results.notes != null) { + results.notes.forEach((value: any) => { + const cipher = this.initLoginCipher(); + cipher.type = CipherType.SecureNote; + cipher.secureNote.type = SecureNoteType.Generic; + cipher.name = this.getValueOrDefault(value.label); + cipher.notes = this.getValueOrDefault(value.text); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + } + + if (results.cards != null) { + results.cards.forEach((value: any) => { + const cipher = this.initLoginCipher(); + cipher.type = CipherType.Card; + cipher.name = this.getValueOrDefault(value.custName); + cipher.notes = this.getValueOrDefault(value.note); + cipher.card.cardholderName = this.getValueOrDefault(value.holderName); + cipher.card.number = this.getValueOrDefault(value.cardNumber); + cipher.card.code = this.getValueOrDefault(value.cvv); + cipher.card.brand = this.getCardBrand(cipher.card.number); + if (value.expirationDate != null) { + if (value.expirationDate.month != null) { + cipher.card.expMonth = value.expirationDate.month + ''; + } + if (value.expirationDate.year != null) { + cipher.card.expYear = value.expirationDate.year + ''; + } + } + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + } + + result.success = true; + return result; + } +} diff --git a/src/services/import.service.ts b/src/services/import.service.ts index df6735bd47..8bf4f0a905 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -25,6 +25,7 @@ import { CipherView } from '../models/view/cipherView'; import { AscendoCsvImporter } from '../importers/ascendoCsvImporter'; import { AvastCsvImporter } from '../importers/avastCsvImporter'; +import { AvastJsonImporter } from '../importers/avastJsonImporter'; import { AviraCsvImporter } from '../importers/aviraCsvImporter'; import { BitwardenCsvImporter } from '../importers/bitwardenCsvImporter'; import { BitwardenJsonImporter } from '../importers/bitwardenJsonImporter'; @@ -117,6 +118,7 @@ export class ImportService implements ImportServiceAbstraction { { id: 'passpackcsv', name: 'Passpack (csv)' }, { id: 'passmanjson', name: 'Passman (json)' }, { id: 'avastcsv', name: 'Avast Passwords (csv)' }, + { id: 'avastjson', name: 'Avast Passwords (json)' }, { id: 'fsecurefsk', name: 'F-Secure KEY (fsk)' }, { id: 'kasperskytxt', name: 'Kaspersky Password Manager (txt)' }, { id: 'remembearcsv', name: 'RememBear (csv)' }, @@ -251,6 +253,8 @@ export class ImportService implements ImportServiceAbstraction { return new PassmanJsonImporter(); case 'avastcsv': return new AvastCsvImporter(); + case 'avastjson': + return new AvastJsonImporter(); case 'fsecurefsk': return new FSecureFskImporter(); case 'kasperskytxt': From 3f17b642b49f838513fcb4d62b5f9b3fbbadf167 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 9 Jan 2020 17:27:07 -0500 Subject: [PATCH 0910/1626] upgrade signalr client --- package-lock.json | 57 ++++++++++++++------------- package.json | 4 +- src/services/notifications.service.ts | 4 +- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4e63ecbe2f..33f064567e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -93,34 +93,6 @@ "tslib": "^1.9.0" } }, - "@aspnet/signalr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@aspnet/signalr/-/signalr-1.1.4.tgz", - "integrity": "sha512-Jp9nPc8hmmhbG9OKiHe2fOKskBHfg+3Y9foSKHxjgGtyI743hXjGFv3uFlUg503K9f8Ilu63gQt3fDkLICBRyg==", - "requires": { - "eventsource": "^1.0.7", - "request": "^2.88.0", - "ws": "^6.0.0" - }, - "dependencies": { - "ws": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", - "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", - "requires": { - "async-limiter": "~1.0.0" - } - } - } - }, - "@aspnet/signalr-protocol-msgpack": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@aspnet/signalr-protocol-msgpack/-/signalr-protocol-msgpack-1.1.0.tgz", - "integrity": "sha512-AQv5AavWvoFz2iLSIDK1DAIXMNhQ1Jt1qRDouXxLKAKP13u8iFq7i3/MwJ30ShOBGBoL5/zn6pBlNjAzTmAsMA==", - "requires": { - "msgpack5": "^4.0.2" - } - }, "@babel/code-frame": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", @@ -261,6 +233,35 @@ "to-fast-properties": "^2.0.0" } }, + "@microsoft/signalr": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-3.1.0.tgz", + "integrity": "sha512-ZAScxodI15jr1MF359jwelhrr/fkvCCTWUB5awRO+ibPyBvMS6KsSzWfN/AObc0EqvJPmYtjJgCBWR62nInlCg==", + "requires": { + "eventsource": "^1.0.7", + "request": "^2.88.0", + "ws": "^6.0.0" + }, + "dependencies": { + "ws": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, + "@microsoft/signalr-protocol-msgpack": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@microsoft/signalr-protocol-msgpack/-/signalr-protocol-msgpack-3.1.0.tgz", + "integrity": "sha512-rpxCeoC7m46Z95RfotcB2Ii1uJ3E+C2yGbzviuKiq2S11owewwWHe/mkA+KpyFnaxhGZi9e2dys6PlFwmoOxmQ==", + "requires": { + "@microsoft/signalr": ">=3.1.0", + "msgpack5": "^4.0.2" + } + }, "@types/commander": { "version": "2.12.2", "resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.2.tgz", diff --git a/package.json b/package.json index 4dd8100042..335db0f0cc 100644 --- a/package.json +++ b/package.json @@ -73,8 +73,8 @@ "@angular/platform-browser-dynamic": "7.2.1", "@angular/router": "7.2.1", "@angular/upgrade": "7.2.1", - "@aspnet/signalr": "1.1.4", - "@aspnet/signalr-protocol-msgpack": "1.1.0", + "@microsoft/signalr": "3.1.0", + "@microsoft/signalr-protocol-msgpack": "3.1.0", "big-integer": "1.6.36", "chalk": "2.4.1", "commander": "2.18.0", diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index 29f1ed5d19..f9d327478f 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -1,5 +1,5 @@ -import * as signalR from '@aspnet/signalr'; -import * as signalRMsgPack from '@aspnet/signalr-protocol-msgpack'; +import * as signalR from '@microsoft/signalr'; +import * as signalRMsgPack from '@microsoft/signalr-protocol-msgpack'; import { NotificationType } from '../enums/notificationType'; From 5819023bc4986cdcff3004e3c75ee5ab8c9a1dfc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 13 Jan 2020 07:49:05 -0500 Subject: [PATCH 0911/1626] no-store cache --- src/angular/components/attachments.component.ts | 4 ++-- src/angular/components/view.component.ts | 2 +- src/services/api.service.ts | 10 +++++----- src/services/cipher.service.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/angular/components/attachments.component.ts b/src/angular/components/attachments.component.ts index 58704900ac..b3f8e7d0c8 100644 --- a/src/angular/components/attachments.component.ts +++ b/src/angular/components/attachments.component.ts @@ -115,7 +115,7 @@ export class AttachmentsComponent implements OnInit { } a.downloading = true; - const response = await fetch(new Request(attachment.url, { cache: 'no-cache' })); + const response = await fetch(new Request(attachment.url, { cache: 'no-store' })); if (response.status !== 200) { this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred')); a.downloading = false; @@ -170,7 +170,7 @@ export class AttachmentsComponent implements OnInit { this.reuploadPromises[attachment.id] = Promise.resolve().then(async () => { // 1. Download a.downloading = true; - const response = await fetch(new Request(attachment.url, { cache: 'no-cache' })); + const response = await fetch(new Request(attachment.url, { cache: 'no-store' })); if (response.status !== 200) { this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred')); a.downloading = false; diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index 365055f87f..431c4d3786 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -192,7 +192,7 @@ export class ViewComponent implements OnDestroy, OnInit { } a.downloading = true; - const response = await fetch(new Request(attachment.url, { cache: 'no-cache' })); + const response = await fetch(new Request(attachment.url, { cache: 'no-store' })); if (response.status !== 200) { this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred')); a.downloading = false; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index d62f2a2abc..a1a0ca3c55 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -169,7 +169,7 @@ export class ApiService implements ApiServiceAbstraction { const response = await this.fetch(new Request(this.identityBaseUrl + '/connect/token', { body: this.qsStringify(request.toIdentityToken(this.platformUtilsService.identityClientId)), credentials: this.getCredentials(), - cache: 'no-cache', + cache: 'no-store', headers: headers, method: 'POST', })); @@ -868,7 +868,7 @@ export class ApiService implements ApiServiceAbstraction { headers.set('User-Agent', this.customUserAgent); } const response = await this.fetch(new Request(this.eventsBaseUrl + '/collect', { - cache: 'no-cache', + cache: 'no-store', credentials: this.getCredentials(), method: 'POST', body: JSON.stringify(request), @@ -918,7 +918,7 @@ export class ApiService implements ApiServiceAbstraction { fetch(request: Request): Promise { if (request.method === 'GET') { - request.headers.set('Cache-Control', 'no-cache'); + request.headers.set('Cache-Control', 'no-store'); request.headers.set('Pragma', 'no-cache'); } return this.nativeFetch(request); @@ -938,7 +938,7 @@ export class ApiService implements ApiServiceAbstraction { } const requestInit: RequestInit = { - cache: 'no-cache', + cache: 'no-store', credentials: this.getCredentials(), method: method, }; @@ -1011,7 +1011,7 @@ export class ApiService implements ApiServiceAbstraction { client_id: decodedToken.client_id, refresh_token: refreshToken, }), - cache: 'no-cache', + cache: 'no-store', credentials: this.getCredentials(), headers: headers, method: 'POST', diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index c602c409ec..6e7296e09c 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -795,7 +795,7 @@ export class CipherService implements CipherServiceAbstraction { private async shareAttachmentWithServer(attachmentView: AttachmentView, cipherId: string, organizationId: string): Promise { const attachmentResponse = await this.apiService.nativeFetch( - new Request(attachmentView.url, { cache: 'no-cache' })); + new Request(attachmentView.url, { cache: 'no-store' })); if (attachmentResponse.status !== 200) { throw Error('Failed to download attachment: ' + attachmentResponse.status.toString()); } From 844699f1a4e33688ae6047c01b41bd791bc5d2c3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 15 Jan 2020 09:56:51 -0500 Subject: [PATCH 0912/1626] new enums --- src/enums/eventType.ts | 4 ++++ src/enums/policyType.ts | 5 +++++ 2 files changed, 9 insertions(+) create mode 100644 src/enums/policyType.ts diff --git a/src/enums/eventType.ts b/src/enums/eventType.ts index eb3b55e67b..eed006e915 100644 --- a/src/enums/eventType.ts +++ b/src/enums/eventType.ts @@ -41,4 +41,8 @@ export enum EventType { Organization_Updated = 1600, Organization_PurgedVault = 1601, // Organization_ClientExportedVault = 1602, + + Policy_Created = 1700, + Policy_Updated = 1701, + Policy_Deleted = 1702, } diff --git a/src/enums/policyType.ts b/src/enums/policyType.ts new file mode 100644 index 0000000000..9ecbf97f67 --- /dev/null +++ b/src/enums/policyType.ts @@ -0,0 +1,5 @@ +export enum PolicyType { + TwoFactorAuthentication = 0, + MasterPassword = 1, + PasswordGenerator = 2, +} From f66de2207cb74320ae9e67768804775e5cc1a046 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 15 Jan 2020 11:24:00 -0500 Subject: [PATCH 0913/1626] policy apis --- src/abstractions/api.service.ts | 8 ++++++++ src/models/request/policyRequest.ts | 7 +++++++ src/models/response/policyResponse.ts | 20 +++++++++++++++++++ src/services/api.service.ts | 28 +++++++++++++++++++++++++++ 4 files changed, 63 insertions(+) create mode 100644 src/models/request/policyRequest.ts create mode 100644 src/models/response/policyResponse.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 977a893862..3a8b104d82 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -33,6 +33,7 @@ import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; import { PaymentRequest } from '../models/request/paymentRequest'; +import { PolicyRequest } from '../models/request/policyRequest'; import { PreloginRequest } from '../models/request/preloginRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { SeatRequest } from '../models/request/seatRequest'; @@ -80,6 +81,7 @@ import { OrganizationUserUserDetailsResponse, } from '../models/response/organizationUserResponse'; import { PaymentResponse } from '../models/response/paymentResponse'; +import { PolicyResponse } from '../models/response/policyResponse'; import { PreloginResponse } from '../models/response/preloginResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SelectionReadOnlyResponse } from '../models/response/selectionReadOnlyResponse'; @@ -186,6 +188,12 @@ export abstract class ApiService { deleteGroup: (organizationId: string, id: string) => Promise; deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise; + getPolicy: (organizationId: string, id: string) => Promise; + getPolicies: (organizationId: string) => Promise>; + postPolicy: (organizationId: string, request: PolicyRequest) => Promise; + putPolicy: (organizationId: string, id: string, request: PolicyRequest) => Promise; + deletePolicy: (organizationId: string, id: string) => Promise; + getOrganizationUser: (organizationId: string, id: string) => Promise; getOrganizationUserGroups: (organizationId: string, id: string) => Promise; getOrganizationUsers: (organizationId: string) => Promise>; diff --git a/src/models/request/policyRequest.ts b/src/models/request/policyRequest.ts new file mode 100644 index 0000000000..a1314d08ac --- /dev/null +++ b/src/models/request/policyRequest.ts @@ -0,0 +1,7 @@ +import { PolicyType } from '../../enums/policyType'; + +export class PolicyRequest { + type: PolicyType; + enabled: boolean; + data: any; +} diff --git a/src/models/response/policyResponse.ts b/src/models/response/policyResponse.ts new file mode 100644 index 0000000000..2fa931f324 --- /dev/null +++ b/src/models/response/policyResponse.ts @@ -0,0 +1,20 @@ +import { BaseResponse } from './baseResponse'; + +import { PolicyType } from '../../enums/policyType'; + +export class PolicyResponse extends BaseResponse { + id: string; + organizationId: string; + type: PolicyType; + data: any; + enabled: boolean; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty('Id'); + this.organizationId = this.getResponseProperty('OrganizationId'); + this.type = this.getResponseProperty('Type'); + this.data = this.getResponseProperty('Data'); + this.enabled = this.getResponseProperty('Enabled'); + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index a1a0ca3c55..4ceec11601 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -39,6 +39,7 @@ import { PasswordHintRequest } from '../models/request/passwordHintRequest'; import { PasswordRequest } from '../models/request/passwordRequest'; import { PasswordVerificationRequest } from '../models/request/passwordVerificationRequest'; import { PaymentRequest } from '../models/request/paymentRequest'; +import { PolicyRequest } from '../models/request/policyRequest'; import { PreloginRequest } from '../models/request/preloginRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { SeatRequest } from '../models/request/seatRequest'; @@ -87,6 +88,7 @@ import { OrganizationUserUserDetailsResponse, } from '../models/response/organizationUserResponse'; import { PaymentResponse } from '../models/response/paymentResponse'; +import { PolicyResponse } from '../models/response/policyResponse'; import { PreloginResponse } from '../models/response/preloginResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SelectionReadOnlyResponse } from '../models/response/selectionReadOnlyResponse'; @@ -549,6 +551,32 @@ export class ApiService implements ApiServiceAbstraction { '/organizations/' + organizationId + '/groups/' + id + '/user/' + organizationUserId, null, true, false); } + // Policy APIs + + async getPolicy(organizationId: string, id: string): Promise { + const r = await this.send('GET', '/organizations/' + organizationId + '/policies/' + id, null, true, true); + return new PolicyResponse(r); + } + + async getPolicies(organizationId: string): Promise> { + const r = await this.send('GET', '/organizations/' + organizationId + '/policies', null, true, true); + return new ListResponse(r, PolicyResponse); + } + + async postPolicy(organizationId: string, request: PolicyRequest): Promise { + const r = await this.send('POST', '/organizations/' + organizationId + '/policies', request, true, true); + return new PolicyResponse(r); + } + + async putPolicy(organizationId: string, id: string, request: PolicyRequest): Promise { + const r = await this.send('PUT', '/organizations/' + organizationId + '/policies/' + id, request, true, true); + return new PolicyResponse(r); + } + + deletePolicy(organizationId: string, id: string): Promise { + return this.send('DELETE', '/organizations/' + organizationId + '/policies/' + id, null, true, false); + } + // Organization User APIs async getOrganizationUser(organizationId: string, id: string): Promise { From ca6f235a34b23ed169893f431a3f854619094de8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 15 Jan 2020 15:22:34 -0500 Subject: [PATCH 0914/1626] usePolicies on org profile response --- src/models/response/profileOrganizationResponse.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/models/response/profileOrganizationResponse.ts b/src/models/response/profileOrganizationResponse.ts index 55eb3a6890..6a76878c5a 100644 --- a/src/models/response/profileOrganizationResponse.ts +++ b/src/models/response/profileOrganizationResponse.ts @@ -6,6 +6,7 @@ import { OrganizationUserType } from '../../enums/organizationUserType'; export class ProfileOrganizationResponse extends BaseResponse { id: string; name: string; + usePolicies: boolean; useGroups: boolean; useDirectory: boolean; useEvents: boolean; @@ -26,6 +27,7 @@ export class ProfileOrganizationResponse extends BaseResponse { super(response); this.id = this.getResponseProperty('Id'); this.name = this.getResponseProperty('Name'); + this.usePolicies = this.getResponseProperty('UsePolicies'); this.useGroups = this.getResponseProperty('UseGroups'); this.useDirectory = this.getResponseProperty('UseDirectory'); this.useEvents = this.getResponseProperty('UseEvents'); From 6c8407196bf9e79468f8f6449b4377083ee6d36b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 15 Jan 2020 15:25:06 -0500 Subject: [PATCH 0915/1626] more usepolicies --- src/models/data/organizationData.ts | 2 ++ src/models/domain/organization.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/models/data/organizationData.ts b/src/models/data/organizationData.ts index d351a7024a..626691c004 100644 --- a/src/models/data/organizationData.ts +++ b/src/models/data/organizationData.ts @@ -9,6 +9,7 @@ export class OrganizationData { status: OrganizationUserStatusType; type: OrganizationUserType; enabled: boolean; + usePolicies: boolean; useGroups: boolean; useDirectory: boolean; useEvents: boolean; @@ -27,6 +28,7 @@ export class OrganizationData { this.status = response.status; this.type = response.type; this.enabled = response.enabled; + this.usePolicies = response.usePolicies; this.useGroups = response.useGroups; this.useDirectory = response.useDirectory; this.useEvents = response.useEvents; diff --git a/src/models/domain/organization.ts b/src/models/domain/organization.ts index 346a08778d..914f8308df 100644 --- a/src/models/domain/organization.ts +++ b/src/models/domain/organization.ts @@ -9,6 +9,7 @@ export class Organization { status: OrganizationUserStatusType; type: OrganizationUserType; enabled: boolean; + usePolicies: boolean; useGroups: boolean; useDirectory: boolean; useEvents: boolean; @@ -31,6 +32,7 @@ export class Organization { this.status = obj.status; this.type = obj.type; this.enabled = obj.enabled; + this.usePolicies = obj.usePolicies; this.useGroups = obj.useGroups; this.useDirectory = obj.useDirectory; this.useEvents = obj.useEvents; From 7d8143b288a5352c439c2a789f97f906a4f54e27 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 20 Jan 2020 08:54:51 -0500 Subject: [PATCH 0916/1626] react to policy api changes --- src/abstractions/api.service.ts | 8 ++++---- src/services/api.service.ts | 18 +++++------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 3a8b104d82..359304b034 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -1,3 +1,5 @@ +import { PolicyType } from '../enums/policyType'; + import { EnvironmentUrls } from '../models/domain/environmentUrls'; import { BitPayInvoiceRequest } from '../models/request/bitPayInvoiceRequest'; @@ -188,11 +190,9 @@ export abstract class ApiService { deleteGroup: (organizationId: string, id: string) => Promise; deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise; - getPolicy: (organizationId: string, id: string) => Promise; + getPolicy: (organizationId: string, type: PolicyType) => Promise; getPolicies: (organizationId: string) => Promise>; - postPolicy: (organizationId: string, request: PolicyRequest) => Promise; - putPolicy: (organizationId: string, id: string, request: PolicyRequest) => Promise; - deletePolicy: (organizationId: string, id: string) => Promise; + putPolicy: (organizationId: string, type: PolicyType, request: PolicyRequest) => Promise; getOrganizationUser: (organizationId: string, id: string) => Promise; getOrganizationUserGroups: (organizationId: string, id: string) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 4ceec11601..4e28e24a1c 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -1,4 +1,5 @@ import { DeviceType } from '../enums/deviceType'; +import { PolicyType } from '../enums/policyType'; import { ApiService as ApiServiceAbstraction } from '../abstractions/api.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; @@ -553,8 +554,8 @@ export class ApiService implements ApiServiceAbstraction { // Policy APIs - async getPolicy(organizationId: string, id: string): Promise { - const r = await this.send('GET', '/organizations/' + organizationId + '/policies/' + id, null, true, true); + async getPolicy(organizationId: string, type: PolicyType): Promise { + const r = await this.send('GET', '/organizations/' + organizationId + '/policies/' + type, null, true, true); return new PolicyResponse(r); } @@ -563,20 +564,11 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, PolicyResponse); } - async postPolicy(organizationId: string, request: PolicyRequest): Promise { - const r = await this.send('POST', '/organizations/' + organizationId + '/policies', request, true, true); + async putPolicy(organizationId: string, type: PolicyType, request: PolicyRequest): Promise { + const r = await this.send('PUT', '/organizations/' + organizationId + '/policies/' + type, request, true, true); return new PolicyResponse(r); } - async putPolicy(organizationId: string, id: string, request: PolicyRequest): Promise { - const r = await this.send('PUT', '/organizations/' + organizationId + '/policies/' + id, request, true, true); - return new PolicyResponse(r); - } - - deletePolicy(organizationId: string, id: string): Promise { - return this.send('DELETE', '/organizations/' + organizationId + '/policies/' + id, null, true, false); - } - // Organization User APIs async getOrganizationUser(organizationId: string, id: string): Promise { From c91ab626c2aacf2b187f318431867f6a86a38576 Mon Sep 17 00:00:00 2001 From: jgfaust Date: Sat, 25 Jan 2020 07:52:51 -0500 Subject: [PATCH 0917/1626] The domain of data URLs should be null. (#59) --- spec/common/misc/utils.spec.ts | 4 ++++ src/misc/utils.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/spec/common/misc/utils.spec.ts b/spec/common/misc/utils.spec.ts index a224cabfd5..5876415c0f 100644 --- a/spec/common/misc/utils.spec.ts +++ b/spec/common/misc/utils.spec.ts @@ -10,6 +10,10 @@ describe('Utils Service', () => { expect(Utils.getDomain('bitwarden')).toBeNull(); }); + it('should fail for data urls', () => { + expect(Utils.getDomain('data:image/jpeg;base64,AAA')).toBeNull(); + }); + it('should handle urls without protocol', () => { expect(Utils.getDomain('bitwarden.com')).toBe('bitwarden.com'); expect(Utils.getDomain('wrong://bitwarden.com')).toBe('bitwarden.com'); diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 6c1764c205..6131fbd836 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -182,6 +182,10 @@ export class Utils { return null; } + if (uriString.startsWith('data:')) { + return null; + } + let httpUrl = uriString.startsWith('http://') || uriString.startsWith('https://'); if (!httpUrl && uriString.indexOf('://') < 0 && Utils.tldEndingRegex.test(uriString)) { uriString = 'http://' + uriString; From f75ae0d4291a15c3f4a393eec7eddaaa810a3f66 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 27 Jan 2020 08:55:26 -0500 Subject: [PATCH 0918/1626] remove angular http module --- package-lock.json | 8 -------- package.json | 1 - 2 files changed, 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 33f064567e..e1440e96ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,14 +53,6 @@ "tslib": "^1.9.0" } }, - "@angular/http": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/@angular/http/-/http-7.2.1.tgz", - "integrity": "sha512-3xfdN2bmCbzATwRGUEZQVkGn3IN6tMX/whLWGWgcEV3CENJqTUjfjn1+nSHASQLUnmOr5T7yTiWK5P7bDrHYzw==", - "requires": { - "tslib": "^1.9.0" - } - }, "@angular/platform-browser": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-7.2.1.tgz", diff --git a/package.json b/package.json index 335db0f0cc..df07942fef 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,6 @@ "@angular/compiler": "7.2.1", "@angular/core": "7.2.1", "@angular/forms": "7.2.1", - "@angular/http": "7.2.1", "@angular/platform-browser": "7.2.1", "@angular/platform-browser-dynamic": "7.2.1", "@angular/router": "7.2.1", From 51705604c4d17fe5c867bf3d8e7fcc6d903d3ad4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 27 Jan 2020 08:57:59 -0500 Subject: [PATCH 0919/1626] npm audit --- package-lock.json | 77 ++++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 40 insertions(+), 39 deletions(-) diff --git a/package-lock.json b/package-lock.json index e1440e96ab..ca8c17df53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -555,12 +555,9 @@ "dev": true }, "agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", - "requires": { - "es6-promisify": "^5.0.0" - } + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==" }, "ajv": { "version": "6.7.0", @@ -2614,6 +2611,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, "requires": { "ms": "2.0.0" } @@ -3078,19 +3076,6 @@ "stackframe": "^1.0.4" } }, - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "requires": { - "es6-promise": "^4.0.3" - } - }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -3689,9 +3674,9 @@ "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" }, "handlebars": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.2.tgz", - "integrity": "sha512-cIv17+GhL8pHHnRJzGu2wwcthL5sb8uDKBHvZ2Dtu5s1YNt0ljbzKbamnc+gr69y7bzwQiBdr5+hOpRd5pnOdg==", + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.2.tgz", + "integrity": "sha512-4PwqDL2laXtTWZghzzCtunQUTLbo31pcCJrd/B/9JP8XbhVzpS5ZXuKqlOzsd1rtcaLo4KqAn8nl8mkknS4MHw==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -3910,12 +3895,27 @@ "dev": true }, "https-proxy-agent": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", - "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", "requires": { - "agent-base": "^4.1.0", - "debug": "^3.1.0" + "agent-base": "5", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } } }, "iconv-lite": { @@ -5301,7 +5301,8 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true }, "msgpack5": { "version": "4.2.1", @@ -8131,9 +8132,9 @@ } }, "tree-kill": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.0.tgz", - "integrity": "sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true }, "trim-newlines": { @@ -8286,20 +8287,20 @@ "dev": true }, "uglify-js": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", - "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "version": "3.7.6", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.6.tgz", + "integrity": "sha512-yYqjArOYSxvqeeiYH2VGjZOqq6SVmhxzaPjJC1W2F9e+bqvFL9QXQ2osQuKUFjM2hGjKG2YclQnRKWQSt/nOTQ==", "dev": true, "optional": true, "requires": { - "commander": "~2.20.0", + "commander": "~2.20.3", "source-map": "~0.6.1" }, "dependencies": { "commander": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz", - "integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==", + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "optional": true } diff --git a/package.json b/package.json index df07942fef..2e98791743 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "electron-store": "1.3.0", "electron-updater": "4.1.2", "form-data": "2.3.2", - "https-proxy-agent": "2.2.1", + "https-proxy-agent": "4.0.0", "inquirer": "6.2.0", "jsdom": "13.2.0", "keytar": "4.13.0", From dce1453732553d991ac910e3a93038f7331c8ab9 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 27 Jan 2020 09:02:41 -0500 Subject: [PATCH 0920/1626] fix HttpsProxyAgent typings --- src/globals.d.ts | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/globals.d.ts b/src/globals.d.ts index 1da4c78936..8116ab173f 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -1,28 +1,3 @@ declare function escape(s: string): string; declare function unescape(s: string): string; declare module 'duo_web_sdk'; - -// From: https://github.com/TooTallNate/node-https-proxy-agent/issues/27 -declare module 'https-proxy-agent' { - import * as https from 'https' - - namespace HttpsProxyAgent { - interface HttpsProxyAgentOptions { - host: string - port: number - secureProxy?: boolean - headers?: { - [key: string]: string - } - [key: string]: any - } - } - - // HttpsProxyAgent doesnt *actually* extend https.Agent, but for my purposes I want it to pretend that it does - class HttpsProxyAgent extends https.Agent { - constructor(opts: string) - constructor(opts: HttpsProxyAgent.HttpsProxyAgentOptions) - } - - export = HttpsProxyAgent -} From 47617c6160870ed7bb1aec5c5670338eeafe6975 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 27 Jan 2020 09:18:18 -0500 Subject: [PATCH 0921/1626] upgrade electron --- package-lock.json | 52 +++++++++++++++++++---------------------------- package.json | 4 ++-- 2 files changed, 23 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index ca8c17df53..8479e65465 100644 --- a/package-lock.json +++ b/package-lock.json @@ -469,9 +469,9 @@ } }, "@types/semver": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.0.1.tgz", - "integrity": "sha512-ffCdcrEE5h8DqVxinQjo+2d1q+FV5z7iNtPofw3JsrltSoSVlOGaW0rY8XxtO9XukdTn8TaCGWmk2VFGhI70mg==" + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.2.0.tgz", + "integrity": "sha512-1OzrNb4RuAzIT7wHSsgZRlMBlNsJl+do6UblR7JMW4oB7bbR+uBEYtUh7gEc/jM84GGilh68lSOokyM/zNUlBA==" }, "@types/through": { "version": "0.0.29", @@ -1349,9 +1349,9 @@ "dev": true }, "builder-util-runtime": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.3.0.tgz", - "integrity": "sha512-CSOdsYqf4RXIHh1HANPbrZHlZ9JQJXSuDDloblZPcWQVN62inyYoTQuSmY3KrgefME2Sv3Kn2MxHvbGQHRf8Iw==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.4.0.tgz", + "integrity": "sha512-CJB/eKfPf2vHrkmirF5eicVnbDCkMBbwd5tRYlTlgud16zFeqD7QmrVUAOEXdnsrcNkiLg9dbuUsQKtl/AwsYQ==", "requires": { "debug": "^4.1.1", "sax": "^1.2.4" @@ -2851,9 +2851,9 @@ "dev": true }, "electron": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/electron/-/electron-5.0.8.tgz", - "integrity": "sha512-wkUVE2GaYCsqQTsISSHWkIkcdpwLwZ1jhzAXSFFoSzsTgugmzhX60rJjIccotUmZ0iPzw+u4ahfcaJ0eslrPNQ==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/electron/-/electron-6.1.7.tgz", + "integrity": "sha512-QhBA/fcYJit2XJGkD2xEfxlWTtTaWYu7qkKVohtVWXpELFqkpel2DCDxet5BTo0qs8ukuZHxlQPnIonODnl2bw==", "dev": true, "requires": { "@types/node": "^10.12.18", @@ -2862,9 +2862,9 @@ }, "dependencies": { "@types/node": { - "version": "10.14.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.13.tgz", - "integrity": "sha512-yN/FNNW1UYsRR1wwAoyOwqvDuLDtVXnaJTZ898XIw/Q5cCaeVAlVwvsmXLX5PuiScBYwZsZU4JYSHB3TvfdwvQ==", + "version": "10.17.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.13.tgz", + "integrity": "sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg==", "dev": true } } @@ -2919,18 +2919,18 @@ } }, "electron-updater": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.1.2.tgz", - "integrity": "sha512-4Sk8IW0LfOilDz+WAB/gEDmX7+FUFRbKHGN1zGjehPilnd6H9cmjgBHK6Xzq/FLq/uOHGJ6GX/9tsF+jr7CvnA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.2.0.tgz", + "integrity": "sha512-GuS3g7HDh17x/SaFjxjswlWUaKHczksYkV2Xc5CKj/bZH0YCvTSHtOmnBAdAmCk99u/71p3zP8f0jIqDfGcjww==", "requires": { - "@types/semver": "^6.0.1", - "builder-util-runtime": "8.3.0", + "@types/semver": "^6.0.2", + "builder-util-runtime": "8.4.0", "fs-extra": "^8.1.0", "js-yaml": "^3.13.1", "lazy-val": "^1.0.4", "lodash.isequal": "^4.5.0", "pako": "^1.0.10", - "semver": "^6.2.0" + "semver": "^6.3.0" }, "dependencies": { "fs-extra": { @@ -2944,18 +2944,9 @@ } }, "graceful-fs": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz", - "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==" - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" }, "semver": { "version": "6.3.0", @@ -4524,7 +4515,6 @@ "version": "3.13.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" diff --git a/package.json b/package.json index 2e98791743..60e94cb4a4 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@types/zxcvbn": "^4.4.0", "concurrently": "3.5.1", "cssstyle": "1.2.1", - "electron": "5.0.8", + "electron": "6.1.7", "jasmine": "^3.3.1", "jasmine-core": "^3.3.0", "jasmine-spec-reporter": "^4.2.1", @@ -81,7 +81,7 @@ "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", "electron-log": "2.2.17", "electron-store": "1.3.0", - "electron-updater": "4.1.2", + "electron-updater": "4.2.0", "form-data": "2.3.2", "https-proxy-agent": "4.0.0", "inquirer": "6.2.0", From e1d42f95d9c662bd4ffc8221bc4e5b9b96a095a6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 27 Jan 2020 09:46:42 -0500 Subject: [PATCH 0922/1626] updates to support electron 6 --- src/electron/baseMenu.ts | 23 +++++++++++-------- .../services/electronPlatformUtils.service.ts | 12 +++++----- src/electron/updater.main.ts | 12 +++++----- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/electron/baseMenu.ts b/src/electron/baseMenu.ts index 3e7e3d3c59..de08e4da68 100644 --- a/src/electron/baseMenu.ts +++ b/src/electron/baseMenu.ts @@ -48,7 +48,7 @@ export class BaseMenu { { type: 'separator' }, { label: this.i18nService.t('selectAll'), - role: 'selectall', + role: 'selectAll', }, ], }; @@ -56,15 +56,18 @@ export class BaseMenu { this.viewSubMenuItemOptions = [ { label: this.i18nService.t('zoomIn'), - role: 'zoomin', accelerator: 'CmdOrCtrl+=', + role: 'zoomIn', + accelerator: 'CmdOrCtrl+=', }, { label: this.i18nService.t('zoomOut'), - role: 'zoomout', accelerator: 'CmdOrCtrl+-', + role: 'zoomOut', + accelerator: 'CmdOrCtrl+-', }, { label: this.i18nService.t('resetZoom'), - role: 'resetzoom', accelerator: 'CmdOrCtrl+0', + role: 'resetZoom', + accelerator: 'CmdOrCtrl+0', }, { type: 'separator' }, { @@ -74,11 +77,11 @@ export class BaseMenu { { type: 'separator' }, { label: this.i18nService.t('reload'), - role: 'forcereload', + role: 'forceReload', }, { label: this.i18nService.t('toggleDevTools'), - role: 'toggledevtools', + role: 'toggleDevTools', accelerator: 'F12', }, ]; @@ -111,7 +114,7 @@ export class BaseMenu { }, { label: this.i18nService.t('hideOthers'), - role: 'hideothers', + role: 'hideOthers', }, { label: this.i18nService.t('showAll'), @@ -159,7 +162,7 @@ export class BaseMenu { { type: 'separator' }, { label: this.i18nService.t('selectAll'), - role: 'selectall', + role: 'selectAll', }, ]); @@ -190,7 +193,7 @@ export class BaseMenu { { type: 'separator' }, { label: this.i18nService.t('selectAll'), - role: 'selectall', + role: 'selectAll', }, ]); @@ -210,7 +213,7 @@ export class BaseMenu { { type: 'separator' }, { label: this.i18nService.t('selectAll'), - role: 'selectall', + role: 'selectAll', }, ]); diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index 877cbfcf4f..ad08b170e5 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -114,9 +114,9 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { remote.dialog.showSaveDialog(remote.getCurrentWindow(), { defaultPath: fileName, showsTagField: false, - }, (path) => { - if (path != null) { - fs.writeFile(path, Buffer.from(blobData), (err) => { + }).then((ret) => { + if (ret.filePath != null) { + fs.writeFile(ret.filePath, Buffer.from(blobData), (err) => { // error check? }); } @@ -147,14 +147,14 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { }); } - showDialog(text: string, title?: string, confirmText?: string, cancelText?: string, type?: string): + async showDialog(text: string, title?: string, confirmText?: string, cancelText?: string, type?: string): Promise { const buttons = [confirmText == null ? this.i18nService.t('ok') : confirmText]; if (cancelText != null) { buttons.push(cancelText); } - const result = remote.dialog.showMessageBox(remote.getCurrentWindow(), { + const result = await remote.dialog.showMessageBox(remote.getCurrentWindow(), { type: type, title: title, message: title, @@ -165,7 +165,7 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { noLink: true, }); - return Promise.resolve(result === 0); + return Promise.resolve(result.response === 0); } eventTrack(action: string, label?: string, options?: any) { diff --git a/src/electron/updater.main.ts b/src/electron/updater.main.ts index 59f61aaf1d..f09ca46847 100644 --- a/src/electron/updater.main.ts +++ b/src/electron/updater.main.ts @@ -50,14 +50,14 @@ export class UpdaterMain { this.doingUpdateCheck = true; }); - autoUpdater.on('update-available', () => { + autoUpdater.on('update-available', async () => { if (this.doingUpdateCheckWithFeedback) { if (this.windowMain.win == null) { this.reset(); return; } - const result = dialog.showMessageBox(this.windowMain.win, { + const result = await dialog.showMessageBox(this.windowMain.win, { type: 'info', title: this.i18nService.t(this.projectName) + ' - ' + this.i18nService.t('updateAvailable'), message: this.i18nService.t('updateAvailable'), @@ -68,7 +68,7 @@ export class UpdaterMain { noLink: true, }); - if (result === 0) { + if (result.response === 0) { autoUpdater.downloadUpdate(); } else { this.reset(); @@ -89,7 +89,7 @@ export class UpdaterMain { this.reset(); }); - autoUpdater.on('update-downloaded', (info) => { + autoUpdater.on('update-downloaded', async (info) => { if (this.onUpdateDownloaded != null) { this.onUpdateDownloaded(); } @@ -98,7 +98,7 @@ export class UpdaterMain { return; } - const result = dialog.showMessageBox(this.windowMain.win, { + const result = await dialog.showMessageBox(this.windowMain.win, { type: 'info', title: this.i18nService.t(this.projectName) + ' - ' + this.i18nService.t('restartToUpdate'), message: this.i18nService.t('restartToUpdate'), @@ -109,7 +109,7 @@ export class UpdaterMain { noLink: true, }); - if (result === 0) { + if (result.response === 0) { autoUpdater.quitAndInstall(false, true); } }); From 337a7ba59f686a5fe8c52d34f3290bbd6ea66669 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Tue, 28 Jan 2020 16:19:50 -0600 Subject: [PATCH 0923/1626] [jslib] Updated shared components for cipher cloning (#60) --- src/angular/components/add-edit.component.ts | 24 ++++++++++++++++---- src/angular/components/view.component.ts | 5 ++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 451abce825..f25186f84f 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -42,6 +42,7 @@ import { SecureNoteView } from '../../models/view/secureNoteView'; import { Utils } from '../../misc/utils'; export class AddEditComponent implements OnInit { + @Input() cloneMode: boolean = false; @Input() folderId: string = null; @Input() cipherId: string; @Input() type: CipherType; @@ -160,7 +161,12 @@ export class AddEditComponent implements OnInit { this.editMode = this.cipherId != null; if (this.editMode) { this.editMode = true; - this.title = this.i18nService.t('editItem'); + if (this.cloneMode) { + this.cloneMode = true; + this.title = this.i18nService.t('addItem'); + } else{ + this.title = this.i18nService.t('editItem'); + } } else { this.title = this.i18nService.t('addItem'); } @@ -176,6 +182,11 @@ export class AddEditComponent implements OnInit { if (this.editMode) { const cipher = await this.loadCipher(); this.cipher = await cipher.decrypt(); + + // Adjust Cipher Name if Cloning + if (this.cloneMode) { + this.cipher.name += " - " + this.i18nService.t('clone'); + } } else { this.cipher = new CipherView(); this.cipher.organizationId = this.organizationId == null ? null : this.organizationId; @@ -227,16 +238,21 @@ export class AddEditComponent implements OnInit { this.collections.filter((c) => (c as any).checked).map((c) => c.id); } + // Clear current Cipher Id to trigger "Add" cipher flow + if (this.cloneMode) { + this.cipher.id = null; + } + const cipher = await this.encryptCipher(); try { this.formPromise = this.saveCipher(cipher); await this.formPromise; this.cipher.id = cipher.id; - this.platformUtilsService.eventTrack(this.editMode ? 'Edited Cipher' : 'Added Cipher'); + this.platformUtilsService.eventTrack(this.editMode && !this.cloneMode ? 'Edited Cipher' : 'Added Cipher'); this.platformUtilsService.showToast('success', null, - this.i18nService.t(this.editMode ? 'editedItem' : 'addedItem')); + this.i18nService.t(this.editMode && !this.cloneMode ? 'editedItem' : 'addedItem')); this.onSavedCipher.emit(this.cipher); - this.messagingService.send(this.editMode ? 'editedCipher' : 'addedCipher'); + this.messagingService.send(this.editMode && !this.cloneMode ? 'editedCipher' : 'addedCipher'); return true; } catch { } diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index 431c4d3786..a0d6076483 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -33,6 +33,7 @@ const BroadcasterSubscriptionId = 'ViewComponent'; export class ViewComponent implements OnDestroy, OnInit { @Input() cipherId: string; @Output() onEditCipher = new EventEmitter(); + @Output() onCloneCipher = new EventEmitter(); cipher: CipherView; showPassword: boolean; @@ -105,6 +106,10 @@ export class ViewComponent implements OnDestroy, OnInit { this.onEditCipher.emit(this.cipher); } + clone() { + this.onCloneCipher.emit(this.cipher); + } + togglePassword() { this.platformUtilsService.eventTrack('Toggled Password'); this.showPassword = !this.showPassword; From 3d2e2cb1741be8b7b2916574f62f7ba2193c7123 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 28 Jan 2020 22:24:02 -0500 Subject: [PATCH 0924/1626] sync policies., set up policy service --- src/abstractions/policy.service.ts | 14 ++++++++ src/models/data/policyData.ts | 19 ++++++++++ src/models/domain/policy.ts | 26 ++++++++++++++ src/models/response/syncResponse.ts | 7 ++++ src/services/policy.service.ts | 55 +++++++++++++++++++++++++++++ src/services/sync.service.ts | 15 +++++++- 6 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 src/abstractions/policy.service.ts create mode 100644 src/models/data/policyData.ts create mode 100644 src/models/domain/policy.ts create mode 100644 src/services/policy.service.ts diff --git a/src/abstractions/policy.service.ts b/src/abstractions/policy.service.ts new file mode 100644 index 0000000000..b08269644a --- /dev/null +++ b/src/abstractions/policy.service.ts @@ -0,0 +1,14 @@ +import { PolicyData } from '../models/data/policyData'; + +import { Policy } from '../models/domain/policy'; + +import { PolicyType } from '../enums/policyType'; + +export abstract class PolicyService { + policyCache: Policy[]; + + clearCache: () => void; + getAll: (type?: PolicyType) => Promise; + replace: (policies: { [id: string]: PolicyData; }) => Promise; + clear: (userId: string) => Promise; +} diff --git a/src/models/data/policyData.ts b/src/models/data/policyData.ts new file mode 100644 index 0000000000..5f012424b9 --- /dev/null +++ b/src/models/data/policyData.ts @@ -0,0 +1,19 @@ +import { PolicyResponse } from '../response/policyResponse'; + +import { PolicyType } from '../../enums/policyType'; + +export class PolicyData { + id: string; + organizationId: string; + type: PolicyType; + data: any; + enabled: boolean; + + constructor(response: PolicyResponse) { + this.id = response.id; + this.organizationId = response.organizationId; + this.type = response.type; + this.data = response.data; + this.enabled = response.enabled; + } +} diff --git a/src/models/domain/policy.ts b/src/models/domain/policy.ts new file mode 100644 index 0000000000..e8e5d00fb3 --- /dev/null +++ b/src/models/domain/policy.ts @@ -0,0 +1,26 @@ +import { PolicyData } from '../data/policyData'; + +import Domain from './domainBase'; + +import { PolicyType } from '../../enums/policyType'; + +export class Policy extends Domain { + id: string; + organizationId: string; + type: PolicyType; + data: any; + enabled: boolean; + + constructor(obj?: PolicyData) { + super(); + if (obj == null) { + return; + } + + this.id = obj.id; + this.organizationId = obj.organizationId; + this.type = obj.type; + this.data = obj.data; + this.enabled = obj.enabled; + } +} diff --git a/src/models/response/syncResponse.ts b/src/models/response/syncResponse.ts index fb84dad89a..dafa6de554 100644 --- a/src/models/response/syncResponse.ts +++ b/src/models/response/syncResponse.ts @@ -3,6 +3,7 @@ import { CipherResponse } from './cipherResponse'; import { CollectionDetailsResponse } from './collectionResponse'; import { DomainsResponse } from './domainsResponse'; import { FolderResponse } from './folderResponse'; +import { PolicyResponse } from './policyResponse'; import { ProfileResponse } from './profileResponse'; export class SyncResponse extends BaseResponse { @@ -11,6 +12,7 @@ export class SyncResponse extends BaseResponse { collections: CollectionDetailsResponse[] = []; ciphers: CipherResponse[] = []; domains?: DomainsResponse; + policies?: PolicyResponse[] = []; constructor(response: any) { super(response); @@ -39,5 +41,10 @@ export class SyncResponse extends BaseResponse { if (domains != null) { this.domains = new DomainsResponse(domains); } + + const policies = this.getResponseProperty('Policies'); + if (policies != null) { + this.policies = policies.map((p: any) => new PolicyResponse(p)); + } } } diff --git a/src/services/policy.service.ts b/src/services/policy.service.ts new file mode 100644 index 0000000000..2baac07678 --- /dev/null +++ b/src/services/policy.service.ts @@ -0,0 +1,55 @@ +import { PolicyService as PolicyServiceAbstraction } from '../abstractions/policy.service'; +import { StorageService } from '../abstractions/storage.service'; +import { UserService } from '../abstractions/user.service'; + +import { PolicyData } from '../models/data/policyData'; + +import { Policy } from '../models/domain/policy'; + +import { PolicyType } from '../enums/policyType'; + +const Keys = { + policiesPrefix: 'policies_', +}; + +export class PolicyService implements PolicyServiceAbstraction { + policyCache: Policy[]; + + constructor(private userService: UserService, private storageService: StorageService) { + } + + clearCache(): void { + this.policyCache = null; + } + + async getAll(type?: PolicyType): Promise { + if (this.policyCache == null) { + const userId = await this.userService.getUserId(); + const policies = await this.storageService.get<{ [id: string]: PolicyData; }>( + Keys.policiesPrefix + userId); + const response: Policy[] = []; + for (const id in policies) { + if (policies.hasOwnProperty(id)) { + response.push(new Policy(policies[id])); + } + } + this.policyCache = response; + } + if (type != null) { + return this.policyCache.filter((p) => p.type === type); + } else { + return this.policyCache; + } + } + + async replace(policies: { [id: string]: PolicyData; }): Promise { + const userId = await this.userService.getUserId(); + await this.storageService.save(Keys.policiesPrefix + userId, policies); + this.policyCache = null; + } + + async clear(userId: string): Promise { + await this.storageService.remove(Keys.policiesPrefix + userId); + this.policyCache = null; + } +} diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index 4e98d785ee..9d915be8b8 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -4,6 +4,7 @@ import { CollectionService } from '../abstractions/collection.service'; import { CryptoService } from '../abstractions/crypto.service'; import { FolderService } from '../abstractions/folder.service'; import { MessagingService } from '../abstractions/messaging.service'; +import { PolicyService } from '../abstractions/policy.service'; import { SettingsService } from '../abstractions/settings.service'; import { StorageService } from '../abstractions/storage.service'; import { SyncService as SyncServiceAbstraction } from '../abstractions/sync.service'; @@ -13,6 +14,7 @@ import { CipherData } from '../models/data/cipherData'; import { CollectionData } from '../models/data/collectionData'; import { FolderData } from '../models/data/folderData'; import { OrganizationData } from '../models/data/organizationData'; +import { PolicyData } from '../models/data/policyData'; import { CipherResponse } from '../models/response/cipherResponse'; import { CollectionDetailsResponse } from '../models/response/collectionResponse'; @@ -22,6 +24,7 @@ import { SyncCipherNotification, SyncFolderNotification, } from '../models/response/notificationResponse'; +import { PolicyResponse } from '../models/response/policyResponse'; import { ProfileResponse } from '../models/response/profileResponse'; const Keys = { @@ -35,7 +38,8 @@ export class SyncService implements SyncServiceAbstraction { private settingsService: SettingsService, private folderService: FolderService, private cipherService: CipherService, private cryptoService: CryptoService, private collectionService: CollectionService, private storageService: StorageService, - private messagingService: MessagingService, private logoutCallback: (expired: boolean) => Promise) { + private messagingService: MessagingService, private policyService: PolicyService, + private logoutCallback: (expired: boolean) => Promise) { } async getLastSync(): Promise { @@ -91,6 +95,7 @@ export class SyncService implements SyncServiceAbstraction { await this.syncCollections(response.collections); await this.syncCiphers(userId, response.ciphers); await this.syncSettings(userId, response.domains); + await this.syncPolicies(response.policies); await this.setLastSync(now); return this.syncCompleted(true); @@ -298,4 +303,12 @@ export class SyncService implements SyncServiceAbstraction { return this.settingsService.setEquivalentDomains(eqDomains); } + + private async syncPolicies(response: PolicyResponse[]) { + const policies: { [id: string]: PolicyData; } = {}; + response.forEach((p) => { + policies[p.id] = new PolicyData(p); + }); + return await this.policyService.replace(policies); + } } From 08b1a022f6086f4bc270c62061e1928cf50131ea Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Sat, 1 Feb 2020 14:28:45 -0600 Subject: [PATCH 0925/1626] Enabled ownership changes for cloned items (#61) --- src/angular/components/add-edit.component.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index f25186f84f..9eae5f7572 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -232,8 +232,9 @@ export class AddEditComponent implements OnInit { (this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === '')) { this.cipher.login.uris = null; } - - if (!this.editMode && this.cipher.organizationId != null) { + + // Allows saving of selected collections during "Add" and "Clone" flows + if ((!this.editMode || this.cloneMode) && this.cipher.organizationId != null) { this.cipher.collectionIds = this.collections == null ? [] : this.collections.filter((c) => (c as any).checked).map((c) => c.id); } From 3a40cb83bf32a9830402a412be5eb3972e74ac8a Mon Sep 17 00:00:00 2001 From: Joseph Petersen Date: Sat, 1 Feb 2020 22:15:27 +0100 Subject: [PATCH 0926/1626] fix `fullAddressPart2` state if empty should not show in address line (#62) * not all countries have states * semi colons --- src/models/view/identityView.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/models/view/identityView.ts b/src/models/view/identityView.ts index 65fdbcde0e..487a59bf1a 100644 --- a/src/models/view/identityView.ts +++ b/src/models/view/identityView.ts @@ -108,8 +108,13 @@ export class IdentityView implements View { return null; } const city = this.city || '-'; - const state = this.state || '-'; + const state = this.state; const postalCode = this.postalCode || '-'; - return city + ', ' + state + ', ' + postalCode; + let addressPart2 = city; + if (!Utils.isNullOrWhitespace(state)) { + addressPart2 += ', ' + state; + } + addressPart2 += ', ' + postalCode; + return addressPart2; } } From bb459ce4b45d0ddcfc2ec0cfe3e925125550d862 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Tue, 4 Feb 2020 15:07:31 -0600 Subject: [PATCH 0927/1626] Fixed lint warning (#63) --- src/angular/components/add-edit.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 9eae5f7572..eea9e3dde4 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -164,7 +164,7 @@ export class AddEditComponent implements OnInit { if (this.cloneMode) { this.cloneMode = true; this.title = this.i18nService.t('addItem'); - } else{ + } else { this.title = this.i18nService.t('editItem'); } } else { @@ -185,7 +185,7 @@ export class AddEditComponent implements OnInit { // Adjust Cipher Name if Cloning if (this.cloneMode) { - this.cipher.name += " - " + this.i18nService.t('clone'); + this.cipher.name += ' - ' + this.i18nService.t('clone'); } } else { this.cipher = new CipherView(); @@ -232,7 +232,7 @@ export class AddEditComponent implements OnInit { (this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === '')) { this.cipher.login.uris = null; } - + // Allows saving of selected collections during "Add" and "Clone" flows if ((!this.editMode || this.cloneMode) && this.cipher.organizationId != null) { this.cipher.collectionIds = this.collections == null ? [] : From 1859357ddbde2f55ae0ff033cdc9f4cf6cd0b4f6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 5 Feb 2020 00:35:30 -0500 Subject: [PATCH 0928/1626] dont import trashed items into 1password. (#64) resolves #451 --- src/importers/onepassword1PifImporter.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/importers/onepassword1PifImporter.ts b/src/importers/onepassword1PifImporter.ts index b3df8f9d54..cd5cfc1f77 100644 --- a/src/importers/onepassword1PifImporter.ts +++ b/src/importers/onepassword1PifImporter.ts @@ -22,6 +22,9 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { return; } const item = JSON.parse(line); + if(item.trashed === true) { + return; + } const cipher = this.initLoginCipher(); if (this.isNullOrWhitespace(item.hmac)) { From eecd774b13106634220c3934207021894199b4f6 Mon Sep 17 00:00:00 2001 From: Reese <3253971+figadore@users.noreply.github.com> Date: Thu, 6 Feb 2020 08:24:18 -0800 Subject: [PATCH 0929/1626] fix lastpass import credit card expiration (#65) * Fix import of expiration date from LastPass Signed-off-by: Felipe Santos * handle empty cc exp from lastpass, add test * check for month/year null/whitespace * check for empty expiration from lp import Co-authored-by: Felipe Santos --- .../importers/lastpassCsvImporter.spec.ts | 136 ++++++++++++++++++ src/importers/lastpassCsvImporter.ts | 22 +++ 2 files changed, 158 insertions(+) create mode 100644 spec/common/importers/lastpassCsvImporter.spec.ts diff --git a/spec/common/importers/lastpassCsvImporter.spec.ts b/spec/common/importers/lastpassCsvImporter.spec.ts new file mode 100644 index 0000000000..d573f0140a --- /dev/null +++ b/spec/common/importers/lastpassCsvImporter.spec.ts @@ -0,0 +1,136 @@ +import { LastPassCsvImporter as Importer } from '../../../src/importers/lastpassCsvImporter'; +import { CipherView } from '../../../src/models/view/cipherView'; + +import { Utils } from '../../../src/misc/utils'; + +if (Utils.isNode) { + // Polyfills + // tslint:disable-next-line + const jsdom: any = require('jsdom'); + (global as any).DOMParser = new jsdom.JSDOM().window.DOMParser; +} + +const CipherData = [ + { + title: 'should parse expiration date', + csv: `url,username,password,extra,name,grouping,fav +http://sn,,,"NoteType:Credit Card +Name on Card:John Doe +Type: +Number:1234567812345678 +Security Code:123 +Start Date:October,2017 +Expiration Date:June,2020 +Notes:some text +",Credit-card,,0`, + expected: Object.assign(new CipherView(), { + id: null, + organizationId: null, + folderId: null, + name: 'Credit-card', + notes: 'Start Date: October,2017\nsome text\n', + type: 3, + card: { + cardholderName: 'John Doe', + number: '1234567812345678', + code: '123', + expYear: '2020', + expMonth: '6', + }, + }), + }, + { + title: 'should parse blank card note', + csv: `url,username,password,extra,name,grouping,fav +http://sn,,,"NoteType:Credit Card +Name on Card: +Type: +Number: +Security Code: +Start Date:, +Expiration Date:, +Notes:",empty,,0`, + expected: Object.assign(new CipherView(), { + id: null, + organizationId: null, + folderId: null, + name: 'empty', + notes: `Start Date: ,`, + type: 3, + card: {}, + }), + }, + { + title: 'should parse card expiration date w/ no exp year', + csv: `url,username,password,extra,name,grouping,fav +http://sn,,,"NoteType:Credit Card +Name on Card:John Doe +Type:Visa +Number:1234567887654321 +Security Code:321 +Start Date:, +Expiration Date:January, +Notes:",noyear,,0`, + expected: Object.assign(new CipherView(), { + id: null, + organizationId: null, + folderId: null, + name: 'noyear', + notes: `Type: Visa +Start Date: ,`, + type: 3, + card: { + cardholderName: 'John Doe', + number: '1234567887654321', + code: '321', + expMonth: '1', + }, + }), + }, + { + title: 'should parse card expiration date w/ no month', + csv: `url,username,password,extra,name,grouping,fav +http://sn,,,"NoteType:Credit Card +Name on Card:John Doe +Type:Mastercard +Number:8765432112345678 +Security Code:987 +Start Date:, +Expiration Date:,2020 +Notes:",nomonth,,0`, + expected: Object.assign(new CipherView(), { + id: null, + organizationId: null, + folderId: null, + name: 'nomonth', + notes: `Type: Mastercard +Start Date: ,`, + type: 3, + card: { + cardholderName: 'John Doe', + number: '8765432112345678', + code: '987', + expYear: '2020', + }, + }), + }, +]; + +describe('Lastpass CSV Importer', () => { + CipherData.forEach((data) => { + it(data.title, async () => { + const importer = new Importer(); + const result = importer.parse(data.csv); + expect(result != null).toBe(true); + expect(result.ciphers.length).toBeGreaterThan(0); + + const cipher = result.ciphers.shift(); + for (const property in data.expected) { + if (data.expected.hasOwnProperty(property)) { + expect(cipher.hasOwnProperty(property)).toBe(true); + expect(cipher[property]).toEqual(data.expected[property]); + } + } + }); + }); +}); diff --git a/src/importers/lastpassCsvImporter.ts b/src/importers/lastpassCsvImporter.ts index a068d6349a..e9c5aac3b5 100644 --- a/src/importers/lastpassCsvImporter.ts +++ b/src/importers/lastpassCsvImporter.ts @@ -170,7 +170,29 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { 'Number': 'number', 'Name on Card': 'cardholderName', 'Security Code': 'code', + // LP provides date in a format like 'June,2020' + // Store in expMonth, then parse and modify + 'Expiration Date': 'expMonth', }); + + const exp = mappedData[0].expMonth; + if (exp === ',' || this.isNullOrWhitespace(exp)) { + // No expiration data + delete mappedData[0].expMonth; + } else { + const [monthString, year] = exp.split(','); + // Parse month name into number + if (!this.isNullOrWhitespace(monthString)) { + const month = new Date(Date.parse(monthString + '1, 2012')).getMonth() + 1; + mappedData[0].expMonth = month.toString(); + } else { + delete mappedData[0].expMonth; + } + if (!this.isNullOrWhitespace(year)) { + mappedData[0].expYear = year; + } + } + cipher.type = CipherType.Card; cipher.card = mappedData[0]; cipher.notes = mappedData[1]; From 3c6f6dbe2f9c51400b7a8977c8b6492f1892ca9d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 6 Feb 2020 15:03:55 -0500 Subject: [PATCH 0930/1626] parse extra note fields into custom fields --- src/importers/lastpassCsvImporter.ts | 49 +++++++++++++--------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/src/importers/lastpassCsvImporter.ts b/src/importers/lastpassCsvImporter.ts index e9c5aac3b5..8c34cea6c9 100644 --- a/src/importers/lastpassCsvImporter.ts +++ b/src/importers/lastpassCsvImporter.ts @@ -166,7 +166,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { if (typeParts.length > 1 && typeParts[0] === 'NoteType' && (typeParts[1] === 'Credit Card' || typeParts[1] === 'Address')) { if (typeParts[1] === 'Credit Card') { - const mappedData = this.parseSecureNoteMapping(extraParts, { + const mappedData = this.parseSecureNoteMapping(cipher, extraParts, { 'Number': 'number', 'Name on Card': 'cardholderName', 'Security Code': 'code', @@ -175,29 +175,31 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { 'Expiration Date': 'expMonth', }); - const exp = mappedData[0].expMonth; - if (exp === ',' || this.isNullOrWhitespace(exp)) { + if (this.isNullOrWhitespace(mappedData.expMonth) || mappedData.expMonth === ',') { // No expiration data - delete mappedData[0].expMonth; + mappedData.expMonth = null; } else { - const [monthString, year] = exp.split(','); + const [monthString, year] = mappedData.expMonth.split(','); // Parse month name into number if (!this.isNullOrWhitespace(monthString)) { - const month = new Date(Date.parse(monthString + '1, 2012')).getMonth() + 1; - mappedData[0].expMonth = month.toString(); + const month = new Date(Date.parse(monthString.trim() + ' 1, 2012')).getMonth() + 1; + if (isNaN(month)) { + mappedData.expMonth = null; + } else { + mappedData.expMonth = month.toString(); + } } else { - delete mappedData[0].expMonth; + mappedData.expMonth = null; } if (!this.isNullOrWhitespace(year)) { - mappedData[0].expYear = year; + mappedData.expYear = year; } } cipher.type = CipherType.Card; - cipher.card = mappedData[0]; - cipher.notes = mappedData[1]; + cipher.card = mappedData; } else if (typeParts[1] === 'Address') { - const mappedData = this.parseSecureNoteMapping(extraParts, { + const mappedData = this.parseSecureNoteMapping(cipher, extraParts, { 'Title': 'title', 'First Name': 'firstName', 'Last Name': 'lastName', @@ -214,8 +216,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { 'Username': 'username', }); cipher.type = CipherType.Identity; - cipher.identity = mappedData[0]; - cipher.notes = mappedData[1]; + cipher.identity = mappedData; } processedNote = true; } @@ -228,8 +229,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { } } - private parseSecureNoteMapping(extraParts: string[], map: any): [T, string] { - let notes: string = null; + private parseSecureNoteMapping(cipher: CipherView, extraParts: string[], map: any): T { const dataObj: any = {}; let processingNotes = false; @@ -255,26 +255,21 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { } if (processingNotes) { - notes += ('\n' + extraPart); + cipher.notes += ('\n' + extraPart); } else if (key === 'Notes') { - if (!this.isNullOrWhitespace(notes)) { - notes += ('\n' + val); + if (!this.isNullOrWhitespace(cipher.notes)) { + cipher.notes += ('\n' + val); } else { - notes = val; + cipher.notes = val; } processingNotes = true; } else if (map.hasOwnProperty(key)) { dataObj[map[key]] = val; } else { - if (!this.isNullOrWhitespace(notes)) { - notes += '\n'; - } else { - notes = ''; - } - notes += (key + ': ' + val); + this.processKvp(cipher, key, val); } }); - return [dataObj as T, notes]; + return dataObj; } } From 76f60dd99e94d78292098d5949f6011e8032caca Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 6 Feb 2020 15:28:17 -0500 Subject: [PATCH 0931/1626] fix lastpass importer tests --- .../importers/lastpassCsvImporter.spec.ts | 135 ++++++++++++------ src/importers/lastpassCsvImporter.ts | 6 +- 2 files changed, 92 insertions(+), 49 deletions(-) diff --git a/spec/common/importers/lastpassCsvImporter.spec.ts b/spec/common/importers/lastpassCsvImporter.spec.ts index d573f0140a..d224792db8 100644 --- a/spec/common/importers/lastpassCsvImporter.spec.ts +++ b/spec/common/importers/lastpassCsvImporter.spec.ts @@ -1,8 +1,12 @@ import { LastPassCsvImporter as Importer } from '../../../src/importers/lastpassCsvImporter'; + import { CipherView } from '../../../src/models/view/cipherView'; +import { FieldView } from '../../../src/models/view/fieldView'; import { Utils } from '../../../src/misc/utils'; +import { FieldType } from '../../../src/enums'; + if (Utils.isNode) { // Polyfills // tslint:disable-next-line @@ -24,19 +28,26 @@ Expiration Date:June,2020 Notes:some text ",Credit-card,,0`, expected: Object.assign(new CipherView(), { - id: null, - organizationId: null, - folderId: null, - name: 'Credit-card', - notes: 'Start Date: October,2017\nsome text\n', - type: 3, - card: { - cardholderName: 'John Doe', - number: '1234567812345678', - code: '123', - expYear: '2020', - expMonth: '6', - }, + id: null, + organizationId: null, + folderId: null, + name: 'Credit-card', + notes: 'some text\n', + type: 3, + card: { + cardholderName: 'John Doe', + number: '1234567812345678', + code: '123', + expYear: '2020', + expMonth: '6', + }, + fields: [ + Object.assign(new FieldView(), { + name: 'Start Date', + value: 'October,2017', + type: FieldType.Text, + }), + ], }), }, { @@ -51,13 +62,22 @@ Start Date:, Expiration Date:, Notes:",empty,,0`, expected: Object.assign(new CipherView(), { - id: null, - organizationId: null, - folderId: null, - name: 'empty', - notes: `Start Date: ,`, - type: 3, - card: {}, + id: null, + organizationId: null, + folderId: null, + name: 'empty', + notes: null, + type: 3, + card: { + expMonth: undefined, + }, + fields: [ + Object.assign(new FieldView(), { + name: 'Start Date', + value: ',', + type: FieldType.Text, + }), + ], }), }, { @@ -72,19 +92,30 @@ Start Date:, Expiration Date:January, Notes:",noyear,,0`, expected: Object.assign(new CipherView(), { - id: null, - organizationId: null, - folderId: null, - name: 'noyear', - notes: `Type: Visa -Start Date: ,`, - type: 3, - card: { - cardholderName: 'John Doe', - number: '1234567887654321', - code: '321', - expMonth: '1', - }, + id: null, + organizationId: null, + folderId: null, + name: 'noyear', + notes: null, + type: 3, + card: { + cardholderName: 'John Doe', + number: '1234567887654321', + code: '321', + expMonth: '1', + }, + fields: [ + Object.assign(new FieldView(), { + name: 'Type', + value: 'Visa', + type: FieldType.Text, + }), + Object.assign(new FieldView(), { + name: 'Start Date', + value: ',', + type: FieldType.Text, + }), + ], }), }, { @@ -99,19 +130,31 @@ Start Date:, Expiration Date:,2020 Notes:",nomonth,,0`, expected: Object.assign(new CipherView(), { - id: null, - organizationId: null, - folderId: null, - name: 'nomonth', - notes: `Type: Mastercard -Start Date: ,`, - type: 3, - card: { - cardholderName: 'John Doe', - number: '8765432112345678', - code: '987', - expYear: '2020', - }, + id: null, + organizationId: null, + folderId: null, + name: 'nomonth', + notes: null, + type: 3, + card: { + cardholderName: 'John Doe', + number: '8765432112345678', + code: '987', + expYear: '2020', + expMonth: undefined, + }, + fields: [ + Object.assign(new FieldView(), { + name: 'Type', + value: 'Mastercard', + type: FieldType.Text, + }), + Object.assign(new FieldView(), { + name: 'Start Date', + value: ',', + type: FieldType.Text, + }), + ], }), }, ]; diff --git a/src/importers/lastpassCsvImporter.ts b/src/importers/lastpassCsvImporter.ts index 8c34cea6c9..fdb0da2991 100644 --- a/src/importers/lastpassCsvImporter.ts +++ b/src/importers/lastpassCsvImporter.ts @@ -177,19 +177,19 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { if (this.isNullOrWhitespace(mappedData.expMonth) || mappedData.expMonth === ',') { // No expiration data - mappedData.expMonth = null; + mappedData.expMonth = undefined; } else { const [monthString, year] = mappedData.expMonth.split(','); // Parse month name into number if (!this.isNullOrWhitespace(monthString)) { const month = new Date(Date.parse(monthString.trim() + ' 1, 2012')).getMonth() + 1; if (isNaN(month)) { - mappedData.expMonth = null; + mappedData.expMonth = undefined; } else { mappedData.expMonth = month.toString(); } } else { - mappedData.expMonth = null; + mappedData.expMonth = undefined; } if (!this.isNullOrWhitespace(year)) { mappedData.expYear = year; From 3b8df85241256e69d5e4b035d9e123bf7f59de5b Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Fri, 7 Feb 2020 09:42:15 -0600 Subject: [PATCH 0932/1626] Show cipher collection ids during clone mode (#67) --- src/angular/components/add-edit.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index eea9e3dde4..4eb6a8ca38 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -201,7 +201,7 @@ export class AddEditComponent implements OnInit { } } - if (this.cipher != null && (!this.editMode || addEditCipherInfo != null)) { + if (this.cipher != null && (!this.editMode || addEditCipherInfo != null || this.cloneMode)) { await this.organizationChanged(); if (this.collectionIds != null && this.collectionIds.length > 0 && this.collections.length > 0) { this.collections.forEach((c) => { From 2d726ee3af3b347f459150643ca1d59637bccac7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 7 Feb 2020 16:48:31 -0500 Subject: [PATCH 0933/1626] add check payment method type --- src/enums/paymentMethodType.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/enums/paymentMethodType.ts b/src/enums/paymentMethodType.ts index 428eb74f0d..da3e50f6ea 100644 --- a/src/enums/paymentMethodType.ts +++ b/src/enums/paymentMethodType.ts @@ -7,4 +7,5 @@ export enum PaymentMethodType { WireTransfer = 5, AppleInApp = 6, GoogleInApp = 7, + Check = 8, } From fd260dfbae1717cdc019353f091501ac1dabe45f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 10 Feb 2020 10:20:45 -0500 Subject: [PATCH 0934/1626] null check to be consistent with mobile codebase --- src/services/export.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/export.service.ts b/src/services/export.service.ts index 7c5ef3cc83..a9b67f617e 100644 --- a/src/services/export.service.ts +++ b/src/services/export.service.ts @@ -44,7 +44,9 @@ export class ExportService implements ExportServiceAbstraction { if (format === 'csv') { const foldersMap = new Map(); decFolders.forEach((f) => { - foldersMap.set(f.id, f); + if (f.id != null) { + foldersMap.set(f.id, f); + } }); const exportCiphers: any[] = []; From fea1c9ada48978412f7b2705e07138ef6e812ce8 Mon Sep 17 00:00:00 2001 From: balagurusurendar Date: Tue, 11 Feb 2020 16:49:39 +0530 Subject: [PATCH 0935/1626] zoho import password header chnges (#69) * zoho import password header chnges * support for both import format --- src/importers/zohoVaultCsvImporter.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/importers/zohoVaultCsvImporter.ts b/src/importers/zohoVaultCsvImporter.ts index f2cc12d1c1..3c86d4fa87 100644 --- a/src/importers/zohoVaultCsvImporter.ts +++ b/src/importers/zohoVaultCsvImporter.ts @@ -14,15 +14,15 @@ export class ZohoVaultCsvImporter extends BaseImporter implements Importer { } results.forEach((value) => { - if (this.isNullOrWhitespace(value['Secret Name'])) { + if (this.isNullOrWhitespace(value['Password Name']) && this.isNullOrWhitespace(value['Secret Name'])) { return; } this.processFolder(result, this.getValueOrDefault(value.ChamberName)); const cipher = this.initLoginCipher(); cipher.favorite = this.getValueOrDefault(value.Favorite, '0') === '1'; cipher.notes = this.getValueOrDefault(value.Notes); - cipher.name = this.getValueOrDefault(value['Secret Name'], '--'); - cipher.login.uris = this.makeUriArray(value['Secret URL']); + cipher.name = this.getValueOrDefault(value['Password Name'], this.getValueOrDefault(value['Secret Name'], '--')); + cipher.login.uris = this.makeUriArray(this.getValueOrDefault(value['Password URL'], this.getValueOrDefault(value['Secret URL']))); this.parseData(cipher, value.SecretData); this.parseData(cipher, value.CustomData); this.convertToNoteIfNeeded(cipher); From 118bdb20b6daf67f2c0a247898ceb9a44964887f Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Wed, 12 Feb 2020 15:17:24 -0600 Subject: [PATCH 0936/1626] Fix lint warning with recent merge (#70) --- src/importers/onepassword1PifImporter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/importers/onepassword1PifImporter.ts b/src/importers/onepassword1PifImporter.ts index cd5cfc1f77..d2aaa5c3db 100644 --- a/src/importers/onepassword1PifImporter.ts +++ b/src/importers/onepassword1PifImporter.ts @@ -22,7 +22,7 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { return; } const item = JSON.parse(line); - if(item.trashed === true) { + if (item.trashed === true) { return; } const cipher = this.initLoginCipher(); From f8ada7913567f3f93183ddd1ef66f4c9cdfa59d4 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Tue, 18 Feb 2020 21:17:21 -0600 Subject: [PATCH 0937/1626] Added missing clone mode condition (#72) --- src/angular/components/add-edit.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 4eb6a8ca38..e0bbc59dc4 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -227,7 +227,7 @@ export class AddEditComponent implements OnInit { return false; } - if (!this.editMode && this.cipher.type === CipherType.Login && + if ((!this.editMode || this.cloneMode) && this.cipher.type === CipherType.Login && this.cipher.login.uris != null && this.cipher.login.uris.length === 1 && (this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === '')) { this.cipher.login.uris = null; From 98ae9b06294e424decbe0ac1ed27e0bb927c79f8 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 18 Feb 2020 22:33:47 -0500 Subject: [PATCH 0938/1626] bitwarden inc. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 60e94cb4a4..03e5e619c6 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "keywords": [ "bitwarden" ], - "author": "8bit Solutions LLC", + "author": "Bitwarden Inc.", "homepage": "https://bitwarden.com", "repository": { "type": "git", From ab9bee29b8ebf0b2812fcc5e5a8e7c36a3868ebd Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 19 Feb 2020 14:51:46 -0500 Subject: [PATCH 0939/1626] support for encryptr csv importer (#73) --- src/importers/encryptrCsvImporter.ts | 54 +++++++++++++++++++++++++++ src/importers/zohoVaultCsvImporter.ts | 6 ++- src/services/import.service.ts | 4 ++ 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 src/importers/encryptrCsvImporter.ts diff --git a/src/importers/encryptrCsvImporter.ts b/src/importers/encryptrCsvImporter.ts new file mode 100644 index 0000000000..edd2a41a89 --- /dev/null +++ b/src/importers/encryptrCsvImporter.ts @@ -0,0 +1,54 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +import { CardView } from '../models/view/cardView'; + +import { CipherType } from '../enums/cipherType'; + +export class EncryptrCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(value.Label, '--'); + cipher.notes = this.getValueOrDefault(value.Notes); + + const type = value['Entry Type']; + if (type === 'Password') { + cipher.login.username = this.getValueOrDefault(value.Username); + cipher.login.password = this.getValueOrDefault(value.Password); + cipher.login.uris = this.makeUriArray(value['Site URL']); + } else if (type === 'Credit Card') { + cipher.type = CipherType.Card; + cipher.card = new CardView(); + cipher.card.cardholderName = this.getValueOrDefault(value['Name on card']); + cipher.card.number = this.getValueOrDefault(value['Card Number']); + cipher.card.brand = this.getCardBrand(cipher.card.number); + cipher.card.code = this.getValueOrDefault(value.CVV); + const expiry = this.getValueOrDefault(value.Expiry); + if (!this.isNullOrWhitespace(expiry)) { + const expParts = expiry.split('/'); + if (expParts.length > 1) { + cipher.card.expMonth = parseInt(expParts[0], null).toString(); + cipher.card.expYear = (2000 + parseInt(expParts[1], null)).toString(); + } + } + } + + this.convertToNoteIfNeeded(cipher); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} diff --git a/src/importers/zohoVaultCsvImporter.ts b/src/importers/zohoVaultCsvImporter.ts index 3c86d4fa87..cf2403e552 100644 --- a/src/importers/zohoVaultCsvImporter.ts +++ b/src/importers/zohoVaultCsvImporter.ts @@ -21,8 +21,10 @@ export class ZohoVaultCsvImporter extends BaseImporter implements Importer { const cipher = this.initLoginCipher(); cipher.favorite = this.getValueOrDefault(value.Favorite, '0') === '1'; cipher.notes = this.getValueOrDefault(value.Notes); - cipher.name = this.getValueOrDefault(value['Password Name'], this.getValueOrDefault(value['Secret Name'], '--')); - cipher.login.uris = this.makeUriArray(this.getValueOrDefault(value['Password URL'], this.getValueOrDefault(value['Secret URL']))); + cipher.name = this.getValueOrDefault( + value['Password Name'], this.getValueOrDefault(value['Secret Name'], '--')); + cipher.login.uris = this.makeUriArray( + this.getValueOrDefault(value['Password URL'], this.getValueOrDefault(value['Secret URL']))); this.parseData(cipher, value.SecretData); this.parseData(cipher, value.CustomData); this.convertToNoteIfNeeded(cipher); diff --git a/src/services/import.service.ts b/src/services/import.service.ts index 8bf4f0a905..e7b58550ca 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -36,6 +36,7 @@ import { ChromeCsvImporter } from '../importers/chromeCsvImporter'; import { ClipperzHtmlImporter } from '../importers/clipperzHtmlImporter'; import { CodebookCsvImporter } from '../importers/codebookCsvImporter'; import { DashlaneJsonImporter } from '../importers/dashlaneJsonImporter'; +import { EncryptrCsvImporter } from '../importers/encryptrCsvImporter'; import { EnpassCsvImporter } from '../importers/enpassCsvImporter'; import { EnpassJsonImporter } from '../importers/enpassJsonImporter'; import { FirefoxCsvImporter } from '../importers/firefoxCsvImporter'; @@ -129,6 +130,7 @@ export class ImportService implements ImportServiceAbstraction { { id: 'blackberrycsv', name: 'BlackBerry Password Keeper (csv)' }, { id: 'buttercupcsv', name: 'Buttercup (csv)' }, { id: 'codebookcsv', name: 'Codebook (csv)' }, + { id: 'encryptrcsv', name: 'Encryptr (csv)' }, ]; constructor(private cipherService: CipherService, private folderService: FolderService, @@ -275,6 +277,8 @@ export class ImportService implements ImportServiceAbstraction { return new ButtercupCsvImporter(); case 'codebookcsv': return new CodebookCsvImporter(); + case 'encryptrcsv': + return new EncryptrCsvImporter(); default: return null; } From 29635bf9da161b591dde9b64cf7fe8b9a3884c1b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 20 Feb 2020 16:53:01 -0500 Subject: [PATCH 0940/1626] support for encryptr text column --- src/importers/encryptrCsvImporter.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/importers/encryptrCsvImporter.ts b/src/importers/encryptrCsvImporter.ts index edd2a41a89..a1e57899e5 100644 --- a/src/importers/encryptrCsvImporter.ts +++ b/src/importers/encryptrCsvImporter.ts @@ -20,6 +20,14 @@ export class EncryptrCsvImporter extends BaseImporter implements Importer { const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(value.Label, '--'); cipher.notes = this.getValueOrDefault(value.Notes); + const text = this.getValueOrDefault(value.Text); + if (!this.isNullOrWhitespace(text)) { + if (this.isNullOrWhitespace(cipher.notes)) { + cipher.notes = text; + } else { + cipher.notes += ('\n\n' + text); + } + } const type = value['Entry Type']; if (type === 'Password') { From 862057dca6bf23b62bf16f2a065c06996d83c5c3 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Wed, 26 Feb 2020 16:38:11 -0600 Subject: [PATCH 0941/1626] Enforce Password Generator Policy (#75) * Enforce Password Generator Policy * Move policy enforcement to service layer * Fixed typo (vscode didn't warn..) and adjust import spacing * Made requested changes --- .../passwordGeneration.service.ts | 4 +- .../password-generator.component.ts | 19 +++- .../domain/passwordGeneratorPolicyOptions.ts | 11 ++ src/services/passwordGeneration.service.ts | 103 +++++++++++++++++- 4 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 src/models/domain/passwordGeneratorPolicyOptions.ts diff --git a/src/abstractions/passwordGeneration.service.ts b/src/abstractions/passwordGeneration.service.ts index a94283d73a..e4ed293d44 100644 --- a/src/abstractions/passwordGeneration.service.ts +++ b/src/abstractions/passwordGeneration.service.ts @@ -1,9 +1,11 @@ import { GeneratedPasswordHistory } from '../models/domain/generatedPasswordHistory'; +import { PasswordGeneratorPolicyOptions } from '../models/domain/passwordGeneratorPolicyOptions'; export abstract class PasswordGenerationService { generatePassword: (options: any) => Promise; generatePassphrase: (options: any) => Promise; - getOptions: () => any; + getOptions: () => Promise<[any, PasswordGeneratorPolicyOptions]>; + getPasswordGeneratorPolicyOptions: () => Promise; saveOptions: (options: any) => Promise; getHistory: () => Promise; addHistory: (password: string) => Promise; diff --git a/src/angular/components/password-generator.component.ts b/src/angular/components/password-generator.component.ts index 97f4bc9695..f35892d7b2 100644 --- a/src/angular/components/password-generator.component.ts +++ b/src/angular/components/password-generator.component.ts @@ -9,6 +9,8 @@ import { I18nService } from '../../abstractions/i18n.service'; import { PasswordGenerationService } from '../../abstractions/passwordGeneration.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { PasswordGeneratorPolicyOptions } from '../../models/domain/passwordGeneratorPolicyOptions'; + export class PasswordGeneratorComponent implements OnInit { @Input() showSelect: boolean = false; @Output() onSelected = new EventEmitter(); @@ -17,13 +19,16 @@ export class PasswordGeneratorComponent implements OnInit { password: string = '-'; showOptions = false; avoidAmbiguous = false; + enforcedPolicyOptions: PasswordGeneratorPolicyOptions; constructor(protected passwordGenerationService: PasswordGenerationService, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, private win: Window) { } async ngOnInit() { - this.options = await this.passwordGenerationService.getOptions(); + const optionsResponse = await this.passwordGenerationService.getOptions(); + this.options = optionsResponse[0]; + this.enforcedPolicyOptions = optionsResponse[1]; this.avoidAmbiguous = !this.options.ambiguous; this.options.type = this.options.type === 'passphrase' ? 'passphrase' : 'password'; this.password = await this.passwordGenerationService.generatePassword(this.options); @@ -95,6 +100,10 @@ export class PasswordGeneratorComponent implements OnInit { this.options.length = 128; } + if (this.options.length < this.enforcedPolicyOptions.minLength) { + this.options.length = this.enforcedPolicyOptions.minLength; + } + if (!this.options.minNumber) { this.options.minNumber = 0; } else if (this.options.minNumber > this.options.length) { @@ -103,6 +112,10 @@ export class PasswordGeneratorComponent implements OnInit { this.options.minNumber = 9; } + if (this.options.minNumber < this.enforcedPolicyOptions.numberCount) { + this.options.minNumber = this.enforcedPolicyOptions.numberCount; + } + if (!this.options.minSpecial) { this.options.minSpecial = 0; } else if (this.options.minSpecial > this.options.length) { @@ -111,6 +124,10 @@ export class PasswordGeneratorComponent implements OnInit { this.options.minSpecial = 9; } + if (this.options.minSpecial < this.enforcedPolicyOptions.specialCount) { + this.options.minSpecial = this.enforcedPolicyOptions.specialCount; + } + if (this.options.minSpecial + this.options.minNumber > this.options.length) { this.options.minSpecial = this.options.length - this.options.minNumber; } diff --git a/src/models/domain/passwordGeneratorPolicyOptions.ts b/src/models/domain/passwordGeneratorPolicyOptions.ts new file mode 100644 index 0000000000..fb68016b95 --- /dev/null +++ b/src/models/domain/passwordGeneratorPolicyOptions.ts @@ -0,0 +1,11 @@ +import Domain from './domainBase'; + +export class PasswordGeneratorPolicyOptions extends Domain { + minLength: number = 0; + useUppercase: boolean = false; + useLowercase: boolean = false; + useNumbers: boolean = false; + numberCount: number = 0; + useSpecial: boolean = false; + specialCount: number = 0; +} diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 7270fdaf69..7e24b9f665 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -2,15 +2,20 @@ import * as zxcvbn from 'zxcvbn'; import { CipherString } from '../models/domain/cipherString'; import { GeneratedPasswordHistory } from '../models/domain/generatedPasswordHistory'; +import { PasswordGeneratorPolicyOptions } from '../models/domain/passwordGeneratorPolicyOptions'; +import { Policy } from '../models/domain/policy'; import { CryptoService } from '../abstractions/crypto.service'; import { PasswordGenerationService as PasswordGenerationServiceAbstraction, } from '../abstractions/passwordGeneration.service'; +import { PolicyService } from '../abstractions/policy.service'; import { StorageService } from '../abstractions/storage.service'; import { EEFLongWordList } from '../misc/wordlist'; +import { PolicyType } from '../enums/policyType'; + const DefaultOptions = { length: 14, ambiguous: false, @@ -40,7 +45,8 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr private optionsCache: any; private history: GeneratedPasswordHistory[]; - constructor(private cryptoService: CryptoService, private storageService: StorageService) { } + constructor(private cryptoService: CryptoService, private storageService: StorageService, + private policyService: PolicyService) { } async generatePassword(options: any): Promise { // overload defaults with given options @@ -207,7 +213,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return wordList.join(o.wordSeparator); } - async getOptions() { + async getOptions(): Promise<[any, PasswordGeneratorPolicyOptions]> { if (this.optionsCache == null) { const options = await this.storageService.get(Keys.options); if (options == null) { @@ -217,7 +223,98 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr } } - return this.optionsCache; + let enforcedPolicyOptions = await this.getPasswordGeneratorPolicyOptions(); + + if (enforcedPolicyOptions != null) { + if (this.optionsCache.length < enforcedPolicyOptions.minLength) { + this.optionsCache.length = enforcedPolicyOptions.minLength; + } + + if (enforcedPolicyOptions.useUppercase) { + this.optionsCache.uppercase = true; + } + + if (enforcedPolicyOptions.useLowercase) { + this.optionsCache.lowercase = true; + } + + if (enforcedPolicyOptions.useNumbers) { + this.optionsCache.number = true; + } + + if (this.optionsCache.minNumber < enforcedPolicyOptions.numberCount) { + this.optionsCache.minNumber = enforcedPolicyOptions.numberCount; + } + + if (enforcedPolicyOptions.useSpecial) { + this.optionsCache.special = true; + } + + if (this.optionsCache.minSpecial < enforcedPolicyOptions.specialCount) { + this.optionsCache.minSpecial = enforcedPolicyOptions.specialCount; + } + + // Must normalize these fields because the receiving call expects all options to pass the current rules + if (this.optionsCache.minSpecial + this.optionsCache.minNumber > this.optionsCache.length) { + this.optionsCache.minSpecial = this.optionsCache.length - this.optionsCache.minNumber; + } + } else { // UI layer expects an instantiated object to prevent more explicit null checks + enforcedPolicyOptions = new PasswordGeneratorPolicyOptions(); + } + + return [this.optionsCache, enforcedPolicyOptions]; + } + + async getPasswordGeneratorPolicyOptions(): Promise { + const policies: Policy[] = await this.policyService.getAll(PolicyType.PasswordGenerator); + let enforcedOptions: PasswordGeneratorPolicyOptions = null; + + if (policies == null || policies.length === 0) { + return enforcedOptions; + } + + policies.forEach((currentPolicy) => { + if (!currentPolicy.enabled || currentPolicy.data == null) { + return; + } + + if (enforcedOptions == null) { + enforcedOptions = new PasswordGeneratorPolicyOptions(); + } + + if (currentPolicy.data.minLength != null + && currentPolicy.data.minLength > enforcedOptions.minLength) { + enforcedOptions.minLength = currentPolicy.data.minLength; + } + + if (currentPolicy.data.useUpper) { + enforcedOptions.useUppercase = true; + } + + if (currentPolicy.data.useLower) { + enforcedOptions.useLowercase = true; + } + + if (currentPolicy.data.useNumbers) { + enforcedOptions.useNumbers = true; + } + + if (currentPolicy.data.minNumbers != null + && currentPolicy.data.minNumbers > enforcedOptions.numberCount) { + enforcedOptions.numberCount = currentPolicy.data.minNumbers; + } + + if (currentPolicy.data.useSpecial) { + enforcedOptions.useSpecial = true; + } + + if (currentPolicy.data.minSpecial != null + && currentPolicy.data.minSpecial > enforcedOptions.specialCount) { + enforcedOptions.specialCount = currentPolicy.data.minSpecial; + } + }); + + return enforcedOptions; } async saveOptions(options: any) { From 6c529422048c95645304ceb14250c8efce938f34 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Fri, 28 Feb 2020 11:09:57 -0600 Subject: [PATCH 0942/1626] Show policy in effect banner for password generator (#76) --- .../components/password-generator.component.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/angular/components/password-generator.component.ts b/src/angular/components/password-generator.component.ts index f35892d7b2..a62d308cdf 100644 --- a/src/angular/components/password-generator.component.ts +++ b/src/angular/components/password-generator.component.ts @@ -79,6 +79,24 @@ export class PasswordGeneratorComponent implements OnInit { this.showOptions = !this.showOptions; } + hasPolicyInEffect() { + if (this.enforcedPolicyOptions == null) { + return false; + } + + if (this.enforcedPolicyOptions.minLength > 0 + || this.enforcedPolicyOptions.numberCount > 0 + || this.enforcedPolicyOptions.specialCount > 0 + || this.enforcedPolicyOptions.useUppercase + || this.enforcedPolicyOptions.useLowercase + || this.enforcedPolicyOptions.useNumbers + || this.enforcedPolicyOptions.useSpecial) { + return true; + } else { + return false; + } + } + private normalizeOptions() { this.options.minLowercase = 0; this.options.minUppercase = 0; From fb48091bb8eea894e623b4f68f8b18f43c793609 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 28 Feb 2020 16:57:34 -0500 Subject: [PATCH 0943/1626] Moved callout to jslib, made policyInEffect a prop (#77) * Moved callout to jslib, made policyInEffect a prop * remove true condition --- src/angular/components/callout.component.html | 7 +++ src/angular/components/callout.component.ts | 53 +++++++++++++++++++ .../password-generator.component.ts | 27 ++++------ 3 files changed, 69 insertions(+), 18 deletions(-) create mode 100644 src/angular/components/callout.component.html create mode 100644 src/angular/components/callout.component.ts diff --git a/src/angular/components/callout.component.html b/src/angular/components/callout.component.html new file mode 100644 index 0000000000..31e1d10e96 --- /dev/null +++ b/src/angular/components/callout.component.html @@ -0,0 +1,7 @@ + diff --git a/src/angular/components/callout.component.ts b/src/angular/components/callout.component.ts new file mode 100644 index 0000000000..34d84f07b6 --- /dev/null +++ b/src/angular/components/callout.component.ts @@ -0,0 +1,53 @@ +import { + Component, + Input, + OnInit, +} from '@angular/core'; + +import { I18nService } from '../../abstractions/i18n.service'; + +@Component({ + selector: 'app-callout', + templateUrl: 'callout.component.html', +}) +export class CalloutComponent implements OnInit { + @Input() type = 'info'; + @Input() icon: string; + @Input() title: string; + + calloutStyle: string; + + constructor(private i18nService: I18nService) { } + + ngOnInit() { + this.calloutStyle = this.type; + + if (this.type === 'warning' || this.type === 'danger') { + if (this.type === 'danger') { + this.calloutStyle = 'danger'; + } + if (this.title === undefined) { + this.title = this.i18nService.t('warning'); + } + if (this.icon === undefined) { + this.icon = 'fa-warning'; + } + } else if (this.type === 'error') { + this.calloutStyle = 'danger'; + if (this.title === undefined) { + this.title = this.i18nService.t('error'); + } + if (this.icon === undefined) { + this.icon = 'fa-bolt'; + } + } else if (this.type === 'tip') { + this.calloutStyle = 'success'; + if (this.title === undefined) { + this.title = this.i18nService.t('tip'); + } + if (this.icon === undefined) { + this.icon = 'fa-lightbulb-o'; + } + } + } +} diff --git a/src/angular/components/password-generator.component.ts b/src/angular/components/password-generator.component.ts index a62d308cdf..9b703a31cb 100644 --- a/src/angular/components/password-generator.component.ts +++ b/src/angular/components/password-generator.component.ts @@ -19,6 +19,7 @@ export class PasswordGeneratorComponent implements OnInit { password: string = '-'; showOptions = false; avoidAmbiguous = false; + policyInEffect = false; enforcedPolicyOptions: PasswordGeneratorPolicyOptions; constructor(protected passwordGenerationService: PasswordGenerationService, @@ -29,6 +30,14 @@ export class PasswordGeneratorComponent implements OnInit { const optionsResponse = await this.passwordGenerationService.getOptions(); this.options = optionsResponse[0]; this.enforcedPolicyOptions = optionsResponse[1]; + this.policyInEffect = this.enforcedPolicyOptions != null && ( + this.enforcedPolicyOptions.minLength > 0 || + this.enforcedPolicyOptions.numberCount > 0 || + this.enforcedPolicyOptions.specialCount > 0 || + this.enforcedPolicyOptions.useUppercase || + this.enforcedPolicyOptions.useLowercase || + this.enforcedPolicyOptions.useNumbers || + this.enforcedPolicyOptions.useSpecial); this.avoidAmbiguous = !this.options.ambiguous; this.options.type = this.options.type === 'passphrase' ? 'passphrase' : 'password'; this.password = await this.passwordGenerationService.generatePassword(this.options); @@ -79,24 +88,6 @@ export class PasswordGeneratorComponent implements OnInit { this.showOptions = !this.showOptions; } - hasPolicyInEffect() { - if (this.enforcedPolicyOptions == null) { - return false; - } - - if (this.enforcedPolicyOptions.minLength > 0 - || this.enforcedPolicyOptions.numberCount > 0 - || this.enforcedPolicyOptions.specialCount > 0 - || this.enforcedPolicyOptions.useUppercase - || this.enforcedPolicyOptions.useLowercase - || this.enforcedPolicyOptions.useNumbers - || this.enforcedPolicyOptions.useSpecial) { - return true; - } else { - return false; - } - } - private normalizeOptions() { this.options.minLowercase = 0; this.options.minUppercase = 0; From 4aecc53ddef892d22457cf32b4f8d140c9253688 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 2 Mar 2020 10:15:54 -0500 Subject: [PATCH 0944/1626] API for getting policies by token (#80) * API for getting policies by token * not authed when calling this API --- src/abstractions/api.service.ts | 2 ++ src/services/api.service.ts | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 359304b034..ac2d913b39 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -192,6 +192,8 @@ export abstract class ApiService { getPolicy: (organizationId: string, type: PolicyType) => Promise; getPolicies: (organizationId: string) => Promise>; + getPoliciesByToken: (organizationId: string, token: string, email: string, organizationUserId: string) => + Promise>; putPolicy: (organizationId: string, type: PolicyType, request: PolicyRequest) => Promise; getOrganizationUser: (organizationId: string, id: string) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 4e28e24a1c..a09d9e230b 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -564,6 +564,13 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, PolicyResponse); } + async getPoliciesByToken(organizationId: string, token: string, email: string, organizationUserId: string): + Promise> { + const r = await this.send('GET', '/organizations/' + organizationId + '/policies?token=' + token + + '&email=' + email + '&organizationUserId=' + organizationUserId, null, false, true); + return new ListResponse(r, PolicyResponse); + } + async putPolicy(organizationId: string, type: PolicyType, request: PolicyRequest): Promise { const r = await this.send('PUT', '/organizations/' + organizationId + '/policies/' + type, request, true, true); return new PolicyResponse(r); From 0f699515a40c2ab61683eadec8443d4c63da6536 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 2 Mar 2020 11:37:44 -0500 Subject: [PATCH 0945/1626] fixes to getPoliciesByToken (#81) --- src/services/api.service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/services/api.service.ts b/src/services/api.service.ts index a09d9e230b..b57ea4b824 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -566,8 +566,9 @@ export class ApiService implements ApiServiceAbstraction { async getPoliciesByToken(organizationId: string, token: string, email: string, organizationUserId: string): Promise> { - const r = await this.send('GET', '/organizations/' + organizationId + '/policies?token=' + token + - '&email=' + email + '&organizationUserId=' + organizationUserId, null, false, true); + const r = await this.send('GET', '/organizations/' + organizationId + '/policies/token?' + + 'token=' + encodeURIComponent(token) + '&email=' + encodeURIComponent(email) + + '&organizationUserId=' + organizationUserId, null, false, true); return new ListResponse(r, PolicyResponse); } From 6210396aa963667095b898fd63645899d84a28db Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Mon, 2 Mar 2020 11:05:05 -0600 Subject: [PATCH 0946/1626] Enforce master password policy (#79) * Enforce master password policy * Updated based on requested changes/discussions --- src/abstractions/policy.service.ts | 6 +- .../domain/masterPasswordPolicyOptions.ts | 10 +++ src/services/policy.service.ts | 86 +++++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 src/models/domain/masterPasswordPolicyOptions.ts diff --git a/src/abstractions/policy.service.ts b/src/abstractions/policy.service.ts index b08269644a..e281fb5855 100644 --- a/src/abstractions/policy.service.ts +++ b/src/abstractions/policy.service.ts @@ -1,6 +1,7 @@ import { PolicyData } from '../models/data/policyData'; -import { Policy } from '../models/domain/policy'; +import { MasterPasswordPolicyOptions } from '../models/domain/masterPasswordPolicyOptions' +import { Policy } from '../models/domain/policy' import { PolicyType } from '../enums/policyType'; @@ -11,4 +12,7 @@ export abstract class PolicyService { getAll: (type?: PolicyType) => Promise; replace: (policies: { [id: string]: PolicyData; }) => Promise; clear: (userId: string) => Promise; + getMasterPasswordPolicyOptions: () => Promise; + evaluateMasterPassword: (passwordStrength: number, newPassword: string, + enforcedPolicyOptions?: MasterPasswordPolicyOptions) => boolean; } diff --git a/src/models/domain/masterPasswordPolicyOptions.ts b/src/models/domain/masterPasswordPolicyOptions.ts new file mode 100644 index 0000000000..ed04a455bf --- /dev/null +++ b/src/models/domain/masterPasswordPolicyOptions.ts @@ -0,0 +1,10 @@ +import Domain from './domainBase'; + +export class MasterPasswordPolicyOptions extends Domain { + minComplexity: number = 0; + minLength: number = 0; + requireUpper: boolean = false; + requireLower: boolean = false; + requireNumbers: boolean = false; + requireSpecial: boolean = false; +} diff --git a/src/services/policy.service.ts b/src/services/policy.service.ts index 2baac07678..040b23ba76 100644 --- a/src/services/policy.service.ts +++ b/src/services/policy.service.ts @@ -5,6 +5,7 @@ import { UserService } from '../abstractions/user.service'; import { PolicyData } from '../models/data/policyData'; import { Policy } from '../models/domain/policy'; +import { MasterPasswordPolicyOptions } from '../models/domain/masterPasswordPolicyOptions' import { PolicyType } from '../enums/policyType'; @@ -52,4 +53,89 @@ export class PolicyService implements PolicyServiceAbstraction { await this.storageService.remove(Keys.policiesPrefix + userId); this.policyCache = null; } + + async getMasterPasswordPolicyOptions(policies?: Policy[]): Promise { + let enforcedOptions: MasterPasswordPolicyOptions = null; + + if (policies == null) { + policies = await this.getAll(PolicyType.MasterPassword); + } else { + policies = policies.filter((p) => p.type === PolicyType.MasterPassword); + } + + if (policies == null || policies.length === 0) { + return enforcedOptions; + } + + policies.forEach((currentPolicy) => { + if (!currentPolicy.enabled || currentPolicy.data == null) { + return; + } + + if (enforcedOptions == null) { + enforcedOptions = new MasterPasswordPolicyOptions(); + } + + if (currentPolicy.data.minComplexity != null + && currentPolicy.data.minComplexity > enforcedOptions.minComplexity) { + enforcedOptions.minComplexity = currentPolicy.data.minComplexity; + } + + if (currentPolicy.data.minLength != null + && currentPolicy.data.minLength > enforcedOptions.minLength) { + enforcedOptions.minLength = currentPolicy.data.minLength; + } + + if (currentPolicy.data.requireUpper) { + enforcedOptions.requireUpper = true; + } + + if (currentPolicy.data.requireLower) { + enforcedOptions.requireLower = true; + } + + if (currentPolicy.data.requireNumbers) { + enforcedOptions.requireNumbers = true; + } + + if (currentPolicy.data.requireSpecial) { + enforcedOptions.requireSpecial = true; + } + }); + + return enforcedOptions; + } + + evaluateMasterPassword(passwordStrength: number, newPassword: string, + enforcedPolicyOptions: MasterPasswordPolicyOptions): boolean { + if (enforcedPolicyOptions == null) { + return true; + } + + if (enforcedPolicyOptions.minComplexity > 0 && enforcedPolicyOptions.minComplexity > passwordStrength) { + return false; + } + + if (enforcedPolicyOptions.minLength > 0 && enforcedPolicyOptions.minLength > newPassword.length) { + return false; + } + + if (enforcedPolicyOptions.requireUpper && newPassword.toLocaleLowerCase() === newPassword) { + return false; + } + + if (enforcedPolicyOptions.requireLower && newPassword.toLocaleUpperCase() === newPassword) { + return false; + } + + if (enforcedPolicyOptions.requireNumbers && !(/[0-9]/.test(newPassword))) { + return false; + } + + if (enforcedPolicyOptions.requireSpecial && !(/[!@#$%\^&*]/g.test(newPassword))) { + return false; + } + + return true; + } } From da9b9b438c4028b16f0d02d9e83477dce9378a16 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Mon, 2 Mar 2020 13:45:06 -0600 Subject: [PATCH 0947/1626] Fixed lint warnings (#82) --- src/abstractions/policy.service.ts | 6 +++--- src/services/policy.service.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/abstractions/policy.service.ts b/src/abstractions/policy.service.ts index e281fb5855..ca0d3c9ed7 100644 --- a/src/abstractions/policy.service.ts +++ b/src/abstractions/policy.service.ts @@ -1,7 +1,7 @@ import { PolicyData } from '../models/data/policyData'; -import { MasterPasswordPolicyOptions } from '../models/domain/masterPasswordPolicyOptions' -import { Policy } from '../models/domain/policy' +import { MasterPasswordPolicyOptions } from '../models/domain/masterPasswordPolicyOptions'; +import { Policy } from '../models/domain/policy'; import { PolicyType } from '../enums/policyType'; @@ -12,7 +12,7 @@ export abstract class PolicyService { getAll: (type?: PolicyType) => Promise; replace: (policies: { [id: string]: PolicyData; }) => Promise; clear: (userId: string) => Promise; - getMasterPasswordPolicyOptions: () => Promise; + getMasterPasswordPolicyOptions: (policies?: Policy[]) => Promise; evaluateMasterPassword: (passwordStrength: number, newPassword: string, enforcedPolicyOptions?: MasterPasswordPolicyOptions) => boolean; } diff --git a/src/services/policy.service.ts b/src/services/policy.service.ts index 040b23ba76..6d3dcb7518 100644 --- a/src/services/policy.service.ts +++ b/src/services/policy.service.ts @@ -4,8 +4,8 @@ import { UserService } from '../abstractions/user.service'; import { PolicyData } from '../models/data/policyData'; +import { MasterPasswordPolicyOptions } from '../models/domain/masterPasswordPolicyOptions'; import { Policy } from '../models/domain/policy'; -import { MasterPasswordPolicyOptions } from '../models/domain/masterPasswordPolicyOptions' import { PolicyType } from '../enums/policyType'; From 44b86f5dd028271059b70a00d7878fbb1a06023f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 3 Mar 2020 16:03:26 -0500 Subject: [PATCH 0948/1626] enforce policies when options already known (#83) --- .../passwordGeneration.service.ts | 1 + src/services/passwordGeneration.service.ts | 33 ++++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/abstractions/passwordGeneration.service.ts b/src/abstractions/passwordGeneration.service.ts index e4ed293d44..aa4d514b57 100644 --- a/src/abstractions/passwordGeneration.service.ts +++ b/src/abstractions/passwordGeneration.service.ts @@ -5,6 +5,7 @@ export abstract class PasswordGenerationService { generatePassword: (options: any) => Promise; generatePassphrase: (options: any) => Promise; getOptions: () => Promise<[any, PasswordGeneratorPolicyOptions]>; + enforcePasswordGeneratorPoliciesOnOptions: (options: any) => Promise<[any, PasswordGeneratorPolicyOptions]>; getPasswordGeneratorPolicyOptions: () => Promise; saveOptions: (options: any) => Promise; getHistory: () => Promise; diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 7e24b9f665..6293555df4 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -222,47 +222,50 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr this.optionsCache = Object.assign({}, DefaultOptions, options); } } + const enforcedOptions = await this.enforcePasswordGeneratorPoliciesOnOptions(this.optionsCache); + this.optionsCache = enforcedOptions[0]; + return [this.optionsCache, enforcedOptions[1]]; + } + async enforcePasswordGeneratorPoliciesOnOptions(options: any): Promise<[any, PasswordGeneratorPolicyOptions]> { let enforcedPolicyOptions = await this.getPasswordGeneratorPolicyOptions(); - if (enforcedPolicyOptions != null) { - if (this.optionsCache.length < enforcedPolicyOptions.minLength) { - this.optionsCache.length = enforcedPolicyOptions.minLength; + if (options.length < enforcedPolicyOptions.minLength) { + options.length = enforcedPolicyOptions.minLength; } if (enforcedPolicyOptions.useUppercase) { - this.optionsCache.uppercase = true; + options.uppercase = true; } if (enforcedPolicyOptions.useLowercase) { - this.optionsCache.lowercase = true; + options.lowercase = true; } if (enforcedPolicyOptions.useNumbers) { - this.optionsCache.number = true; + options.number = true; } - if (this.optionsCache.minNumber < enforcedPolicyOptions.numberCount) { - this.optionsCache.minNumber = enforcedPolicyOptions.numberCount; + if (options.minNumber < enforcedPolicyOptions.numberCount) { + options.minNumber = enforcedPolicyOptions.numberCount; } if (enforcedPolicyOptions.useSpecial) { - this.optionsCache.special = true; + options.special = true; } - if (this.optionsCache.minSpecial < enforcedPolicyOptions.specialCount) { - this.optionsCache.minSpecial = enforcedPolicyOptions.specialCount; + if (options.minSpecial < enforcedPolicyOptions.specialCount) { + options.minSpecial = enforcedPolicyOptions.specialCount; } // Must normalize these fields because the receiving call expects all options to pass the current rules - if (this.optionsCache.minSpecial + this.optionsCache.minNumber > this.optionsCache.length) { - this.optionsCache.minSpecial = this.optionsCache.length - this.optionsCache.minNumber; + if (options.minSpecial + options.minNumber > options.length) { + options.minSpecial = options.length - options.minNumber; } } else { // UI layer expects an instantiated object to prevent more explicit null checks enforcedPolicyOptions = new PasswordGeneratorPolicyOptions(); } - - return [this.optionsCache, enforcedPolicyOptions]; + return [options, enforcedPolicyOptions]; } async getPasswordGeneratorPolicyOptions(): Promise { From ee8ca0beed5e04676281e0d1649e10cd3084e74a Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Tue, 10 Mar 2020 12:50:54 -0500 Subject: [PATCH 0949/1626] Password Generator Passphrase Policy (#85) * Initial commit for passphrase enforcement * Updated type implementation * Updated default type conditional * Added helper method to enforced options object Co-authored-by: Vincent Salucci --- .../password-generator.component.ts | 13 +++---- .../domain/passwordGeneratorPolicyOptions.ts | 18 ++++++++++ src/services/passwordGeneration.service.ts | 36 +++++++++++++++++++ 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/angular/components/password-generator.component.ts b/src/angular/components/password-generator.component.ts index 9b703a31cb..51f70bc8bf 100644 --- a/src/angular/components/password-generator.component.ts +++ b/src/angular/components/password-generator.component.ts @@ -19,7 +19,6 @@ export class PasswordGeneratorComponent implements OnInit { password: string = '-'; showOptions = false; avoidAmbiguous = false; - policyInEffect = false; enforcedPolicyOptions: PasswordGeneratorPolicyOptions; constructor(protected passwordGenerationService: PasswordGenerationService, @@ -30,14 +29,6 @@ export class PasswordGeneratorComponent implements OnInit { const optionsResponse = await this.passwordGenerationService.getOptions(); this.options = optionsResponse[0]; this.enforcedPolicyOptions = optionsResponse[1]; - this.policyInEffect = this.enforcedPolicyOptions != null && ( - this.enforcedPolicyOptions.minLength > 0 || - this.enforcedPolicyOptions.numberCount > 0 || - this.enforcedPolicyOptions.specialCount > 0 || - this.enforcedPolicyOptions.useUppercase || - this.enforcedPolicyOptions.useLowercase || - this.enforcedPolicyOptions.useNumbers || - this.enforcedPolicyOptions.useSpecial); this.avoidAmbiguous = !this.options.ambiguous; this.options.type = this.options.type === 'passphrase' ? 'passphrase' : 'password'; this.password = await this.passwordGenerationService.generatePassword(this.options); @@ -147,6 +138,10 @@ export class PasswordGeneratorComponent implements OnInit { this.options.numWords = 20; } + if (this.options.numWords < this.enforcedPolicyOptions.minNumberWords) { + this.options.numWords = this.enforcedPolicyOptions.minNumberWords; + } + if (this.options.wordSeparator != null && this.options.wordSeparator.length > 1) { this.options.wordSeparator = this.options.wordSeparator[0]; } diff --git a/src/models/domain/passwordGeneratorPolicyOptions.ts b/src/models/domain/passwordGeneratorPolicyOptions.ts index fb68016b95..d84b575a4d 100644 --- a/src/models/domain/passwordGeneratorPolicyOptions.ts +++ b/src/models/domain/passwordGeneratorPolicyOptions.ts @@ -1,6 +1,7 @@ import Domain from './domainBase'; export class PasswordGeneratorPolicyOptions extends Domain { + defaultType: string = ''; minLength: number = 0; useUppercase: boolean = false; useLowercase: boolean = false; @@ -8,4 +9,21 @@ export class PasswordGeneratorPolicyOptions extends Domain { numberCount: number = 0; useSpecial: boolean = false; specialCount: number = 0; + minNumberWords: number = 0; + capitalize: boolean = false; + includeNumber: boolean = false; + + inEffect() { + return this.defaultType !== '' || + this.minLength > 0 || + this.numberCount > 0 || + this.specialCount > 0 || + this.useUppercase || + this.useLowercase || + this.useNumbers || + this.useSpecial || + this.minNumberWords > 0 || + this.capitalize || + this.includeNumber; + } } diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 6293555df4..2af4c40ce5 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -262,6 +262,24 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr if (options.minSpecial + options.minNumber > options.length) { options.minSpecial = options.length - options.minNumber; } + + if (options.numWords < enforcedPolicyOptions.minNumberWords) { + options.numWords = enforcedPolicyOptions.minNumberWords; + } + + if (enforcedPolicyOptions.capitalize) { + options.capitalize = true; + } + + if (enforcedPolicyOptions.includeNumber) { + options.includeNumber = true; + } + + // Force default type if password/passphrase selected via policy + if (enforcedPolicyOptions.defaultType === 'password' || + enforcedPolicyOptions.defaultType === 'passphrase') { + options.type = enforcedPolicyOptions.defaultType; + } } else { // UI layer expects an instantiated object to prevent more explicit null checks enforcedPolicyOptions = new PasswordGeneratorPolicyOptions(); } @@ -285,6 +303,11 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr enforcedOptions = new PasswordGeneratorPolicyOptions(); } + // Password wins in multi-org collisions + if (currentPolicy.data.defaultType != null && enforcedOptions.defaultType !== 'password') { + enforcedOptions.defaultType = currentPolicy.data.defaultType; + } + if (currentPolicy.data.minLength != null && currentPolicy.data.minLength > enforcedOptions.minLength) { enforcedOptions.minLength = currentPolicy.data.minLength; @@ -315,6 +338,19 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr && currentPolicy.data.minSpecial > enforcedOptions.specialCount) { enforcedOptions.specialCount = currentPolicy.data.minSpecial; } + + if (currentPolicy.data.minNumberWords != null + && currentPolicy.data.minNumberWords > enforcedOptions.minNumberWords) { + enforcedOptions.minNumberWords = currentPolicy.data.minNumberWords; + } + + if (currentPolicy.data.capitalize) { + enforcedOptions.capitalize = true; + } + + if (currentPolicy.data.includeNumber) { + enforcedOptions.includeNumber = true; + } }); return enforcedOptions; From 36241e9eac029cfc3275e2a1d642ad5b0dfe8110 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 10 Mar 2020 15:00:29 -0400 Subject: [PATCH 0950/1626] bump user agent version for desktop --- src/electron/window.main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index 96d59ddf7e..c5d428d77f 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -121,7 +121,7 @@ export class WindowMain { pathname: path.join(__dirname, '/index.html'), slashes: true, }), { - userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0', + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0', }); // Open the DevTools. From d6c9acdf6f87ddb2e3f1a7e6efd3132b6dd6e2f6 Mon Sep 17 00:00:00 2001 From: mtgto Date: Wed, 11 Mar 2020 22:00:14 +0900 Subject: [PATCH 0951/1626] Add noImplicitAny to tsc compiler options (#86) --- spec/common/importers/lastpassCsvImporter.spec.ts | 3 ++- spec/common/misc/sequentialize.spec.ts | 8 ++++---- tsconfig.json | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/spec/common/importers/lastpassCsvImporter.spec.ts b/spec/common/importers/lastpassCsvImporter.spec.ts index d224792db8..6254a816fe 100644 --- a/spec/common/importers/lastpassCsvImporter.spec.ts +++ b/spec/common/importers/lastpassCsvImporter.spec.ts @@ -168,7 +168,8 @@ describe('Lastpass CSV Importer', () => { expect(result.ciphers.length).toBeGreaterThan(0); const cipher = result.ciphers.shift(); - for (const property in data.expected) { + let property: keyof typeof data.expected; + for (property in data.expected) { if (data.expected.hasOwnProperty(property)) { expect(cipher.hasOwnProperty(property)).toBe(true); expect(cipher[property]).toEqual(data.expected[property]); diff --git a/spec/common/misc/sequentialize.spec.ts b/spec/common/misc/sequentialize.spec.ts index 86e846b38e..f3ec1d0ec3 100644 --- a/spec/common/misc/sequentialize.spec.ts +++ b/spec/common/misc/sequentialize.spec.ts @@ -81,7 +81,7 @@ describe('sequentialize decorator', () => { it('should return correct result for each call', async () => { const foo = new Foo(); - const allRes = []; + const allRes: number[] = []; await Promise.all([ foo.bar(1).then((res) => allRes.push(res)), @@ -99,7 +99,7 @@ describe('sequentialize decorator', () => { it('should return correct result for each call with key function', async () => { const foo = new Foo(); - const allRes = []; + const allRes: number[] = []; await Promise.all([ foo.baz(1).then((res) => allRes.push(res)), @@ -120,7 +120,7 @@ class Foo { calls = 0; @sequentialize((args) => 'bar' + args[0]) - bar(a: number) { + bar(a: number): Promise { this.calls++; return new Promise((res) => { setTimeout(() => { @@ -130,7 +130,7 @@ class Foo { } @sequentialize((args) => 'baz' + args[0]) - baz(a: number) { + baz(a: number): Promise { this.calls++; return new Promise((res) => { setTimeout(() => { diff --git a/tsconfig.json b/tsconfig.json index 6f8a590992..7ed555a337 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "pretty": true, "moduleResolution": "node", + "noImplicitAny": true, "target": "ES6", "module": "commonjs", "lib": ["es5", "es6", "dom"], From 72b4c4401af5cba800a669e6cae30fefc18d8f4f Mon Sep 17 00:00:00 2001 From: mtgto Date: Wed, 11 Mar 2020 22:06:45 +0900 Subject: [PATCH 0952/1626] Add the lint to CI script (#87) --- appveyor.yml | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 1bef398056..1ff596d5b0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,6 +19,7 @@ before_build: build_script: - cmd: npm install +- cmd: npm run lint - cmd: npm run build - cmd: npm run test - cmd: 7z a coverage-%APPVEYOR_BUILD_NUMBER%.zip coverage\* diff --git a/package.json b/package.json index 03e5e619c6..d10190eccf 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "clean": "rimraf dist/**/*", "build": "npm run clean && tsc", "build:watch": "npm run clean && tsc -watch", - "lint": "tslint src/**/*.ts spec/**/*.ts || true", + "lint": "tslint src/**/*.ts spec/**/*.ts", "lint:fix": "tslint src/**/*.ts spec/**/*.ts --fix", "test": "karma start ./spec/support/karma.conf.js --single-run", "test:watch": "karma start ./spec/support/karma.conf.js", From 13d1067eda0b8f8680f80b5d7afdbac90423d3cc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 12 Mar 2020 15:16:22 -0400 Subject: [PATCH 0953/1626] null check on policies response prop --- src/services/sync.service.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index 9d915be8b8..c97968384f 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -306,9 +306,11 @@ export class SyncService implements SyncServiceAbstraction { private async syncPolicies(response: PolicyResponse[]) { const policies: { [id: string]: PolicyData; } = {}; - response.forEach((p) => { - policies[p.id] = new PolicyData(p); - }); + if (response != null) { + response.forEach((p) => { + policies[p.id] = new PolicyData(p); + }); + } return await this.policyService.replace(policies); } } From b816ddddff96b53a77620b8b31d6ca699f0a8195 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 12 Mar 2020 15:37:21 -0400 Subject: [PATCH 0954/1626] only block interaction if env var set --- src/cli/commands/login.command.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/commands/login.command.ts b/src/cli/commands/login.command.ts index 6801cf2413..f08c0fa6c0 100644 --- a/src/cli/commands/login.command.ts +++ b/src/cli/commands/login.command.ts @@ -22,7 +22,7 @@ export class LoginCommand { protected i18nService: I18nService) { } async run(email: string, password: string, cmd: program.Command) { - const canInteract = process.stdout.isTTY && process.env.BW_NOINTERACTION !== 'true'; + const canInteract = process.env.BW_NOINTERACTION !== 'true'; if ((email == null || email === '') && canInteract) { const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ type: 'input', From 0a30c7eb1ecbac500e6c55a7d4024d98efa982bc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 12 Mar 2020 16:59:59 -0400 Subject: [PATCH 0955/1626] fix 1password csv importer --- src/importers/onepasswordWinCsvImporter.ts | 39 +++++++++++++--------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/importers/onepasswordWinCsvImporter.ts b/src/importers/onepasswordWinCsvImporter.ts index 5b1a433704..0ca9a93ab1 100644 --- a/src/importers/onepasswordWinCsvImporter.ts +++ b/src/importers/onepasswordWinCsvImporter.ts @@ -6,7 +6,7 @@ import { ImportResult } from '../models/domain/importResult'; import { CipherType } from '../enums/cipherType'; import { CardView } from '../models/view'; -const IgnoredProperties = ['ainfo', 'autosubmit', 'notesPlain', 'ps', 'scope', 'tags', 'title', 'uuid']; +const IgnoredProperties = ['ainfo', 'autosubmit', 'notesplain', 'ps', 'scope', 'tags', 'title', 'uuid']; export class OnePasswordWinCsvImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { @@ -18,15 +18,16 @@ export class OnePasswordWinCsvImporter extends BaseImporter implements Importer } results.forEach((value) => { - if (this.isNullOrWhitespace(value.title)) { + if (this.isNullOrWhitespace(this.getProp(value, 'title'))) { return; } const cipher = this.initLoginCipher(); - cipher.name = this.getValueOrDefault(value.title, '--'); - cipher.notes = this.getValueOrDefault(value.notesPlain, '') + '\n'; + cipher.name = this.getValueOrDefault(this.getProp(value, 'title'), '--'); + cipher.notes = this.getValueOrDefault(this.getProp(value, 'notesPlain'), '') + '\n'; - if (!this.isNullOrWhitespace(value.number) && !this.isNullOrWhitespace(value['expiry date'])) { + if (!this.isNullOrWhitespace(this.getProp(value, 'number')) && + !this.isNullOrWhitespace(this.getProp(value, 'expiry date'))) { cipher.type = CipherType.Card; cipher.card = new CardView(); } @@ -37,30 +38,31 @@ export class OnePasswordWinCsvImporter extends BaseImporter implements Importer continue; } + const lowerProp = property.toLowerCase(); if (cipher.type === CipherType.Login) { - if (this.isNullOrWhitespace(cipher.login.password) && property === 'password') { + if (this.isNullOrWhitespace(cipher.login.password) && lowerProp === 'password') { cipher.login.password = value[property]; continue; - } else if (this.isNullOrWhitespace(cipher.login.username) && property === 'username') { + } else if (this.isNullOrWhitespace(cipher.login.username) && lowerProp === 'username') { cipher.login.username = value[property]; continue; - } else if ((cipher.login.uris == null || cipher.login.uri.length === 0) && property === 'urls') { + } else if ((cipher.login.uris == null || cipher.login.uri.length === 0) && lowerProp === 'urls') { const urls = value[property].split(this.newLineRegex); cipher.login.uris = this.makeUriArray(urls); continue; } } else if (cipher.type === CipherType.Card) { - if (this.isNullOrWhitespace(cipher.card.number) && property === 'number') { + if (this.isNullOrWhitespace(cipher.card.number) && lowerProp === 'number') { cipher.card.number = value[property]; - cipher.card.brand = this.getCardBrand(value.number); + cipher.card.brand = this.getCardBrand(this.getProp(value, 'number')); continue; - } else if (this.isNullOrWhitespace(cipher.card.code) && property === 'verification number') { + } else if (this.isNullOrWhitespace(cipher.card.code) && lowerProp === 'verification number') { cipher.card.code = value[property]; continue; - } else if (this.isNullOrWhitespace(cipher.card.cardholderName) && property === 'cardholder name') { + } else if (this.isNullOrWhitespace(cipher.card.cardholderName) && lowerProp === 'cardholder name') { cipher.card.cardholderName = value[property]; continue; - } else if (this.isNullOrWhitespace(cipher.card.expiration) && property === 'expiry date' && + } else if (this.isNullOrWhitespace(cipher.card.expiration) && lowerProp === 'expiry date' && value[property].length === 6) { cipher.card.expMonth = (value[property] as string).substr(4, 2); if (cipher.card.expMonth[0] === '0') { @@ -68,14 +70,15 @@ export class OnePasswordWinCsvImporter extends BaseImporter implements Importer } cipher.card.expYear = (value[property] as string).substr(0, 4); continue; - } else if (property === 'type') { + } else if (lowerProp === 'type') { // Skip since brand was determined from number above continue; } } - if (IgnoredProperties.indexOf(property) === -1 && !property.startsWith('section:')) { - if (altUsername == null && property === 'email') { + if (IgnoredProperties.indexOf(lowerProp) === -1 && !lowerProp.startsWith('section:') && + !lowerProp.startsWith('section ')) { + if (altUsername == null && lowerProp === 'email') { altUsername = value[property]; } this.processKvp(cipher, property, value[property]); @@ -95,4 +98,8 @@ export class OnePasswordWinCsvImporter extends BaseImporter implements Importer result.success = true; return result; } + + private getProp(obj: any, name: string): any { + return obj[name] || obj[name.toUpperCase()]; + } } From 3ad546c39fe15db578cb794a018398be34f943ca Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Wed, 18 Mar 2020 10:07:57 -0500 Subject: [PATCH 0956/1626] Password Generator Sanitize Length (#89) * Initial commit for length sanitization * Updated sanitize function * Updated type instantiation Co-authored-by: Vincent Salucci --- .../passwordGeneration.service.ts | 1 + .../password-generator.component.ts | 55 +------ src/services/passwordGeneration.service.ts | 137 ++++++++++++++---- 3 files changed, 113 insertions(+), 80 deletions(-) diff --git a/src/abstractions/passwordGeneration.service.ts b/src/abstractions/passwordGeneration.service.ts index aa4d514b57..7978b7339e 100644 --- a/src/abstractions/passwordGeneration.service.ts +++ b/src/abstractions/passwordGeneration.service.ts @@ -12,4 +12,5 @@ export abstract class PasswordGenerationService { addHistory: (password: string) => Promise; clear: () => Promise; passwordStrength: (password: string, userInputs?: string[]) => zxcvbn.ZXCVBNResult; + normalizeOptions: (options: any, enforcedPolicyOptions: PasswordGeneratorPolicyOptions) => void; } diff --git a/src/angular/components/password-generator.component.ts b/src/angular/components/password-generator.component.ts index 51f70bc8bf..7b4fb07305 100644 --- a/src/angular/components/password-generator.component.ts +++ b/src/angular/components/password-generator.component.ts @@ -80,8 +80,7 @@ export class PasswordGeneratorComponent implements OnInit { } private normalizeOptions() { - this.options.minLowercase = 0; - this.options.minUppercase = 0; + // Application level normalize options depedent on class variables this.options.ambiguous = !this.avoidAmbiguous; if (!this.options.uppercase && !this.options.lowercase && !this.options.number && !this.options.special) { @@ -94,56 +93,6 @@ export class PasswordGeneratorComponent implements OnInit { } } - if (!this.options.length || this.options.length < 5) { - this.options.length = 5; - } else if (this.options.length > 128) { - this.options.length = 128; - } - - if (this.options.length < this.enforcedPolicyOptions.minLength) { - this.options.length = this.enforcedPolicyOptions.minLength; - } - - if (!this.options.minNumber) { - this.options.minNumber = 0; - } else if (this.options.minNumber > this.options.length) { - this.options.minNumber = this.options.length; - } else if (this.options.minNumber > 9) { - this.options.minNumber = 9; - } - - if (this.options.minNumber < this.enforcedPolicyOptions.numberCount) { - this.options.minNumber = this.enforcedPolicyOptions.numberCount; - } - - if (!this.options.minSpecial) { - this.options.minSpecial = 0; - } else if (this.options.minSpecial > this.options.length) { - this.options.minSpecial = this.options.length; - } else if (this.options.minSpecial > 9) { - this.options.minSpecial = 9; - } - - if (this.options.minSpecial < this.enforcedPolicyOptions.specialCount) { - this.options.minSpecial = this.enforcedPolicyOptions.specialCount; - } - - if (this.options.minSpecial + this.options.minNumber > this.options.length) { - this.options.minSpecial = this.options.length - this.options.minNumber; - } - - if (this.options.numWords == null || this.options.length < 3) { - this.options.numWords = 3; - } else if (this.options.numWords > 20) { - this.options.numWords = 20; - } - - if (this.options.numWords < this.enforcedPolicyOptions.minNumberWords) { - this.options.numWords = this.enforcedPolicyOptions.minNumberWords; - } - - if (this.options.wordSeparator != null && this.options.wordSeparator.length > 1) { - this.options.wordSeparator = this.options.wordSeparator[0]; - } + this.passwordGenerationService.normalizeOptions(this.options, this.enforcedPolicyOptions); } } diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 2af4c40ce5..4aff5a4223 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -57,33 +57,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr } // sanitize - if (o.uppercase && o.minUppercase <= 0) { - o.minUppercase = 1; - } else if (!o.uppercase) { - o.minUppercase = 0; - } - - if (o.lowercase && o.minLowercase <= 0) { - o.minLowercase = 1; - } else if (!o.lowercase) { - o.minLowercase = 0; - } - - if (o.number && o.minNumber <= 0) { - o.minNumber = 1; - } else if (!o.number) { - o.minNumber = 0; - } - - if (o.special && o.minSpecial <= 0) { - o.minSpecial = 1; - } else if (!o.special) { - o.minSpecial = 0; - } - - if (!o.length || o.length < 1) { - o.length = 10; - } + this.sanitizePasswordLength(o, true); const minLength: number = o.minUppercase + o.minLowercase + o.minNumber + o.minSpecial; if (o.length < minLength) { @@ -419,6 +393,65 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return result; } + normalizeOptions(options: any, enforcedPolicyOptions: PasswordGeneratorPolicyOptions) { + options.minLowercase = 0; + options.minUppercase = 0; + + if (!options.length || options.length < 5) { + options.length = 5; + } else if (options.length > 128) { + options.length = 128; + } + + if (options.length < enforcedPolicyOptions.minLength) { + options.length = enforcedPolicyOptions.minLength; + } + + if (!options.minNumber) { + options.minNumber = 0; + } else if (options.minNumber > options.length) { + options.minNumber = options.length; + } else if (options.minNumber > 9) { + options.minNumber = 9; + } + + if (options.minNumber < enforcedPolicyOptions.numberCount) { + options.minNumber = enforcedPolicyOptions.numberCount; + } + + if (!options.minSpecial) { + options.minSpecial = 0; + } else if (options.minSpecial > options.length) { + options.minSpecial = options.length; + } else if (options.minSpecial > 9) { + options.minSpecial = 9; + } + + if (options.minSpecial < enforcedPolicyOptions.specialCount) { + options.minSpecial = enforcedPolicyOptions.specialCount; + } + + if (options.minSpecial + options.minNumber > options.length) { + options.minSpecial = options.length - options.minNumber; + } + + if (options.numWords == null || options.length < 3) { + options.numWords = 3; + } else if (options.numWords > 20) { + options.numWords = 20; + } + + if (options.numWords < enforcedPolicyOptions.minNumberWords) { + options.numWords = enforcedPolicyOptions.minNumberWords; + } + + if (options.wordSeparator != null && options.wordSeparator.length > 1) { + options.wordSeparator = options.wordSeparator[0]; + } + + this.sanitizePasswordLength(options, false); + } + private capitalize(str: string) { return str.charAt(0).toUpperCase() + str.slice(1); } @@ -473,4 +506,54 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr [array[i], array[j]] = [array[j], array[i]]; } } + + private sanitizePasswordLength(options: any, forGeneration: boolean) { + let minUppercaseCalc = 0; + let minLowercaseCalc = 0; + let minNumberCalc: number = options.minNumber; + let minSpecialCalc: number = options.minSpecial; + + if (options.uppercase && options.minUppercase <= 0) { + minUppercaseCalc = 1; + } else if (!options.uppercase) { + minUppercaseCalc = 0; + } + + if (options.lowercase && options.minLowercase <= 0) { + minLowercaseCalc = 1; + } else if (!options.lowercase) { + minLowercaseCalc = 0; + } + + if (options.number && options.minNumber <= 0) { + minNumberCalc = 1; + } else if (!options.number) { + minNumberCalc = 0; + } + + if (options.special && options.minSpecial <= 0) { + minSpecialCalc = 1; + } else if (!options.special) { + minSpecialCalc = 0; + } + + // This should never happen but is a final safety net + if (!options.length || options.length < 1) { + options.length = 10; + } + + const minLength: number = minUppercaseCalc + minLowercaseCalc + minNumberCalc + minSpecialCalc; + // Normalize and Generation both require this modification + if (options.length < minLength) { + options.length = minLength; + } + + // Apply other changes if the options object passed in is for generation + if (forGeneration) { + options.minUppercase = minUppercaseCalc; + options.minLowercase = minLowercaseCalc; + options.minNumber = minNumberCalc; + options.minSpecial = minSpecialCalc; + } + } } From 31a257407be7f8f47624b0d021363aaf2cfda2d7 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 21 Mar 2020 00:19:40 -0400 Subject: [PATCH 0957/1626] skipNegotiation for websockets --- src/services/notifications.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index f9d327478f..c9f349e326 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -55,6 +55,8 @@ export class NotificationsService implements NotificationsServiceAbstraction { this.signalrConnection = new signalR.HubConnectionBuilder() .withUrl(this.url + '/hub', { accessTokenFactory: () => this.apiService.getActiveBearerToken(), + skipNegotiation: true, + transport: signalR.HttpTransportType.WebSockets, }) .withHubProtocol(new signalRMsgPack.MessagePackHubProtocol()) // .configureLogging(signalR.LogLevel.Trace) From 64c54cfb865cfd79437b6e873d1248dc970b43a5 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Fri, 27 Mar 2020 09:03:27 -0500 Subject: [PATCH 0958/1626] [Auto-Logout] Refactor LockService and Update Dependencies (#91) * initial commit for lockService name refactor * Reverted ConstantsService vault timeout key to legacy string value Co-authored-by: Vincent Salucci --- src/abstractions/index.ts | 2 +- src/abstractions/platformUtils.service.ts | 3 +++ ...ock.service.ts => vaultTimeout.service.ts} | 7 +++--- src/angular/components/lock.component.ts | 12 +++++----- src/angular/services/auth-guard.service.ts | 8 +++---- src/services/constants.service.ts | 6 +++-- src/services/crypto.service.ts | 8 +++---- src/services/index.ts | 2 +- src/services/notifications.service.ts | 6 ++--- src/services/system.service.ts | 6 ++--- ...ock.service.ts => vaultTimeout.service.ts} | 23 ++++++++++++------- 11 files changed, 48 insertions(+), 35 deletions(-) rename src/abstractions/{lock.service.ts => vaultTimeout.service.ts} (55%) rename src/services/{lock.service.ts => vaultTimeout.service.ts} (81%) diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index c9d10b5e58..cec535ffab 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -8,7 +8,6 @@ export { CryptoService } from './crypto.service'; export { EnvironmentService } from './environment.service'; export { FolderService } from './folder.service'; export { I18nService } from './i18n.service'; -export { LockService } from './lock.service'; export { LogService } from './log.service'; export { MessagingService } from './messaging.service'; export { PasswordGenerationService } from './passwordGeneration.service'; @@ -21,3 +20,4 @@ export { SyncService } from './sync.service'; export { TokenService } from './token.service'; export { TotpService } from './totp.service'; export { UserService } from './user.service'; +export { VaultTimeoutService } from './vaultTimeout.service'; diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index 188bd80090..070abf0027 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -14,6 +14,9 @@ export abstract class PlatformUtilsService { isMacAppStore: () => boolean; analyticsId: () => string; isViewOpen: () => Promise; + /** + * @deprecated This only ever returns null. Pull from your platform's storage using ConstantsService.vaultTimeoutKey + */ lockTimeout: () => number; launchUri: (uri: string, options?: any) => void; saveFile: (win: Window, blobData: any, blobOptions: any, fileName: string) => void; diff --git a/src/abstractions/lock.service.ts b/src/abstractions/vaultTimeout.service.ts similarity index 55% rename from src/abstractions/lock.service.ts rename to src/abstractions/vaultTimeout.service.ts index f43938bb46..6f1fe5e6fc 100644 --- a/src/abstractions/lock.service.ts +++ b/src/abstractions/vaultTimeout.service.ts @@ -1,11 +1,12 @@ import { CipherString } from '../models/domain/cipherString'; -export abstract class LockService { +export abstract class VaultTimeoutService { pinProtectedKey: CipherString; isLocked: () => Promise; - checkLock: () => Promise; + checkVaultTimeout: () => Promise; lock: (allowSoftLock?: boolean) => Promise; - setLockOption: (lockOption: number) => Promise; + logout: () => Promise; + setVaultTimeoutOptions: (vaultTimeout: number, vaultTimeoutAction: string) => Promise; isPinLockSet: () => Promise<[boolean, boolean]>; clear: () => Promise; } diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 1258b545d5..107ab47191 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -4,12 +4,12 @@ import { Router } from '@angular/router'; import { CryptoService } from '../../abstractions/crypto.service'; import { EnvironmentService } from '../../abstractions/environment.service'; import { I18nService } from '../../abstractions/i18n.service'; -import { LockService } from '../../abstractions/lock.service'; import { MessagingService } from '../../abstractions/messaging.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { StateService } from '../../abstractions/state.service'; import { StorageService } from '../../abstractions/storage.service'; import { UserService } from '../../abstractions/user.service'; +import { VaultTimeoutService } from '../../abstractions/vaultTimeout.service'; import { ConstantsService } from '../../services/constants.service'; @@ -35,12 +35,12 @@ export class LockComponent implements OnInit { constructor(protected router: Router, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected messagingService: MessagingService, protected userService: UserService, protected cryptoService: CryptoService, - protected storageService: StorageService, protected lockService: LockService, + protected storageService: StorageService, protected vaultTimeoutService: VaultTimeoutService, protected environmentService: EnvironmentService, protected stateService: StateService) { } async ngOnInit() { - this.pinSet = await this.lockService.isPinLockSet(); - this.pinLock = (this.pinSet[0] && this.lockService.pinProtectedKey != null) || this.pinSet[1]; + this.pinSet = await this.vaultTimeoutService.isPinLockSet(); + this.pinLock = (this.pinSet[0] && this.vaultTimeoutService.pinProtectedKey != null) || this.pinSet[1]; this.email = await this.userService.getEmail(); let vaultUrl = this.environmentService.getWebVaultUrl(); if (vaultUrl == null) { @@ -69,7 +69,7 @@ export class LockComponent implements OnInit { try { if (this.pinSet[0]) { const key = await this.cryptoService.makeKeyFromPin(this.pin, this.email, kdf, kdfIterations, - this.lockService.pinProtectedKey); + this.vaultTimeoutService.pinProtectedKey); const encKey = await this.cryptoService.getEncKey(key); const protectedPin = await this.storageService.get(ConstantsService.protectedPin); const decPin = await this.cryptoService.decryptToUtf8(new CipherString(protectedPin), encKey); @@ -106,7 +106,7 @@ export class LockComponent implements OnInit { const encKey = await this.cryptoService.getEncKey(key); const decPin = await this.cryptoService.decryptToUtf8(new CipherString(protectedPin), encKey); const pinKey = await this.cryptoService.makePinKey(decPin, this.email, kdf, kdfIterations); - this.lockService.pinProtectedKey = await this.cryptoService.encrypt(key.key, pinKey); + this.vaultTimeoutService.pinProtectedKey = await this.cryptoService.encrypt(key.key, pinKey); } this.setKeyAndContinue(key); } else { diff --git a/src/angular/services/auth-guard.service.ts b/src/angular/services/auth-guard.service.ts index d8efd78b77..0401c37875 100644 --- a/src/angular/services/auth-guard.service.ts +++ b/src/angular/services/auth-guard.service.ts @@ -6,14 +6,14 @@ import { RouterStateSnapshot, } from '@angular/router'; -import { LockService } from '../../abstractions/lock.service'; import { MessagingService } from '../../abstractions/messaging.service'; import { UserService } from '../../abstractions/user.service'; +import { VaultTimeoutService } from '../../abstractions/vaultTimeout.service'; @Injectable() export class AuthGuardService implements CanActivate { - constructor(private lockService: LockService, private userService: UserService, private router: Router, - private messagingService: MessagingService) { } + constructor(private vaultTimeoutService: VaultTimeoutService, private userService: UserService, + private router: Router, private messagingService: MessagingService) { } async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) { const isAuthed = await this.userService.isAuthenticated(); @@ -22,7 +22,7 @@ export class AuthGuardService implements CanActivate { return false; } - const locked = await this.lockService.isLocked(); + const locked = await this.vaultTimeoutService.isLocked(); if (locked) { if (routerState != null) { this.messagingService.send('lockedUrl', { url: routerState.url }); diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index 648c85ff54..396ebffa3f 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -7,7 +7,8 @@ export class ConstantsService { static readonly disableFaviconKey: string = 'disableFavicon'; static readonly disableAutoTotpCopyKey: string = 'disableAutoTotpCopy'; static readonly enableAutoFillOnPageLoadKey: string = 'enableAutoFillOnPageLoad'; - static readonly lockOptionKey: string = 'lockOption'; + static readonly vaultTimeoutKey: string = 'lockOption'; + static readonly vaultTimeoutActionKey: string = 'vaultTimeoutAction'; static readonly lastActiveKey: string = 'lastActive'; static readonly neverDomainsKey: string = 'neverDomains'; static readonly installedVersionKey: string = 'installedVersion'; @@ -30,7 +31,8 @@ export class ConstantsService { readonly disableFaviconKey: string = ConstantsService.disableFaviconKey; readonly disableAutoTotpCopyKey: string = ConstantsService.disableAutoTotpCopyKey; readonly enableAutoFillOnPageLoadKey: string = ConstantsService.enableAutoFillOnPageLoadKey; - readonly lockOptionKey: string = ConstantsService.lockOptionKey; + readonly vaultTimeoutKey: string = ConstantsService.vaultTimeoutKey; + readonly vaultTimeoutActionKey: string = ConstantsService.vaultTimeoutActionKey; readonly lastActiveKey: string = ConstantsService.lastActiveKey; readonly neverDomainsKey: string = ConstantsService.neverDomainsKey; readonly installedVersionKey: string = ConstantsService.installedVersionKey; diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index b0a38f6d23..5fa86444e3 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -19,10 +19,10 @@ import { Utils } from '../misc/utils'; import { EEFLongWordList } from '../misc/wordlist'; const Keys = { - key: 'key', + key: 'key', // Master Key encOrgKeys: 'encOrgKeys', encPrivateKey: 'encPrivateKey', - encKey: 'encKey', + encKey: 'encKey', // Generated Symmetric Key keyHash: 'keyHash', }; @@ -41,7 +41,7 @@ export class CryptoService implements CryptoServiceAbstraction { async setKey(key: SymmetricCryptoKey): Promise { this.key = key; - const option = await this.storageService.get(ConstantsService.lockOptionKey); + const option = await this.storageService.get(ConstantsService.vaultTimeoutKey); if (option != null) { // if we have a lock option set, we do not store the key return; @@ -290,7 +290,7 @@ export class CryptoService implements CryptoServiceAbstraction { async toggleKey(): Promise { const key = await this.getKey(); - const option = await this.storageService.get(ConstantsService.lockOptionKey); + const option = await this.storageService.get(ConstantsService.vaultTimeoutKey); if (option != null || option === 0) { // if we have a lock option set, clear the key await this.clearKey(); diff --git a/src/services/index.ts b/src/services/index.ts index bff56b273c..dc710996fc 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -10,7 +10,6 @@ export { CryptoService } from './crypto.service'; export { EnvironmentService } from './environment.service'; export { FolderService } from './folder.service'; export { I18nService } from './i18n.service'; -export { LockService } from './lock.service'; export { PasswordGenerationService } from './passwordGeneration.service'; export { SettingsService } from './settings.service'; export { StateService } from './state.service'; @@ -18,3 +17,4 @@ export { SyncService } from './sync.service'; export { TokenService } from './token.service'; export { TotpService } from './totp.service'; export { UserService } from './user.service'; +export { VaultTimeoutService } from './vaultTimeout.service'; diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index c9f349e326..5f5026ad6e 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -6,10 +6,10 @@ import { NotificationType } from '../enums/notificationType'; import { ApiService } from '../abstractions/api.service'; import { AppIdService } from '../abstractions/appId.service'; import { EnvironmentService } from '../abstractions/environment.service'; -import { LockService } from '../abstractions/lock.service'; import { NotificationsService as NotificationsServiceAbstraction } from '../abstractions/notifications.service'; import { SyncService } from '../abstractions/sync.service'; import { UserService } from '../abstractions/user.service'; +import { VaultTimeoutService } from '../abstractions/vaultTimeout.service'; import { NotificationResponse, @@ -27,7 +27,7 @@ export class NotificationsService implements NotificationsServiceAbstraction { constructor(private userService: UserService, private syncService: SyncService, private appIdService: AppIdService, private apiService: ApiService, - private lockService: LockService, private logoutCallback: () => Promise) { } + private vaultTimeoutService: VaultTimeoutService, private logoutCallback: () => Promise) { } async init(environmentService: EnvironmentService): Promise { this.inited = false; @@ -190,7 +190,7 @@ export class NotificationsService implements NotificationsServiceAbstraction { private async isAuthedAndUnlocked() { if (await this.userService.isAuthenticated()) { - const locked = await this.lockService.isLocked(); + const locked = await this.vaultTimeoutService.isLocked(); return !locked; } return false; diff --git a/src/services/system.service.ts b/src/services/system.service.ts index aa77cb3d82..1ec3a138d6 100644 --- a/src/services/system.service.ts +++ b/src/services/system.service.ts @@ -1,8 +1,8 @@ -import { LockService } from '../abstractions/lock.service'; import { MessagingService } from '../abstractions/messaging.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { StorageService } from '../abstractions/storage.service'; import { SystemService as SystemServiceAbstraction } from '../abstractions/system.service'; +import { VaultTimeoutService } from '../abstractions/vaultTimeout.service'; import { ConstantsService } from './constants.service'; @@ -13,13 +13,13 @@ export class SystemService implements SystemServiceAbstraction { private clearClipboardTimeout: any = null; private clearClipboardTimeoutFunction: () => Promise = null; - constructor(private storageService: StorageService, private lockService: LockService, + constructor(private storageService: StorageService, private vaultTimeoutService: VaultTimeoutService, private messagingService: MessagingService, private platformUtilsService: PlatformUtilsService, private reloadCallback: () => Promise = null) { } startProcessReload(): void { - if (this.lockService.pinProtectedKey != null || this.reloadInterval != null) { + if (this.vaultTimeoutService.pinProtectedKey != null || this.reloadInterval != null) { return; } this.cancelProcessReload(); diff --git a/src/services/lock.service.ts b/src/services/vaultTimeout.service.ts similarity index 81% rename from src/services/lock.service.ts rename to src/services/vaultTimeout.service.ts index b82c87661e..6131e4d734 100644 --- a/src/services/lock.service.ts +++ b/src/services/vaultTimeout.service.ts @@ -4,16 +4,16 @@ import { CipherService } from '../abstractions/cipher.service'; import { CollectionService } from '../abstractions/collection.service'; import { CryptoService } from '../abstractions/crypto.service'; import { FolderService } from '../abstractions/folder.service'; -import { LockService as LockServiceAbstraction } from '../abstractions/lock.service'; import { MessagingService } from '../abstractions/messaging.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { SearchService } from '../abstractions/search.service'; import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; +import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from '../abstractions/vaultTimeout.service'; import { CipherString } from '../models/domain/cipherString'; -export class LockService implements LockServiceAbstraction { +export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { pinProtectedKey: CipherString = null; private inited = false; @@ -32,8 +32,8 @@ export class LockService implements LockServiceAbstraction { this.inited = true; if (checkOnInterval) { - this.checkLock(); - setInterval(() => this.checkLock(), 10 * 1000); // check every 10 seconds + this.checkVaultTimeout(); + setInterval(() => this.checkVaultTimeout(), 10 * 1000); // check every 10 seconds } } @@ -42,12 +42,13 @@ export class LockService implements LockServiceAbstraction { return !hasKey; } - async checkLock(): Promise { + async checkVaultTimeout(): Promise { if (await this.platformUtilsService.isViewOpen()) { // Do not lock return; } + // "is logged out check" - similar to isLocked, below const authed = await this.userService.isAuthenticated(); if (!authed) { return; @@ -59,7 +60,7 @@ export class LockService implements LockServiceAbstraction { let lockOption = this.platformUtilsService.lockTimeout(); if (lockOption == null) { - lockOption = await this.storageService.get(ConstantsService.lockOptionKey); + lockOption = await this.storageService.get(ConstantsService.vaultTimeoutKey); } if (lockOption == null || lockOption < 0) { return; @@ -70,6 +71,7 @@ export class LockService implements LockServiceAbstraction { return; } + // TODO update with vault timeout name and pivot based on action saved const lockOptionSeconds = lockOption * 60; const diffSeconds = ((new Date()).getTime() - lastActive) / 1000; if (diffSeconds >= lockOptionSeconds) { @@ -101,8 +103,13 @@ export class LockService implements LockServiceAbstraction { } } - async setLockOption(lockOption: number): Promise { - await this.storageService.save(ConstantsService.lockOptionKey, lockOption); + async logout(): Promise { + // TODO Add logic for loggedOutCallback + } + + async setVaultTimeoutOptions(vaultTimeout: number, vaultTimeoutAction: string): Promise { + await this.storageService.save(ConstantsService.vaultTimeoutKey, vaultTimeout); + // TODO Add logic for vaultTimeoutAction await this.cryptoService.toggleKey(); } From 28e3fff739e64c2dd80d3d98717e2921895d16df Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Sun, 29 Mar 2020 10:38:16 -0500 Subject: [PATCH 0959/1626] [Auto-Logout] Implement logout functionality in VaultTimeoutService (#92) * Initial commit for logic changes in VaultTimeoutService * Fixed lint error * Updated logOut spelling - as an action its two words * Hitting save to make sure all my changes are included * Made requested changes Co-authored-by: Vincent Salucci --- src/abstractions/vaultTimeout.service.ts | 2 +- src/services/vaultTimeout.service.ts | 36 ++++++++++++++---------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/abstractions/vaultTimeout.service.ts b/src/abstractions/vaultTimeout.service.ts index 6f1fe5e6fc..4c0ad3d0d2 100644 --- a/src/abstractions/vaultTimeout.service.ts +++ b/src/abstractions/vaultTimeout.service.ts @@ -5,7 +5,7 @@ export abstract class VaultTimeoutService { isLocked: () => Promise; checkVaultTimeout: () => Promise; lock: (allowSoftLock?: boolean) => Promise; - logout: () => Promise; + logOut: () => Promise; setVaultTimeoutOptions: (vaultTimeout: number, vaultTimeoutAction: string) => Promise; isPinLockSet: () => Promise<[boolean, boolean]>; clear: () => Promise; diff --git a/src/services/vaultTimeout.service.ts b/src/services/vaultTimeout.service.ts index 6131e4d734..d6304df0df 100644 --- a/src/services/vaultTimeout.service.ts +++ b/src/services/vaultTimeout.service.ts @@ -22,7 +22,8 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { private collectionService: CollectionService, private cryptoService: CryptoService, private platformUtilsService: PlatformUtilsService, private storageService: StorageService, private messagingService: MessagingService, private searchService: SearchService, - private userService: UserService, private lockedCallback: () => Promise = null) { + private userService: UserService, private lockedCallback: () => Promise = null, + private loggedOutCallback: () => Promise = null) { } init(checkOnInterval: boolean) { @@ -37,6 +38,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { } } + // Keys aren't stored for a device that is locked or logged out. async isLocked(): Promise { const hasKey = await this.cryptoService.hasKey(); return !hasKey; @@ -58,11 +60,13 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { return; } - let lockOption = this.platformUtilsService.lockTimeout(); - if (lockOption == null) { - lockOption = await this.storageService.get(ConstantsService.vaultTimeoutKey); + // This has the potential to be removed. Evaluate after all platforms complete with auto-logout + let vaultTimeout = this.platformUtilsService.lockTimeout(); + if (vaultTimeout == null) { + vaultTimeout = await this.storageService.get(ConstantsService.vaultTimeoutKey); } - if (lockOption == null || lockOption < 0) { + + if (vaultTimeout == null || vaultTimeout < 0) { return; } @@ -71,12 +75,12 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { return; } - // TODO update with vault timeout name and pivot based on action saved - const lockOptionSeconds = lockOption * 60; + const vaultTimeoutSeconds = vaultTimeout * 60; const diffSeconds = ((new Date()).getTime() - lastActive) / 1000; - if (diffSeconds >= lockOptionSeconds) { - // need to lock now - await this.lock(true); + if (diffSeconds >= vaultTimeoutSeconds) { + // Pivot based on the saved vault timeout action + const timeoutAction = await this.storageService.get(ConstantsService.vaultTimeoutActionKey); + timeoutAction === 'lock' ? await this.lock(true) : await this.logOut(); } } @@ -103,13 +107,15 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { } } - async logout(): Promise { - // TODO Add logic for loggedOutCallback + async logOut(): Promise { + if (this.loggedOutCallback != null) { + await this.loggedOutCallback(); + } } - async setVaultTimeoutOptions(vaultTimeout: number, vaultTimeoutAction: string): Promise { - await this.storageService.save(ConstantsService.vaultTimeoutKey, vaultTimeout); - // TODO Add logic for vaultTimeoutAction + async setVaultTimeoutOptions(timeout: number, action: string): Promise { + await this.storageService.save(ConstantsService.vaultTimeoutKey, timeout); + await this.storageService.save(ConstantsService.vaultTimeoutActionKey, action); await this.cryptoService.toggleKey(); } From 19668ab5f2a979cc52c3c3d18033b6899eecb8a5 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Fri, 3 Apr 2020 16:32:15 -0400 Subject: [PATCH 0960/1626] [Soft Delete] jslib updates for new API updates New API methods and cipher Deleted Date property, plus search expansion to toggle on deleted flag. --- src/abstractions/api.service.ts | 7 ++ src/abstractions/cipher.service.ts | 6 ++ src/abstractions/search.service.ts | 4 +- src/angular/components/ciphers.component.ts | 8 ++- src/angular/components/groupings.component.ts | 10 +++ src/angular/pipes/search-ciphers.pipe.ts | 9 ++- src/enums/eventType.ts | 2 + src/models/data/cipherData.ts | 2 + src/models/domain/cipher.ts | 3 + .../request/cipherBulkRestoreRequest.ts | 7 ++ src/models/response/cipherResponse.ts | 2 + src/models/view/cipherView.ts | 6 ++ src/services/api.service.ts | 24 +++++++ src/services/cipher.service.ts | 71 +++++++++++++++++++ src/services/search.service.ts | 21 ++++-- 15 files changed, 170 insertions(+), 12 deletions(-) create mode 100644 src/models/request/cipherBulkRestoreRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index ac2d913b39..1fc71b9c1c 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -5,6 +5,7 @@ import { EnvironmentUrls } from '../models/domain/environmentUrls'; import { BitPayInvoiceRequest } from '../models/request/bitPayInvoiceRequest'; import { CipherBulkDeleteRequest } from '../models/request/cipherBulkDeleteRequest'; import { CipherBulkMoveRequest } from '../models/request/cipherBulkMoveRequest'; +import { CipherBulkRestoreRequest } from '../models/request/cipherBulkRestoreRequest'; import { CipherBulkShareRequest } from '../models/request/cipherBulkShareRequest'; import { CipherCollectionsRequest } from '../models/request/cipherCollectionsRequest'; import { CipherCreateRequest } from '../models/request/cipherCreateRequest'; @@ -163,6 +164,12 @@ export abstract class ApiService { postPurgeCiphers: (request: PasswordVerificationRequest, organizationId?: string) => Promise; postImportCiphers: (request: ImportCiphersRequest) => Promise; postImportOrganizationCiphers: (organizationId: string, request: ImportOrganizationCiphersRequest) => Promise; + putDeleteCipher: (id: string) => Promise; + putDeleteCipherAdmin: (id: string) => Promise; + putDeleteManyCiphers: (request: CipherBulkDeleteRequest) => Promise; + putRestoreCipher: (id: string) => Promise; + putRestoreCipherAdmin: (id: string) => Promise; + putRestoreManyCiphers: (request: CipherBulkRestoreRequest) => Promise; postCipherAttachment: (id: string, data: FormData) => Promise; postCipherAttachmentAdmin: (id: string, data: FormData) => Promise; diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index 97c9c37abc..3c55b2cdbf 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -45,4 +45,10 @@ export abstract class CipherService { sortCiphersByLastUsed: (a: any, b: any) => number; sortCiphersByLastUsedThenName: (a: any, b: any) => number; getLocaleSortingFunction: () => (a: CipherView, b: CipherView) => number; + softDelete: (id: string | string[]) => Promise; + softDeleteWithServer: (id: string) => Promise; + softDeleteManyWithServer: (ids: string[]) => Promise; + restore: (id: string | string[]) => Promise; + restoreWithServer: (id: string) => Promise; + restoreManyWithServer: (ids: string[]) => Promise; } diff --git a/src/abstractions/search.service.ts b/src/abstractions/search.service.ts index 1ef8f92cd0..3e146e94bc 100644 --- a/src/abstractions/search.service.ts +++ b/src/abstractions/search.service.ts @@ -5,6 +5,6 @@ export abstract class SearchService { isSearchable: (query: string) => boolean; indexCiphers: () => Promise; searchCiphers: (query: string, filter?: (cipher: CipherView) => boolean, - ciphers?: CipherView[]) => Promise; - searchCiphersBasic: (ciphers: CipherView[], query: string) => CipherView[]; + ciphers?: CipherView[], deleted?: boolean) => Promise; + searchCiphersBasic: (ciphers: CipherView[], query: string, deleted?: boolean) => CipherView[]; } diff --git a/src/angular/components/ciphers.component.ts b/src/angular/components/ciphers.component.ts index b7220f514c..d00bd1ca73 100644 --- a/src/angular/components/ciphers.component.ts +++ b/src/angular/components/ciphers.component.ts @@ -21,6 +21,7 @@ export class CiphersComponent { searchText: string; searchPlaceholder: string = null; filter: (cipher: CipherView) => boolean = null; + deleted: boolean = false; protected searchPending = false; protected didScroll = false; @@ -32,7 +33,8 @@ export class CiphersComponent { constructor(protected searchService: SearchService) { } - async load(filter: (cipher: CipherView) => boolean = null) { + async load(filter: (cipher: CipherView) => boolean = null, deleted: boolean = false) { + this.deleted = deleted || false; await this.applyFilter(filter); this.loaded = true; } @@ -79,13 +81,13 @@ export class CiphersComponent { clearTimeout(this.searchTimeout); } if (timeout == null) { - this.ciphers = await this.searchService.searchCiphers(this.searchText, this.filter); + this.ciphers = await this.searchService.searchCiphers(this.searchText, this.filter, null, this.deleted); await this.resetPaging(); return; } this.searchPending = true; this.searchTimeout = setTimeout(async () => { - this.ciphers = await this.searchService.searchCiphers(this.searchText, this.filter); + this.ciphers = await this.searchService.searchCiphers(this.searchText, this.filter, null, this.deleted); await this.resetPaging(); this.searchPending = false; }, timeout); diff --git a/src/angular/components/groupings.component.ts b/src/angular/components/groupings.component.ts index 9983823dfa..022da692d5 100644 --- a/src/angular/components/groupings.component.ts +++ b/src/angular/components/groupings.component.ts @@ -22,9 +22,11 @@ export class GroupingsComponent { @Input() showFolders = true; @Input() showCollections = true; @Input() showFavorites = true; + @Input() showTrash = true; @Output() onAllClicked = new EventEmitter(); @Output() onFavoritesClicked = new EventEmitter(); + @Output() onTrashClicked = new EventEmitter(); @Output() onCipherTypeClicked = new EventEmitter(); @Output() onFolderClicked = new EventEmitter(); @Output() onAddFolder = new EventEmitter(); @@ -39,6 +41,7 @@ export class GroupingsComponent { cipherType = CipherType; selectedAll: boolean = false; selectedFavorites: boolean = false; + selectedTrash: boolean = false; selectedType: CipherType = null; selectedFolder: boolean = false; selectedFolderId: string = null; @@ -101,6 +104,12 @@ export class GroupingsComponent { this.onFavoritesClicked.emit(); } + selectTrash() { + this.clearSelections(); + this.selectedTrash = true; + this.onTrashClicked.emit(); + } + selectType(type: CipherType) { this.clearSelections(); this.selectedType = type; @@ -131,6 +140,7 @@ export class GroupingsComponent { clearSelections() { this.selectedAll = false; this.selectedFavorites = false; + this.selectedTrash = false; this.selectedType = null; this.selectedFolder = false; this.selectedFolderId = null; diff --git a/src/angular/pipes/search-ciphers.pipe.ts b/src/angular/pipes/search-ciphers.pipe.ts index e81371c4e8..1de1fbf267 100644 --- a/src/angular/pipes/search-ciphers.pipe.ts +++ b/src/angular/pipes/search-ciphers.pipe.ts @@ -19,17 +19,22 @@ export class SearchCiphersPipe implements PipeTransform { this.onlySearchName = platformUtilsService.getDevice() === DeviceType.EdgeExtension; } - transform(ciphers: CipherView[], searchText: string): CipherView[] { + transform(ciphers: CipherView[], searchText: string, deleted: boolean = false): CipherView[] { if (ciphers == null || ciphers.length === 0) { return []; } if (searchText == null || searchText.length < 2) { - return ciphers; + return ciphers.filter((c) => { + return deleted !== c.isDeleted; + }); } searchText = searchText.trim().toLowerCase(); return ciphers.filter((c) => { + if (deleted !== c.isDeleted) { + return false; + } if (c.name != null && c.name.toLowerCase().indexOf(searchText) > -1) { return true; } diff --git a/src/enums/eventType.ts b/src/enums/eventType.ts index eed006e915..40f626e751 100644 --- a/src/enums/eventType.ts +++ b/src/enums/eventType.ts @@ -23,6 +23,8 @@ export enum EventType { Cipher_ClientCopiedHiddenField = 1112, Cipher_ClientCopiedCardCode = 1113, Cipher_ClientAutofilled = 1114, + Cipher_SoftDeleted = 1115, + Cipher_Restored = 1116, Collection_Created = 1300, Collection_Updated = 1301, diff --git a/src/models/data/cipherData.ts b/src/models/data/cipherData.ts index 19478debad..9a45c73dae 100644 --- a/src/models/data/cipherData.ts +++ b/src/models/data/cipherData.ts @@ -31,6 +31,7 @@ export class CipherData { attachments?: AttachmentData[]; passwordHistory?: PasswordHistoryData[]; collectionIds?: string[]; + deletedDate: string; constructor(response?: CipherResponse, userId?: string, collectionIds?: string[]) { if (response == null) { @@ -49,6 +50,7 @@ export class CipherData { this.name = response.name; this.notes = response.notes; this.collectionIds = collectionIds != null ? collectionIds : response.collectionIds; + this.deletedDate = response.deletedDate; switch (this.type) { case CipherType.Login: diff --git a/src/models/domain/cipher.ts b/src/models/domain/cipher.ts index 85a961dd8f..647251f4ea 100644 --- a/src/models/domain/cipher.ts +++ b/src/models/domain/cipher.ts @@ -34,6 +34,7 @@ export class Cipher extends Domain { fields: Field[]; passwordHistory: Password[]; collectionIds: string[]; + deletedDate: Date; constructor(obj?: CipherData, alreadyEncrypted: boolean = false, localData: any = null) { super(); @@ -57,6 +58,7 @@ export class Cipher extends Domain { this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null; this.collectionIds = obj.collectionIds; this.localData = localData; + this.deletedDate = obj.deletedDate != null ? new Date(obj.deletedDate) : null; switch (this.type) { case CipherType.Login: @@ -172,6 +174,7 @@ export class Cipher extends Domain { c.revisionDate = this.revisionDate != null ? this.revisionDate.toISOString() : null; c.type = this.type; c.collectionIds = this.collectionIds; + c.deletedDate = this.deletedDate != null ? this.deletedDate.toISOString() : null; this.buildDataModel(this, c, { name: null, diff --git a/src/models/request/cipherBulkRestoreRequest.ts b/src/models/request/cipherBulkRestoreRequest.ts new file mode 100644 index 0000000000..546cc92450 --- /dev/null +++ b/src/models/request/cipherBulkRestoreRequest.ts @@ -0,0 +1,7 @@ +export class CipherBulkRestoreRequest { + ids: string[]; + + constructor(ids: string[]) { + this.ids = ids == null ? [] : ids; + } +} diff --git a/src/models/response/cipherResponse.ts b/src/models/response/cipherResponse.ts index 54584123d4..580e21fa7e 100644 --- a/src/models/response/cipherResponse.ts +++ b/src/models/response/cipherResponse.ts @@ -27,6 +27,7 @@ export class CipherResponse extends BaseResponse { attachments: AttachmentResponse[]; passwordHistory: PasswordHistoryResponse[]; collectionIds: string[]; + deletedDate: string; constructor(response: any) { super(response); @@ -41,6 +42,7 @@ export class CipherResponse extends BaseResponse { this.organizationUseTotp = this.getResponseProperty('OrganizationUseTotp'); this.revisionDate = this.getResponseProperty('RevisionDate'); this.collectionIds = this.getResponseProperty('CollectionIds'); + this.deletedDate = this.getResponseProperty('DeletedDate'); const login = this.getResponseProperty('Login'); if (login != null) { diff --git a/src/models/view/cipherView.ts b/src/models/view/cipherView.ts index 14feea8c42..e1c8d5fa66 100644 --- a/src/models/view/cipherView.ts +++ b/src/models/view/cipherView.ts @@ -31,6 +31,7 @@ export class CipherView implements View { passwordHistory: PasswordHistoryView[] = null; collectionIds: string[] = null; revisionDate: Date = null; + deletedDate: Date = null; constructor(c?: Cipher) { if (!c) { @@ -47,6 +48,7 @@ export class CipherView implements View { this.localData = c.localData; this.collectionIds = c.collectionIds; this.revisionDate = c.revisionDate; + this.deletedDate = c.deletedDate; } get subTitle(): string { @@ -97,4 +99,8 @@ export class CipherView implements View { } return this.login.passwordRevisionDate; } + + get isDeleted(): boolean { + return this.deletedDate != null; + } } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index b57ea4b824..70a91f36c3 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -434,6 +434,30 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/ciphers/import-organization?organizationId=' + organizationId, request, true, false); } + putDeleteCipher(id: string): Promise { + return this.send('PUT', '/ciphers/' + id + '/delete', null, true, false); + } + + putDeleteCipherAdmin(id: string): Promise { + return this.send('PUT', '/ciphers/' + id + '/delete-admin', null, true, false); + } + + putDeleteManyCiphers(request: CipherBulkDeleteRequest): Promise { + return this.send('PUT', '/ciphers/delete', request, true, false); + } + + putRestoreCipher(id: string): Promise { + return this.send('PUT', '/ciphers/' + id + '/restore', null, true, false); + } + + putRestoreCipherAdmin(id: string): Promise { + return this.send('PUT', '/ciphers/' + id + '/restore-admin', null, true, false); + } + + putRestoreManyCiphers(request: CipherBulkDeleteRequest): Promise { + return this.send('PUT', '/ciphers/restore', request, true, false); + } + // Attachments APIs async postCipherAttachment(id: string, data: FormData): Promise { diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 6e7296e09c..d2856d7449 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -19,6 +19,7 @@ import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; import { CipherBulkDeleteRequest } from '../models/request/cipherBulkDeleteRequest'; import { CipherBulkMoveRequest } from '../models/request/cipherBulkMoveRequest'; +import { CipherBulkRestoreRequest } from '../models/request/cipherBulkRestoreRequest'; import { CipherBulkShareRequest } from '../models/request/cipherBulkShareRequest'; import { CipherCollectionsRequest } from '../models/request/cipherCollectionsRequest'; import { CipherCreateRequest } from '../models/request/cipherCreateRequest'; @@ -790,6 +791,76 @@ export class CipherService implements CipherServiceAbstraction { }; } + async softDelete(id: string | string[]): Promise { + const userId = await this.userService.getUserId(); + const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( + Keys.ciphersPrefix + userId); + if (ciphers == null) { + return; + } + + const setDeletedDate = (cipherId: string) => { + if (ciphers[cipherId] == null) { + return; + } + ciphers[cipherId].deletedDate = new Date().toISOString(); + }; + + if (typeof id === 'string') { + setDeletedDate(id); + } else { + (id as string[]).forEach(setDeletedDate); + } + + await this.storageService.save(Keys.ciphersPrefix + userId, ciphers); + this.decryptedCipherCache = null; + } + + async softDeleteWithServer(id: string): Promise { + await this.apiService.putDeleteCipher(id); + await this.softDelete(id); + } + + async softDeleteManyWithServer(ids: string[]): Promise { + await this.apiService.putDeleteManyCiphers(new CipherBulkDeleteRequest(ids)); + await this.softDelete(ids); + } + + async restore(id: string | string[]): Promise { + const userId = await this.userService.getUserId(); + const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( + Keys.ciphersPrefix + userId); + if (ciphers == null) { + return; + } + + const clearDeletedDate = (cipherId: string) => { + if (ciphers[cipherId] == null) { + return; + } + ciphers[cipherId].deletedDate = null; + }; + + if (typeof id === 'string') { + clearDeletedDate(id); + } else { + (id as string[]).forEach(clearDeletedDate); + } + + await this.storageService.save(Keys.ciphersPrefix + userId, ciphers); + this.decryptedCipherCache = null; + } + + async restoreWithServer(id: string): Promise { + await this.apiService.putRestoreCipher(id); + await this.restore(id); + } + + async restoreManyWithServer(ids: string[]): Promise { + await this.apiService.putRestoreManyCiphers(new CipherBulkRestoreRequest(ids)); + await this.restore(ids); + } + // Helpers private async shareAttachmentWithServer(attachmentView: AttachmentView, cipherId: string, diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 1977924d5c..ff1f2bf994 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -71,7 +71,8 @@ export class SearchService implements SearchServiceAbstraction { console.timeEnd('search indexing'); } - async searchCiphers(query: string, filter: (cipher: CipherView) => boolean = null, ciphers: CipherView[] = null): + async searchCiphers(query: string, filter: (cipher: CipherView) => boolean = null, ciphers: CipherView[] = null, + deleted: boolean = false): Promise { const results: CipherView[] = []; if (query != null) { @@ -84,9 +85,16 @@ export class SearchService implements SearchServiceAbstraction { if (ciphers == null) { ciphers = await this.cipherService.getAllDecrypted(); } - if (filter != null) { - ciphers = ciphers.filter(filter); - } + + ciphers = ciphers.filter((c) => { + if (deleted !== c.isDeleted) { + return false; + } + if (filter != null) { + return filter(c); + } + return true; + }); if (!this.isSearchable(query)) { return ciphers; @@ -138,9 +146,12 @@ export class SearchService implements SearchServiceAbstraction { return results; } - searchCiphersBasic(ciphers: CipherView[], query: string) { + searchCiphersBasic(ciphers: CipherView[], query: string, deleted: boolean = false) { query = query.trim().toLowerCase(); return ciphers.filter((c) => { + if (deleted !== c.isDeleted) { + return false; + } if (c.name != null && c.name.toLowerCase().indexOf(query) > -1) { return true; } From 72e3893f8eee79f1e3678839aa194f1096c343ea Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Mon, 6 Apr 2020 11:06:32 -0500 Subject: [PATCH 0961/1626] [Auto-Logout] Update Token Service (#94) * Auto logout on restart * Updated setTokens function * Remove async deocrator from setTokens Co-authored-by: Vincent Salucci --- src/abstractions/token.service.ts | 1 + src/services/token.service.ts | 39 ++++++++++++++++++++++++++-- src/services/vaultTimeout.service.ts | 6 +++-- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/abstractions/token.service.ts b/src/abstractions/token.service.ts index dbc446a41d..f55bed38d2 100644 --- a/src/abstractions/token.service.ts +++ b/src/abstractions/token.service.ts @@ -7,6 +7,7 @@ export abstract class TokenService { getToken: () => Promise; setRefreshToken: (refreshToken: string) => Promise; getRefreshToken: () => Promise; + toggleTokens: () => Promise; setTwoFactorToken: (token: string, email: string) => Promise; getTwoFactorToken: (email: string) => Promise; clearTwoFactorToken: (email: string) => Promise; diff --git a/src/services/token.service.ts b/src/services/token.service.ts index 4036d342ed..709556f7b6 100644 --- a/src/services/token.service.ts +++ b/src/services/token.service.ts @@ -26,9 +26,15 @@ export class TokenService implements TokenServiceAbstraction { ]); } - setToken(token: string): Promise { + async setToken(token: string): Promise { this.token = token; this.decodedToken = null; + + if (await this.skipTokenStorage()) { + // if we have a vault timeout and the action is log out, don't store token + return; + } + return this.storageService.save(Keys.accessToken, token); } @@ -41,8 +47,14 @@ export class TokenService implements TokenServiceAbstraction { return this.token; } - setRefreshToken(refreshToken: string): Promise { + async setRefreshToken(refreshToken: string): Promise { this.refreshToken = refreshToken; + + if (await this.skipTokenStorage()) { + // if we have a vault timeout and the action is log out, don't store token + return; + } + return this.storageService.save(Keys.refreshToken, refreshToken); } @@ -55,6 +67,23 @@ export class TokenService implements TokenServiceAbstraction { return this.refreshToken; } + async toggleTokens(): Promise { + const token = await this.getToken(); + const refreshToken = await this.getRefreshToken(); + const timeout = await this.storageService.get(ConstantsService.vaultTimeoutKey); + const action = await this.storageService.get(ConstantsService.vaultTimeoutActionKey); + if ((timeout != null || timeout === 0) && action === 'logOut') { + // if we have a vault timeout and the action is log out, reset tokens + await this.clearToken(); + this.token = token; + this.refreshToken = refreshToken; + return; + } + + await this.setToken(token); + await this.setRefreshToken(refreshToken); + } + setTwoFactorToken(token: string, email: string): Promise { return this.storageService.save(Keys.twoFactorTokenPrefix + email, token); } @@ -183,4 +212,10 @@ export class TokenService implements TokenServiceAbstraction { return decoded.iss as string; } + + private async skipTokenStorage(): Promise { + const timeout = await this.storageService.get(ConstantsService.vaultTimeoutKey); + const action = await this.storageService.get(ConstantsService.vaultTimeoutActionKey); + return timeout != null && action === 'logOut'; + } } diff --git a/src/services/vaultTimeout.service.ts b/src/services/vaultTimeout.service.ts index d6304df0df..bc13667f9b 100644 --- a/src/services/vaultTimeout.service.ts +++ b/src/services/vaultTimeout.service.ts @@ -8,6 +8,7 @@ import { MessagingService } from '../abstractions/messaging.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { SearchService } from '../abstractions/search.service'; import { StorageService } from '../abstractions/storage.service'; +import { TokenService } from '../abstractions/token.service'; import { UserService } from '../abstractions/user.service'; import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from '../abstractions/vaultTimeout.service'; @@ -22,8 +23,8 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { private collectionService: CollectionService, private cryptoService: CryptoService, private platformUtilsService: PlatformUtilsService, private storageService: StorageService, private messagingService: MessagingService, private searchService: SearchService, - private userService: UserService, private lockedCallback: () => Promise = null, - private loggedOutCallback: () => Promise = null) { + private userService: UserService, private tokenService: TokenService, + private lockedCallback: () => Promise = null, private loggedOutCallback: () => Promise = null) { } init(checkOnInterval: boolean) { @@ -117,6 +118,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { await this.storageService.save(ConstantsService.vaultTimeoutKey, timeout); await this.storageService.save(ConstantsService.vaultTimeoutActionKey, action); await this.cryptoService.toggleKey(); + await this.tokenService.toggleTokens(); } async isPinLockSet(): Promise<[boolean, boolean]> { From 2a3e03c70d7b4c6a9cc353af60bbd7c8936367f3 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Mon, 6 Apr 2020 18:20:39 -0400 Subject: [PATCH 0962/1626] [Soft Delete] - Included deleted flag on reload --- src/angular/components/ciphers.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/angular/components/ciphers.component.ts b/src/angular/components/ciphers.component.ts index d00bd1ca73..7244ca6e21 100644 --- a/src/angular/components/ciphers.component.ts +++ b/src/angular/components/ciphers.component.ts @@ -55,10 +55,10 @@ export class CiphersComponent { this.didScroll = this.pagedCiphers.length > this.pageSize; } - async reload(filter: (cipher: CipherView) => boolean = null) { + async reload(filter: (cipher: CipherView) => boolean = null, deleted: boolean = false) { this.loaded = false; this.ciphers = []; - await this.load(filter); + await this.load(filter, deleted); } async refresh() { From 3a10c1ff3027832953094b2f7edddb2361119b09 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Wed, 8 Apr 2020 16:44:13 -0400 Subject: [PATCH 0963/1626] [Soft Delete] - cipher search rem deleted flag, filter array conditional --- src/abstractions/search.service.ts | 5 ++- src/angular/components/add-edit.component.ts | 44 ++++++++++++++++++-- src/angular/components/ciphers.component.ts | 7 ++-- src/angular/components/view.component.ts | 8 ++++ src/services/search.service.ts | 19 ++++----- 5 files changed, 63 insertions(+), 20 deletions(-) diff --git a/src/abstractions/search.service.ts b/src/abstractions/search.service.ts index 3e146e94bc..1cfc32b19c 100644 --- a/src/abstractions/search.service.ts +++ b/src/abstractions/search.service.ts @@ -4,7 +4,8 @@ export abstract class SearchService { clearIndex: () => void; isSearchable: (query: string) => boolean; indexCiphers: () => Promise; - searchCiphers: (query: string, filter?: (cipher: CipherView) => boolean, - ciphers?: CipherView[], deleted?: boolean) => Promise; + searchCiphers: (query: string, + filter?: ((cipher: CipherView) => boolean) | (Array<(cipher: CipherView) => boolean>), + ciphers?: CipherView[]) => Promise; searchCiphersBasic: (ciphers: CipherView[], query: string, deleted?: boolean) => CipherView[]; } diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index e0bbc59dc4..40d3261d63 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -50,6 +50,7 @@ export class AddEditComponent implements OnInit { @Input() organizationId: string = null; @Output() onSavedCipher = new EventEmitter(); @Output() onDeletedCipher = new EventEmitter(); + @Output() onRestoredCipher = new EventEmitter(); @Output() onCancelled = new EventEmitter(); @Output() onEditAttachments = new EventEmitter(); @Output() onShareCipher = new EventEmitter(); @@ -63,6 +64,7 @@ export class AddEditComponent implements OnInit { title: string; formPromise: Promise; deletePromise: Promise; + restorePromise: Promise; checkPasswordPromise: Promise; showPassword: boolean = false; showCardCode: boolean = false; @@ -221,6 +223,10 @@ export class AddEditComponent implements OnInit { } async submit(): Promise { + if (this.cipher.isDeleted) { + return this.restore(); + } + if (this.cipher.name == null || this.cipher.name === '') { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('nameRequired')); @@ -331,10 +337,35 @@ export class AddEditComponent implements OnInit { try { this.deletePromise = this.deleteCipher(); await this.deletePromise; - this.platformUtilsService.eventTrack('Deleted Cipher'); - this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedItem')); + this.platformUtilsService.eventTrack((this.cipher.isDeleted ? 'Permanently ' : '') + 'Deleted Cipher'); + this.platformUtilsService.showToast('success', null, + this.i18nService.t(this.cipher.isDeleted ? 'permanentlyDeletedItem' : 'deletedItem')); this.onDeletedCipher.emit(this.cipher); - this.messagingService.send('deletedCipher'); + this.messagingService.send(this.cipher.isDeleted ? 'permanentlyDeletedCipher' : 'deletedCipher'); + } catch { } + + return true; + } + + async restore(): Promise { + if (!this.cipher.isDeleted) { + return false; + } + + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('restoreItemConfirmation'), this.i18nService.t('restoreItem'), + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return false; + } + + try { + this.restorePromise = this.restoreCipher(); + await this.restorePromise; + this.platformUtilsService.eventTrack('Restored Cipher'); + this.platformUtilsService.showToast('success', null, this.i18nService.t('restoredItem')); + this.onRestoredCipher.emit(this.cipher); + this.messagingService.send('restoredCipher'); } catch { } return true; @@ -449,6 +480,11 @@ export class AddEditComponent implements OnInit { } protected deleteCipher() { - return this.cipherService.deleteWithServer(this.cipher.id); + return this.cipher.isDeleted ? this.cipherService.deleteWithServer(this.cipher.id) + : this.cipherService.softDeleteWithServer(this.cipher.id); + } + + protected restoreCipher() { + return this.cipherService.restoreWithServer(this.cipher.id); } } diff --git a/src/angular/components/ciphers.component.ts b/src/angular/components/ciphers.component.ts index 7244ca6e21..50beab2f54 100644 --- a/src/angular/components/ciphers.component.ts +++ b/src/angular/components/ciphers.component.ts @@ -64,7 +64,7 @@ export class CiphersComponent { async refresh() { try { this.refreshing = true; - await this.reload(this.filter); + await this.reload(this.filter, this.deleted); } finally { this.refreshing = false; } @@ -80,14 +80,15 @@ export class CiphersComponent { if (this.searchTimeout != null) { clearTimeout(this.searchTimeout); } + const deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted; if (timeout == null) { - this.ciphers = await this.searchService.searchCiphers(this.searchText, this.filter, null, this.deleted); + this.ciphers = await this.searchService.searchCiphers(this.searchText, [this.filter, deletedFilter], null); await this.resetPaging(); return; } this.searchPending = true; this.searchTimeout = setTimeout(async () => { - this.ciphers = await this.searchService.searchCiphers(this.searchText, this.filter, null, this.deleted); + this.ciphers = await this.searchService.searchCiphers(this.searchText, [this.filter, deletedFilter], null); await this.resetPaging(); this.searchPending = false; }, timeout); diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index a0d6076483..d325a5f895 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -34,6 +34,7 @@ export class ViewComponent implements OnDestroy, OnInit { @Input() cipherId: string; @Output() onEditCipher = new EventEmitter(); @Output() onCloneCipher = new EventEmitter(); + @Output() onRestoreCipher = new EventEmitter(); cipher: CipherView; showPassword: boolean; @@ -110,6 +111,13 @@ export class ViewComponent implements OnDestroy, OnInit { this.onCloneCipher.emit(this.cipher); } + restore() { + if (!this.cipher.isDeleted) { + return; + } + this.onRestoreCipher.emit(this.cipher); + } + togglePassword() { this.platformUtilsService.eventTrack('Toggled Password'); this.showPassword = !this.showPassword; diff --git a/src/services/search.service.ts b/src/services/search.service.ts index ff1f2bf994..4a994c411d 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -71,8 +71,9 @@ export class SearchService implements SearchServiceAbstraction { console.timeEnd('search indexing'); } - async searchCiphers(query: string, filter: (cipher: CipherView) => boolean = null, ciphers: CipherView[] = null, - deleted: boolean = false): + async searchCiphers(query: string, + filter: (((cipher: CipherView) => boolean) | (Array<(cipher: CipherView) => boolean>)) = null, + ciphers: CipherView[] = null): Promise { const results: CipherView[] = []; if (query != null) { @@ -86,15 +87,11 @@ export class SearchService implements SearchServiceAbstraction { ciphers = await this.cipherService.getAllDecrypted(); } - ciphers = ciphers.filter((c) => { - if (deleted !== c.isDeleted) { - return false; - } - if (filter != null) { - return filter(c); - } - return true; - }); + if (filter != null && Array.isArray(filter) && filter.length > 0) { + ciphers = ciphers.filter((c) => filter.every((f) => f == null || f(c))); + } else if (filter != null) { + ciphers = ciphers.filter(filter as (cipher: CipherView) => boolean); + } if (!this.isSearchable(query)) { return ciphers; From 6ea0ce5287b8bf9a120e807e5d44ff8cb060d8ff Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Fri, 10 Apr 2020 16:42:34 -0400 Subject: [PATCH 0964/1626] [Sot Delete] update to view and i18n for delete --- src/angular/components/add-edit.component.ts | 4 +- src/angular/components/view.component.ts | 55 ++++++++++++++++++-- src/services/cipher.service.ts | 6 +++ 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 40d3261d63..389f6d4908 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -328,8 +328,8 @@ export class AddEditComponent implements OnInit { async delete(): Promise { const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t('deleteItemConfirmation'), this.i18nService.t('deleteItem'), - this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + this.i18nService.t(this.cipher.isDeleted ? 'permanentlyDeleteItemConfirmation' : 'deleteItemConfirmation'), + this.i18nService.t('deleteItem'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); if (!confirmed) { return false; } diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index d325a5f895..7e9d97894e 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -27,6 +27,7 @@ import { CipherView } from '../../models/view/cipherView'; import { FieldView } from '../../models/view/fieldView'; import { LoginUriView } from '../../models/view/loginUriView'; import { BroadcasterService } from '../services/broadcaster.service'; +import { Cipher } from '../../models/domain'; const BroadcasterSubscriptionId = 'ViewComponent'; @@ -34,7 +35,8 @@ export class ViewComponent implements OnDestroy, OnInit { @Input() cipherId: string; @Output() onEditCipher = new EventEmitter(); @Output() onCloneCipher = new EventEmitter(); - @Output() onRestoreCipher = new EventEmitter(); + @Output() onRestoredCipher = new EventEmitter(); + @Output() onDeletedCipher = new EventEmitter(); cipher: CipherView; showPassword: boolean; @@ -111,11 +113,45 @@ export class ViewComponent implements OnDestroy, OnInit { this.onCloneCipher.emit(this.cipher); } - restore() { - if (!this.cipher.isDeleted) { - return; + async delete(): Promise { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t(this.cipher.isDeleted ? 'permanentlyDeleteItemConfirmation' : 'deleteItemConfirmation'), + this.i18nService.t('deleteItem'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return false; } - this.onRestoreCipher.emit(this.cipher); + + try { + await this.deleteCipher(); + this.platformUtilsService.eventTrack((this.cipher.isDeleted ? 'Permanently ' : '') + 'Deleted Cipher'); + this.platformUtilsService.showToast('success', null, + this.i18nService.t(this.cipher.isDeleted ? 'permanentlyDeletedItem' : 'deletedItem')); + this.onDeletedCipher.emit(this.cipher); + } catch { } + + return true; + } + + async restore(): Promise { + if (!this.cipher.isDeleted) { + return false; + } + + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('restoreItemConfirmation'), this.i18nService.t('restoreItem'), + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return false; + } + + try { + await this.restoreCipher(); + this.platformUtilsService.eventTrack('Restored Cipher'); + this.platformUtilsService.showToast('success', null, this.i18nService.t('restoredItem')); + this.onRestoredCipher.emit(this.cipher); + } catch { } + + return true; } togglePassword() { @@ -225,6 +261,15 @@ export class ViewComponent implements OnDestroy, OnInit { a.downloading = false; } + protected deleteCipher() { + return this.cipher.isDeleted ? this.cipherService.deleteWithServer(this.cipher.id) + : this.cipherService.softDeleteWithServer(this.cipher.id); + } + + protected restoreCipher() { + return this.cipherService.restoreWithServer(this.cipher.id); + } + private cleanUp() { this.totpCode = null; this.cipher = null; diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index d2856d7449..7b519a1a4f 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -310,6 +310,9 @@ export class CipherService implements CipherServiceAbstraction { const ciphers = await this.getAllDecrypted(); return ciphers.filter((cipher) => { + if (cipher.isDeleted) { + return false; + } if (folder && cipher.folderId === groupingId) { return true; } else if (!folder && cipher.collectionIds != null && cipher.collectionIds.indexOf(groupingId) > -1) { @@ -352,6 +355,9 @@ export class CipherService implements CipherServiceAbstraction { } return ciphers.filter((cipher) => { + if (cipher.deletedDate != null) { + return false; + } if (includeOtherTypes != null && includeOtherTypes.indexOf(cipher.type) > -1) { return true; } From e52df4f74350911fee3434c80df93c25dddf13ee Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Fri, 10 Apr 2020 16:42:34 -0400 Subject: [PATCH 0965/1626] [Sot Delete] update to view and i18n for delete --- src/angular/components/add-edit.component.ts | 4 +- src/angular/components/view.component.ts | 55 ++++++++++++++++++-- src/services/cipher.service.ts | 6 +++ 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 40d3261d63..389f6d4908 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -328,8 +328,8 @@ export class AddEditComponent implements OnInit { async delete(): Promise { const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t('deleteItemConfirmation'), this.i18nService.t('deleteItem'), - this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + this.i18nService.t(this.cipher.isDeleted ? 'permanentlyDeleteItemConfirmation' : 'deleteItemConfirmation'), + this.i18nService.t('deleteItem'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); if (!confirmed) { return false; } diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index d325a5f895..7e9d97894e 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -27,6 +27,7 @@ import { CipherView } from '../../models/view/cipherView'; import { FieldView } from '../../models/view/fieldView'; import { LoginUriView } from '../../models/view/loginUriView'; import { BroadcasterService } from '../services/broadcaster.service'; +import { Cipher } from '../../models/domain'; const BroadcasterSubscriptionId = 'ViewComponent'; @@ -34,7 +35,8 @@ export class ViewComponent implements OnDestroy, OnInit { @Input() cipherId: string; @Output() onEditCipher = new EventEmitter(); @Output() onCloneCipher = new EventEmitter(); - @Output() onRestoreCipher = new EventEmitter(); + @Output() onRestoredCipher = new EventEmitter(); + @Output() onDeletedCipher = new EventEmitter(); cipher: CipherView; showPassword: boolean; @@ -111,11 +113,45 @@ export class ViewComponent implements OnDestroy, OnInit { this.onCloneCipher.emit(this.cipher); } - restore() { - if (!this.cipher.isDeleted) { - return; + async delete(): Promise { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t(this.cipher.isDeleted ? 'permanentlyDeleteItemConfirmation' : 'deleteItemConfirmation'), + this.i18nService.t('deleteItem'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return false; } - this.onRestoreCipher.emit(this.cipher); + + try { + await this.deleteCipher(); + this.platformUtilsService.eventTrack((this.cipher.isDeleted ? 'Permanently ' : '') + 'Deleted Cipher'); + this.platformUtilsService.showToast('success', null, + this.i18nService.t(this.cipher.isDeleted ? 'permanentlyDeletedItem' : 'deletedItem')); + this.onDeletedCipher.emit(this.cipher); + } catch { } + + return true; + } + + async restore(): Promise { + if (!this.cipher.isDeleted) { + return false; + } + + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('restoreItemConfirmation'), this.i18nService.t('restoreItem'), + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return false; + } + + try { + await this.restoreCipher(); + this.platformUtilsService.eventTrack('Restored Cipher'); + this.platformUtilsService.showToast('success', null, this.i18nService.t('restoredItem')); + this.onRestoredCipher.emit(this.cipher); + } catch { } + + return true; } togglePassword() { @@ -225,6 +261,15 @@ export class ViewComponent implements OnDestroy, OnInit { a.downloading = false; } + protected deleteCipher() { + return this.cipher.isDeleted ? this.cipherService.deleteWithServer(this.cipher.id) + : this.cipherService.softDeleteWithServer(this.cipher.id); + } + + protected restoreCipher() { + return this.cipherService.restoreWithServer(this.cipher.id); + } + private cleanUp() { this.totpCode = null; this.cipher = null; diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index d2856d7449..7b519a1a4f 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -310,6 +310,9 @@ export class CipherService implements CipherServiceAbstraction { const ciphers = await this.getAllDecrypted(); return ciphers.filter((cipher) => { + if (cipher.isDeleted) { + return false; + } if (folder && cipher.folderId === groupingId) { return true; } else if (!folder && cipher.collectionIds != null && cipher.collectionIds.indexOf(groupingId) > -1) { @@ -352,6 +355,9 @@ export class CipherService implements CipherServiceAbstraction { } return ciphers.filter((cipher) => { + if (cipher.deletedDate != null) { + return false; + } if (includeOtherTypes != null && includeOtherTypes.indexOf(cipher.type) > -1) { return true; } From 222792940c4b4fd5c722c73ea4e95e837afbd1de Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Fri, 10 Apr 2020 16:59:39 -0400 Subject: [PATCH 0966/1626] [Soft Delete] - tslint err fix --- src/angular/components/view.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index 7e9d97894e..4737b21306 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -27,7 +27,6 @@ import { CipherView } from '../../models/view/cipherView'; import { FieldView } from '../../models/view/fieldView'; import { LoginUriView } from '../../models/view/loginUriView'; import { BroadcasterService } from '../services/broadcaster.service'; -import { Cipher } from '../../models/domain'; const BroadcasterSubscriptionId = 'ViewComponent'; @@ -35,8 +34,8 @@ export class ViewComponent implements OnDestroy, OnInit { @Input() cipherId: string; @Output() onEditCipher = new EventEmitter(); @Output() onCloneCipher = new EventEmitter(); - @Output() onRestoredCipher = new EventEmitter(); @Output() onDeletedCipher = new EventEmitter(); + @Output() onRestoredCipher = new EventEmitter(); cipher: CipherView; showPassword: boolean; From 8438cafbd08c1c9b1440e0c5385e15e8fb5ac524 Mon Sep 17 00:00:00 2001 From: mtgto Date: Wed, 15 Apr 2020 04:16:18 +0900 Subject: [PATCH 0967/1626] Update lunr type (#93) --- package-lock.json | 6 +++--- package.json | 2 +- src/services/search.service.ts | 20 ++++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8479e65465..95111a5b5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -304,9 +304,9 @@ } }, "@types/lunr": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@types/lunr/-/lunr-2.1.6.tgz", - "integrity": "sha512-Bz6fUhX1llTa7ygQJN3ttoVkkrpW7xxSEP7D7OYFO/FCBKqKqruRUZtJzTtYA0GkQX13lxU5u+8LuCviJlAXkQ==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/lunr/-/lunr-2.3.3.tgz", + "integrity": "sha512-09sXZZVsB3Ib41U0fC+O1O+4UOZT1bl/e+/QubPxpqDWHNEchvx/DEb1KJMOwq6K3MTNzZFoNSzVdR++o1DVnw==", "dev": true }, "@types/node": { diff --git a/package.json b/package.json index d10190eccf..5fec1f5667 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@types/inquirer": "^0.0.43", "@types/jasmine": "^3.3.12", "@types/lowdb": "^1.0.5", - "@types/lunr": "^2.1.6", + "@types/lunr": "^2.3.3", "@types/node": "^10.9.4", "@types/node-fetch": "^2.1.2", "@types/node-forge": "^0.7.5", diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 4a994c411d..a7149cafba 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -41,9 +41,9 @@ export class SearchService implements SearchServiceAbstraction { this.index = null; const builder = new lunr.Builder(); builder.ref('id'); - (builder as any).field('shortid', { boost: 100, extractor: (c: CipherView) => c.id.substr(0, 8) }); - (builder as any).field('name', { boost: 10 }); - (builder as any).field('subtitle', { + builder.field('shortid', { boost: 100, extractor: (c: CipherView) => c.id.substr(0, 8) }); + builder.field('name', { boost: 10 }); + builder.field('subtitle', { boost: 5, extractor: (c: CipherView) => { if (c.subTitle != null && c.type === CipherType.Card) { @@ -53,16 +53,16 @@ export class SearchService implements SearchServiceAbstraction { }, }); builder.field('notes'); - (builder as any).field('login.username', { + builder.field('login.username', { extractor: (c: CipherView) => c.type === CipherType.Login && c.login != null ? c.login.username : null, }); - (builder as any).field('login.uris', { boost: 2, extractor: (c: CipherView) => this.uriExtractor(c) }); - (builder as any).field('fields', { extractor: (c: CipherView) => this.fieldExtractor(c, false) }); - (builder as any).field('fields_joined', { extractor: (c: CipherView) => this.fieldExtractor(c, true) }); - (builder as any).field('attachments', { extractor: (c: CipherView) => this.attachmentExtractor(c, false) }); - (builder as any).field('attachments_joined', + builder.field('login.uris', { boost: 2, extractor: (c: CipherView) => this.uriExtractor(c) }); + builder.field('fields', { extractor: (c: CipherView) => this.fieldExtractor(c, false) }); + builder.field('fields_joined', { extractor: (c: CipherView) => this.fieldExtractor(c, true) }); + builder.field('attachments', { extractor: (c: CipherView) => this.attachmentExtractor(c, false) }); + builder.field('attachments_joined', { extractor: (c: CipherView) => this.attachmentExtractor(c, true) }); - (builder as any).field('organizationid', { extractor: (c: CipherView) => c.organizationId }); + builder.field('organizationid', { extractor: (c: CipherView) => c.organizationId }); const ciphers = await this.cipherService.getAllDecrypted(); ciphers.forEach((c) => builder.add(c)); this.index = builder.build(); From 2de8c5ed165f00e5d3a2b1dd92763176d6150782 Mon Sep 17 00:00:00 2001 From: Elias Papavasileiou Date: Tue, 14 Apr 2020 23:11:00 +0300 Subject: [PATCH 0968/1626] Add minimizeOnCopyToClipboardKey constant (#74) * Add minimizeOnCopyToClipboardKey constant * Move minimizeOnCopyToClipboardKey constant to electronConstants.ts * Add minimizeIfNeeded method to view component * Revert "Add minimizeIfNeeded method to view component" This reverts commit 7a5f2a3aa3d418ead5c03ce4c3f0ba00ffd34bb2. * Make storageService protected in window.main * Revert "Make storageService protected in window.main" This reverts commit 0431565c6596f7e4cb8c20b84fcbb56ce5772565. --- src/electron/electronConstants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/electron/electronConstants.ts b/src/electron/electronConstants.ts index b0086f8029..f444b0a1d8 100644 --- a/src/electron/electronConstants.ts +++ b/src/electron/electronConstants.ts @@ -4,4 +4,5 @@ export class ElectronConstants { static readonly enableTrayKey: string = 'enableTray'; static readonly enableStartToTrayKey: string = 'enableStartToTrayKey'; static readonly enableAlwaysOnTopKey: string = 'enableAlwaysOnTopKey'; + static readonly minimizeOnCopyToClipboardKey: string = 'minimizeOnCopyToClipboardKey'; } From 5e24e396ab9c1089dcb08f8e0e401ab610d3acc6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 29 Apr 2020 11:12:59 -0400 Subject: [PATCH 0969/1626] Add support for enpass sensitive fields (#98) --- src/importers/enpassJsonImporter.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/importers/enpassJsonImporter.ts b/src/importers/enpassJsonImporter.ts index e0a4634a99..82579a5a11 100644 --- a/src/importers/enpassJsonImporter.ts +++ b/src/importers/enpassJsonImporter.ts @@ -8,6 +8,7 @@ import { CipherView } from '../models/view/cipherView'; import { FolderView } from '../models/view/folderView'; import { CipherType } from '../enums/cipherType'; +import { FieldType } from '../enums/fieldType'; export class EnpassJsonImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { @@ -78,7 +79,7 @@ export class EnpassJsonImporter extends BaseImporter implements Importer { } else if (field.type === 'url') { urls.push(field.value); } else { - this.processKvp(cipher, field.label, field.value); + this.processKvp(cipher, field.label, field.value, field.sensitive === 1 ? FieldType.Hidden : null); } }); cipher.login.uris = this.makeUriArray(urls); @@ -101,10 +102,10 @@ export class EnpassJsonImporter extends BaseImporter implements Importer { cipher.card.code = field.value; } else if (field.type === 'ccExpiry' && this.isNullOrWhitespace(cipher.card.expYear)) { if (!this.setCardExpiration(cipher, field.value)) { - this.processKvp(cipher, field.label, field.value); + this.processKvp(cipher, field.label, field.value, field.sensitive === 1 ? FieldType.Hidden : null); } } else { - this.processKvp(cipher, field.label, field.value); + this.processKvp(cipher, field.label, field.value, field.sensitive === 1 ? FieldType.Hidden : null); } }); } @@ -114,7 +115,7 @@ export class EnpassJsonImporter extends BaseImporter implements Importer { if (this.isNullOrWhitespace(field.value) || field.type === 'section') { return; } - this.processKvp(cipher, field.label, field.value); + this.processKvp(cipher, field.label, field.value, field.sensitive === 1 ? FieldType.Hidden : null); }); } From 0092aac275e8efca66838a8c266eec1d455883aa Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 29 Apr 2020 11:47:34 -0400 Subject: [PATCH 0970/1626] fixes to url parsing (#99) * fixes to url parsing * make it a little more intelligent to pass tests --- src/misc/utils.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 6131fbd836..5c86ccd94c 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -281,14 +281,14 @@ export class Utils { return null; } - const hasProtocol = uriString.indexOf('://') > -1; - if (!hasProtocol && uriString.indexOf('.') > -1) { - uriString = 'http://' + uriString; - } else if (!hasProtocol) { - return null; + let url = Utils.getUrlObject(uriString); + if (url == null) { + const hasHttpProtocol = uriString.indexOf('http://') === 0 || uriString.indexOf('https://') === 0; + if (!hasHttpProtocol && uriString.indexOf('.') > -1) { + url = Utils.getUrlObject('http://' + uriString); + } } - - return Utils.getUrlObject(uriString); + return url; } private static getUrlObject(uriString: string): URL { @@ -298,6 +298,12 @@ export class Utils { } else if (typeof URL === 'function') { return new URL(uriString); } else if (window != null) { + const hasProtocol = uriString.indexOf('://') > -1; + if (!hasProtocol && uriString.indexOf('.') > -1) { + uriString = 'http://' + uriString; + } else if (!hasProtocol) { + return null; + } const anchor = window.document.createElement('a'); anchor.href = uriString; return anchor as any; From fb7335b927c252605eb08cfaf671ffd758ec8a87 Mon Sep 17 00:00:00 2001 From: Pasi Niemi Date: Fri, 8 May 2020 17:38:28 +0300 Subject: [PATCH 0971/1626] Enable alternative ways for settings passwords (#101) * Enable alternative ways for settings passwords: * the environment variable BW_PASSWORD * prefix the command line argument with "file:" and the password will read from the first line of that file * prefix the command line argument with "env:" and the password will be read from that environment variable * Appveyor fixes * Switch to using command options for password file and password env * Lowercase options --- src/cli/commands/login.command.ts | 23 ++++++++++++++++------- src/misc/nodeUtils.ts | 13 +++++++++++++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/cli/commands/login.command.ts b/src/cli/commands/login.command.ts index f08c0fa6c0..f900c0e0b1 100644 --- a/src/cli/commands/login.command.ts +++ b/src/cli/commands/login.command.ts @@ -14,6 +14,8 @@ import { Response } from '../models/response'; import { MessageResponse } from '../models/response/messageResponse'; +import { NodeUtils } from '../../misc/nodeUtils'; + export class LoginCommand { protected validatedParams: () => Promise; protected success: () => Promise; @@ -38,14 +40,21 @@ export class LoginCommand { return Response.badRequest('Email address is invalid.'); } - if ((password == null || password === '') && canInteract) { - const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ - type: 'password', - name: 'password', - message: 'Master password:', - }); - password = answer.password; + if (password == null || password === '') { + if (cmd.passwordfile) { + password = await NodeUtils.readFirstLine(cmd.passwordfile); + } else if (cmd.passwordenv && process.env[cmd.passwordenv]) { + password = process.env[cmd.passwordenv]; + } else if (canInteract) { + const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ + type: 'password', + name: 'password', + message: 'Master password:', + }); + password = answer.password; + } } + if (password == null || password === '') { return Response.badRequest('Master password is required.'); } diff --git a/src/misc/nodeUtils.ts b/src/misc/nodeUtils.ts index ee12c74459..f5e216c43f 100644 --- a/src/misc/nodeUtils.ts +++ b/src/misc/nodeUtils.ts @@ -1,5 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; +import * as readline from 'readline'; export class NodeUtils { static mkdirpSync(targetDir: string, mode = '700', relative = false, relativeDir: string = null) { @@ -13,4 +14,16 @@ export class NodeUtils { return dir; }, initialDir); } + static readFirstLine(fileName: string) { + return new Promise((resolve, reject) => { + const readStream = fs.createReadStream(fileName, {encoding: 'utf8'}); + const readInterface = readline.createInterface(readStream); + readInterface + .on('line', (line) => { + readStream.close(); + resolve(line); + }) + .on('error', (err) => reject(err)); + }); + } } From 2858724f4431038be190fc0b748efe287dd1bae6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 20 May 2020 15:30:58 -0400 Subject: [PATCH 0972/1626] default to text field type (#102) --- src/importers/enpassJsonImporter.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/importers/enpassJsonImporter.ts b/src/importers/enpassJsonImporter.ts index 82579a5a11..ca43577239 100644 --- a/src/importers/enpassJsonImporter.ts +++ b/src/importers/enpassJsonImporter.ts @@ -79,7 +79,8 @@ export class EnpassJsonImporter extends BaseImporter implements Importer { } else if (field.type === 'url') { urls.push(field.value); } else { - this.processKvp(cipher, field.label, field.value, field.sensitive === 1 ? FieldType.Hidden : null); + this.processKvp(cipher, field.label, field.value, + field.sensitive === 1 ? FieldType.Hidden : FieldType.Text); } }); cipher.login.uris = this.makeUriArray(urls); @@ -102,10 +103,12 @@ export class EnpassJsonImporter extends BaseImporter implements Importer { cipher.card.code = field.value; } else if (field.type === 'ccExpiry' && this.isNullOrWhitespace(cipher.card.expYear)) { if (!this.setCardExpiration(cipher, field.value)) { - this.processKvp(cipher, field.label, field.value, field.sensitive === 1 ? FieldType.Hidden : null); + this.processKvp(cipher, field.label, field.value, + field.sensitive === 1 ? FieldType.Hidden : FieldType.Text); } } else { - this.processKvp(cipher, field.label, field.value, field.sensitive === 1 ? FieldType.Hidden : null); + this.processKvp(cipher, field.label, field.value, + field.sensitive === 1 ? FieldType.Hidden : FieldType.Text); } }); } @@ -115,7 +118,8 @@ export class EnpassJsonImporter extends BaseImporter implements Importer { if (this.isNullOrWhitespace(field.value) || field.type === 'section') { return; } - this.processKvp(cipher, field.label, field.value, field.sensitive === 1 ? FieldType.Hidden : null); + this.processKvp(cipher, field.label, field.value, + field.sensitive === 1 ? FieldType.Hidden : FieldType.Text); }); } From ab506ffbdbd1a9bb2d6161aea69005cbc45fa4d9 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Thu, 21 May 2020 14:31:53 +0200 Subject: [PATCH 0973/1626] Resolve edit always being true for ciphers I noticed that ciphers will always have `edit = True` even if the sync response has `edit = false`. The root cause seems to be a boolean default to true. And since `false || true => true` it would always be set to true. --- src/models/response/cipherResponse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/response/cipherResponse.ts b/src/models/response/cipherResponse.ts index 580e21fa7e..d66e8f8447 100644 --- a/src/models/response/cipherResponse.ts +++ b/src/models/response/cipherResponse.ts @@ -38,7 +38,7 @@ export class CipherResponse extends BaseResponse { this.name = this.getResponseProperty('Name'); this.notes = this.getResponseProperty('Notes'); this.favorite = this.getResponseProperty('Favorite') || false; - this.edit = this.getResponseProperty('Edit') || true; + this.edit = !!this.getResponseProperty('Edit'); this.organizationUseTotp = this.getResponseProperty('OrganizationUseTotp'); this.revisionDate = this.getResponseProperty('RevisionDate'); this.collectionIds = this.getResponseProperty('CollectionIds'); From 1fa3eb49adda582bab594284eb3f39439ff9c666 Mon Sep 17 00:00:00 2001 From: hinton Date: Thu, 21 May 2020 15:49:56 +0200 Subject: [PATCH 0974/1626] Add support for collections with hide passwords --- src/models/data/cipherData.ts | 2 ++ src/models/domain/cipher.ts | 3 +++ src/models/domain/collection.ts | 4 +++- src/models/request/selectionReadOnlyRequest.ts | 4 +++- src/models/response/cipherResponse.ts | 8 +++++++- src/models/response/selectionReadOnlyResponse.ts | 2 ++ src/models/view/cipherView.ts | 2 ++ src/models/view/collectionView.ts | 2 ++ 8 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/models/data/cipherData.ts b/src/models/data/cipherData.ts index 9a45c73dae..fc87ae60d6 100644 --- a/src/models/data/cipherData.ts +++ b/src/models/data/cipherData.ts @@ -16,6 +16,7 @@ export class CipherData { folderId: string; userId: string; edit: boolean; + viewPassword: boolean; organizationUseTotp: boolean; favorite: boolean; revisionDate: string; @@ -43,6 +44,7 @@ export class CipherData { this.folderId = response.folderId; this.userId = userId; this.edit = response.edit; + this.viewPassword = response.viewPassword; this.organizationUseTotp = response.organizationUseTotp; this.favorite = response.favorite; this.revisionDate = response.revisionDate; diff --git a/src/models/domain/cipher.ts b/src/models/domain/cipher.ts index 647251f4ea..f0c37cf75c 100644 --- a/src/models/domain/cipher.ts +++ b/src/models/domain/cipher.ts @@ -24,6 +24,7 @@ export class Cipher extends Domain { favorite: boolean; organizationUseTotp: boolean; edit: boolean; + viewPassword: boolean; revisionDate: Date; localData: any; login: Login; @@ -55,6 +56,7 @@ export class Cipher extends Domain { this.favorite = obj.favorite; this.organizationUseTotp = obj.organizationUseTotp; this.edit = obj.edit; + this.viewPassword = obj.viewPassword; this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null; this.collectionIds = obj.collectionIds; this.localData = localData; @@ -169,6 +171,7 @@ export class Cipher extends Domain { c.folderId = this.folderId; c.userId = this.organizationId != null ? userId : null; c.edit = this.edit; + c.viewPassword = this.viewPassword; c.organizationUseTotp = this.organizationUseTotp; c.favorite = this.favorite; c.revisionDate = this.revisionDate != null ? this.revisionDate.toISOString() : null; diff --git a/src/models/domain/collection.ts b/src/models/domain/collection.ts index 59bbf24d07..6ed5f216f9 100644 --- a/src/models/domain/collection.ts +++ b/src/models/domain/collection.ts @@ -11,6 +11,7 @@ export class Collection extends Domain { name: CipherString; externalId: string; readOnly: boolean; + hidePasswords: boolean; constructor(obj?: CollectionData, alreadyEncrypted: boolean = false) { super(); @@ -24,7 +25,8 @@ export class Collection extends Domain { name: null, externalId: null, readOnly: null, - }, alreadyEncrypted, ['id', 'organizationId', 'externalId', 'readOnly']); + hidePasswords: null, + }, alreadyEncrypted, ['id', 'organizationId', 'externalId', 'readOnly', 'hidePasswords']); } decrypt(): Promise { diff --git a/src/models/request/selectionReadOnlyRequest.ts b/src/models/request/selectionReadOnlyRequest.ts index e947b0a364..d001edb2b0 100644 --- a/src/models/request/selectionReadOnlyRequest.ts +++ b/src/models/request/selectionReadOnlyRequest.ts @@ -1,9 +1,11 @@ export class SelectionReadOnlyRequest { id: string; readOnly: boolean; + hidePasswords: boolean; - constructor(id: string, readOnly: boolean) { + constructor(id: string, readOnly: boolean, hidePasswords: boolean) { this.id = id; this.readOnly = readOnly; + this.hidePasswords = hidePasswords; } } diff --git a/src/models/response/cipherResponse.ts b/src/models/response/cipherResponse.ts index 580e21fa7e..32555a65b8 100644 --- a/src/models/response/cipherResponse.ts +++ b/src/models/response/cipherResponse.ts @@ -22,6 +22,7 @@ export class CipherResponse extends BaseResponse { secureNote: SecureNoteApi; favorite: boolean; edit: boolean; + viewPassword: boolean; organizationUseTotp: boolean; revisionDate: string; attachments: AttachmentResponse[]; @@ -38,7 +39,12 @@ export class CipherResponse extends BaseResponse { this.name = this.getResponseProperty('Name'); this.notes = this.getResponseProperty('Notes'); this.favorite = this.getResponseProperty('Favorite') || false; - this.edit = this.getResponseProperty('Edit') || true; + this.edit = !!this.getResponseProperty('Edit'); + if (this.getResponseProperty('ViewPassword') == null) { + this.viewPassword = true; + } else { + this.viewPassword = this.getResponseProperty('ViewPassword'); + } this.organizationUseTotp = this.getResponseProperty('OrganizationUseTotp'); this.revisionDate = this.getResponseProperty('RevisionDate'); this.collectionIds = this.getResponseProperty('CollectionIds'); diff --git a/src/models/response/selectionReadOnlyResponse.ts b/src/models/response/selectionReadOnlyResponse.ts index 5bdfc6e417..ebcf524746 100644 --- a/src/models/response/selectionReadOnlyResponse.ts +++ b/src/models/response/selectionReadOnlyResponse.ts @@ -3,10 +3,12 @@ import { BaseResponse } from './baseResponse'; export class SelectionReadOnlyResponse extends BaseResponse { id: string; readOnly: boolean; + hidePasswords: boolean; constructor(response: any) { super(response); this.id = this.getResponseProperty('Id'); this.readOnly = this.getResponseProperty('ReadOnly'); + this.hidePasswords = this.getResponseProperty('HidePasswords'); } } diff --git a/src/models/view/cipherView.ts b/src/models/view/cipherView.ts index e1c8d5fa66..4cd0daf039 100644 --- a/src/models/view/cipherView.ts +++ b/src/models/view/cipherView.ts @@ -21,6 +21,7 @@ export class CipherView implements View { favorite = false; organizationUseTotp = false; edit = false; + viewPassword = true; localData: any; login = new LoginView(); identity = new IdentityView(); @@ -44,6 +45,7 @@ export class CipherView implements View { this.favorite = c.favorite; this.organizationUseTotp = c.organizationUseTotp; this.edit = c.edit; + this.viewPassword = c.viewPassword; this.type = c.type; this.localData = c.localData; this.collectionIds = c.collectionIds; diff --git a/src/models/view/collectionView.ts b/src/models/view/collectionView.ts index 622a9e4cf0..9c27c9fb58 100644 --- a/src/models/view/collectionView.ts +++ b/src/models/view/collectionView.ts @@ -11,6 +11,7 @@ export class CollectionView implements View, ITreeNodeObject { name: string = null; externalId: string = null; readOnly: boolean = null; + hidePasswords: boolean = null; constructor(c?: Collection | CollectionGroupDetailsResponse) { if (!c) { @@ -22,6 +23,7 @@ export class CollectionView implements View, ITreeNodeObject { this.externalId = c.externalId; if (c instanceof Collection) { this.readOnly = c.readOnly; + this.hidePasswords = c.hidePasswords; } } } From 1bbd8081057701bda9452121c59fafc3971b5f9f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 27 May 2020 15:21:53 -0400 Subject: [PATCH 0975/1626] getEnterprisePortalSignInToken api (#105) --- src/abstractions/api.service.ts | 1 + src/services/api.service.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 1fc71b9c1c..dec3062ae7 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -139,6 +139,7 @@ export abstract class ApiService { postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise; postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise; postAccountKdf: (request: KdfRequest) => Promise; + getEnterprisePortalSignInToken: () => Promise; getFolder: (id: string) => Promise; postFolder: (request: FolderRequest) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 70a91f36c3..e8c5c0d462 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -321,6 +321,11 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/accounts/kdf', request, true, false); } + async getEnterprisePortalSignInToken(): Promise { + const r = await this.send('GET', '/accounts/enterprise-portal-signin-token', null, true, true); + return r as string; + } + // Folder APIs async getFolder(id: string): Promise { From 212a2e3745e6e0e2b3057ed308c47daf6aeefbc8 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Thu, 28 May 2020 13:09:55 -0500 Subject: [PATCH 0976/1626] Bug fix: made lock default conditional check (#106) --- src/services/vaultTimeout.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/vaultTimeout.service.ts b/src/services/vaultTimeout.service.ts index bc13667f9b..2601dc5454 100644 --- a/src/services/vaultTimeout.service.ts +++ b/src/services/vaultTimeout.service.ts @@ -81,7 +81,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { if (diffSeconds >= vaultTimeoutSeconds) { // Pivot based on the saved vault timeout action const timeoutAction = await this.storageService.get(ConstantsService.vaultTimeoutActionKey); - timeoutAction === 'lock' ? await this.lock(true) : await this.logOut(); + timeoutAction === 'logOut' ? await this.logOut() : await this.lock(true); } } From eef9588646091d268ef4c94f3d6db1412d9a99ab Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 1 Jun 2020 14:31:42 -0400 Subject: [PATCH 0977/1626] check for empty string on malformed URL (#108) --- src/misc/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 5c86ccd94c..e0b7041356 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -157,7 +157,7 @@ export class Utils { static getHostname(uriString: string): string { const url = Utils.getUrl(uriString); try { - return url != null ? url.hostname : null; + return url != null && url.hostname !== '' ? url.hostname : null; } catch { return null; } @@ -166,7 +166,7 @@ export class Utils { static getHost(uriString: string): string { const url = Utils.getUrl(uriString); try { - return url != null ? url.host : null; + return url != null && url.host !== '' ? url.host : null; } catch { return null; } From 34402571e7615e9edcd688225ceb79085abbcc46 Mon Sep 17 00:00:00 2001 From: hinton Date: Wed, 3 Jun 2020 20:45:53 +0200 Subject: [PATCH 0978/1626] Add newField property to FieldView, used for allowing edits for new fields --- src/angular/components/add-edit.component.ts | 1 + src/models/view/fieldView.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 389f6d4908..87c7cd3f8c 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -296,6 +296,7 @@ export class AddEditComponent implements OnInit { const f = new FieldView(); f.type = this.addFieldType; + f.newField = true; this.cipher.fields.push(f); } diff --git a/src/models/view/fieldView.ts b/src/models/view/fieldView.ts index 20d695da15..e09d5957dd 100644 --- a/src/models/view/fieldView.ts +++ b/src/models/view/fieldView.ts @@ -8,6 +8,7 @@ export class FieldView implements View { name: string = null; value: string = null; type: FieldType = null; + newField: boolean = false; // Marks if the filed is new and haven't been saved constructor(f?: Field) { if (!f) { From 4d776ca0efc8f1ba712b6659c559f6de846ad124 Mon Sep 17 00:00:00 2001 From: hinton Date: Thu, 4 Jun 2020 22:15:45 +0200 Subject: [PATCH 0979/1626] Fix spelling mistakes --- src/models/view/fieldView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/view/fieldView.ts b/src/models/view/fieldView.ts index e09d5957dd..c839e1ecb6 100644 --- a/src/models/view/fieldView.ts +++ b/src/models/view/fieldView.ts @@ -8,7 +8,7 @@ export class FieldView implements View { name: string = null; value: string = null; type: FieldType = null; - newField: boolean = false; // Marks if the filed is new and haven't been saved + newField: boolean = false; // Marks if the field is new and hasn't been saved constructor(f?: Field) { if (!f) { From 6e52c2846d70afd0659f272aa3b1553fd26e1775 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 27 May 2020 15:21:53 -0400 Subject: [PATCH 0980/1626] getEnterprisePortalSignInToken api (#105) --- src/abstractions/api.service.ts | 1 + src/services/api.service.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 1fc71b9c1c..dec3062ae7 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -139,6 +139,7 @@ export abstract class ApiService { postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise; postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise; postAccountKdf: (request: KdfRequest) => Promise; + getEnterprisePortalSignInToken: () => Promise; getFolder: (id: string) => Promise; postFolder: (request: FolderRequest) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 70a91f36c3..e8c5c0d462 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -321,6 +321,11 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/accounts/kdf', request, true, false); } + async getEnterprisePortalSignInToken(): Promise { + const r = await this.send('GET', '/accounts/enterprise-portal-signin-token', null, true, true); + return r as string; + } + // Folder APIs async getFolder(id: string): Promise { From 17298cf188ea90d2fd79607aa27b575a26f5ce8b Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Thu, 28 May 2020 13:09:55 -0500 Subject: [PATCH 0981/1626] Bug fix: made lock default conditional check (#106) --- src/services/vaultTimeout.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/vaultTimeout.service.ts b/src/services/vaultTimeout.service.ts index bc13667f9b..2601dc5454 100644 --- a/src/services/vaultTimeout.service.ts +++ b/src/services/vaultTimeout.service.ts @@ -81,7 +81,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { if (diffSeconds >= vaultTimeoutSeconds) { // Pivot based on the saved vault timeout action const timeoutAction = await this.storageService.get(ConstantsService.vaultTimeoutActionKey); - timeoutAction === 'lock' ? await this.lock(true) : await this.logOut(); + timeoutAction === 'logOut' ? await this.logOut() : await this.lock(true); } } From d1e4eebebb679a19fa673195f3f33516247a9524 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 1 Jun 2020 14:31:42 -0400 Subject: [PATCH 0982/1626] check for empty string on malformed URL (#108) --- src/misc/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 5c86ccd94c..e0b7041356 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -157,7 +157,7 @@ export class Utils { static getHostname(uriString: string): string { const url = Utils.getUrl(uriString); try { - return url != null ? url.hostname : null; + return url != null && url.hostname !== '' ? url.hostname : null; } catch { return null; } @@ -166,7 +166,7 @@ export class Utils { static getHost(uriString: string): string { const url = Utils.getUrl(uriString); try { - return url != null ? url.host : null; + return url != null && url.host !== '' ? url.host : null; } catch { return null; } From dd147ce3382c6ad04e3b0c52439e2fccee9a2491 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Thu, 11 Jun 2020 15:04:40 -0500 Subject: [PATCH 0983/1626] [Enterprise] Updated Environment Settings (#112) * Initial commit of enterprise environment option * Reverting API/EnvironmentUrls changes --- src/abstractions/environment.service.ts | 1 + src/angular/components/environment.component.ts | 4 ++++ src/services/environment.service.ts | 6 ++++++ 3 files changed, 11 insertions(+) diff --git a/src/abstractions/environment.service.ts b/src/abstractions/environment.service.ts index 455447bbf6..d68d6a6fc4 100644 --- a/src/abstractions/environment.service.ts +++ b/src/abstractions/environment.service.ts @@ -6,6 +6,7 @@ export abstract class EnvironmentService { iconsUrl: string; notificationsUrl: string; eventsUrl: string; + enterpriseUrl: string; getWebVaultUrl: () => string; setUrlsFromStorage: () => Promise; diff --git a/src/angular/components/environment.component.ts b/src/angular/components/environment.component.ts index 70db95d2ec..3a65af58a0 100644 --- a/src/angular/components/environment.component.ts +++ b/src/angular/components/environment.component.ts @@ -17,6 +17,7 @@ export class EnvironmentComponent { notificationsUrl: string; baseUrl: string; showCustom = false; + enterpriseUrl: string; constructor(protected platformUtilsService: PlatformUtilsService, protected environmentService: EnvironmentService, protected i18nService: I18nService) { @@ -26,6 +27,7 @@ export class EnvironmentComponent { this.identityUrl = environmentService.identityUrl || ''; this.iconsUrl = environmentService.iconsUrl || ''; this.notificationsUrl = environmentService.notificationsUrl || ''; + this.enterpriseUrl = environmentService.enterpriseUrl || ''; } async submit() { @@ -36,6 +38,7 @@ export class EnvironmentComponent { webVault: this.webVaultUrl, icons: this.iconsUrl, notifications: this.notificationsUrl, + enterprise: this.enterpriseUrl, }); // re-set urls since service can change them, ex: prefixing https:// @@ -45,6 +48,7 @@ export class EnvironmentComponent { this.webVaultUrl = resUrls.webVault; this.iconsUrl = resUrls.icons; this.notificationsUrl = resUrls.notifications; + this.enterpriseUrl = resUrls.enterprise; this.platformUtilsService.eventTrack('Set Environment URLs'); this.platformUtilsService.showToast('success', null, this.i18nService.t('environmentSaved')); diff --git a/src/services/environment.service.ts b/src/services/environment.service.ts index a5bfea2679..daf7b5c6b1 100644 --- a/src/services/environment.service.ts +++ b/src/services/environment.service.ts @@ -15,6 +15,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { iconsUrl: string; notificationsUrl: string; eventsUrl: string; + enterpriseUrl: string; constructor(private apiService: ApiService, private storageService: StorageService, private notificationsService: NotificationsService) { } @@ -38,6 +39,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { notifications: null, events: null, webVault: null, + enterprise: null, }; const envUrls = new EnvironmentUrls(); @@ -54,6 +56,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { this.iconsUrl = urls.icons; this.notificationsUrl = urls.notifications; this.eventsUrl = envUrls.events = urls.events; + this.enterpriseUrl = urls.enterprise; this.apiService.setUrls(envUrls); } @@ -65,6 +68,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { urls.icons = this.formatUrl(urls.icons); urls.notifications = this.formatUrl(urls.notifications); urls.events = this.formatUrl(urls.events); + urls.enterprise = this.formatUrl(urls.enterprise); await this.storageService.save(ConstantsService.environmentUrlsKey, { base: urls.base, @@ -74,6 +78,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { icons: urls.icons, notifications: urls.notifications, events: urls.events, + enterprise: urls.enterprise, }); this.baseUrl = urls.base; @@ -83,6 +88,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { this.iconsUrl = urls.icons; this.notificationsUrl = urls.notifications; this.eventsUrl = urls.events; + this.enterpriseUrl = urls.enterprise; const envUrls = new EnvironmentUrls(); if (this.baseUrl) { From 81c76816b96f7f7545b9c0fc3da54bc41724cf0d Mon Sep 17 00:00:00 2001 From: Hinton Date: Fri, 12 Jun 2020 14:21:27 +0200 Subject: [PATCH 0984/1626] Ensure viewPassword has a default value --- src/models/domain/cipher.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/models/domain/cipher.ts b/src/models/domain/cipher.ts index f0c37cf75c..9b31235a77 100644 --- a/src/models/domain/cipher.ts +++ b/src/models/domain/cipher.ts @@ -56,7 +56,11 @@ export class Cipher extends Domain { this.favorite = obj.favorite; this.organizationUseTotp = obj.organizationUseTotp; this.edit = obj.edit; - this.viewPassword = obj.viewPassword; + if (obj.viewPassword != null) { + this.viewPassword = obj.viewPassword; + } else { + this.viewPassword = true; // Default for already synced Ciphers without viewPassword + } this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null; this.collectionIds = obj.collectionIds; this.localData = localData; From dea0233ee38b9cb4d25ca9ef8fc69b6af8a7068f Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Fri, 12 Jun 2020 19:29:52 -0400 Subject: [PATCH 0985/1626] Support tax collection info --- src/abstractions/api.service.ts | 7 ++++++ .../request/organizationCreateRequest.ts | 7 ++++++ .../organizationTaxInfoUpdateRequest.ts | 9 +++++++ src/models/request/taxInfoUpdateRequest.ts | 4 ++++ src/models/response/taxInfoResponse.ts | 24 +++++++++++++++++++ src/services/api.service.ts | 21 ++++++++++++++++ 6 files changed, 72 insertions(+) create mode 100644 src/models/request/organizationTaxInfoUpdateRequest.ts create mode 100644 src/models/request/taxInfoUpdateRequest.ts create mode 100644 src/models/response/taxInfoResponse.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 1fc71b9c1c..3411f06028 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -25,6 +25,7 @@ import { ImportOrganizationCiphersRequest } from '../models/request/importOrgani import { KdfRequest } from '../models/request/kdfRequest'; import { KeysRequest } from '../models/request/keysRequest'; import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest'; +import { OrganizationTaxInfoUpdateRequest } from '../models/request/organizationTaxInfoUpdateRequest'; import { OrganizationUpdateRequest } from '../models/request/organizationUpdateRequest'; import { OrganizationUpgradeRequest } from '../models/request/organizationUpgradeRequest'; import { OrganizationUserAcceptRequest } from '../models/request/organizationUserAcceptRequest'; @@ -42,6 +43,7 @@ import { RegisterRequest } from '../models/request/registerRequest'; import { SeatRequest } from '../models/request/seatRequest'; import { SelectionReadOnlyRequest } from '../models/request/selectionReadOnlyRequest'; import { StorageRequest } from '../models/request/storageRequest'; +import { TaxInfoUpdateRequest } from '../models/request/taxInfoUpdateRequest'; import { TokenRequest } from '../models/request/tokenRequest'; import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; import { TwoFactorProviderRequest } from '../models/request/twoFactorProviderRequest'; @@ -90,6 +92,7 @@ import { ProfileResponse } from '../models/response/profileResponse'; import { SelectionReadOnlyResponse } from '../models/response/selectionReadOnlyResponse'; import { SubscriptionResponse } from '../models/response/subscriptionResponse'; import { SyncResponse } from '../models/response/syncResponse'; +import { TaxInfoResponse } from '../models/response/taxInfoResponse'; import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; import { TwoFactorDuoResponse } from '../models/response/twoFactorDuoResponse'; import { TwoFactorEmailResponse } from '../models/response/twoFactorEmailResponse'; @@ -115,7 +118,9 @@ export abstract class ApiService { getProfile: () => Promise; getUserBilling: () => Promise; getUserSubscription: () => Promise; + getTaxInfo: () => Promise; putProfile: (request: UpdateProfileRequest) => Promise; + putTaxInfo: (request: TaxInfoUpdateRequest) => Promise; postPrelogin: (request: PreloginRequest) => Promise; postEmailToken: (request: EmailTokenRequest) => Promise; postEmail: (request: EmailRequest) => Promise; @@ -254,8 +259,10 @@ export abstract class ApiService { getOrganizationBilling: (id: string) => Promise; getOrganizationSubscription: (id: string) => Promise; getOrganizationLicense: (id: string, installationId: string) => Promise; + getOrganizationTaxInfo: (id: string) => Promise; postOrganization: (request: OrganizationCreateRequest) => Promise; putOrganization: (id: string, request: OrganizationUpdateRequest) => Promise; + putOrganizationTaxInfo: (id: string, request: OrganizationTaxInfoUpdateRequest) => Promise; postLeaveOrganization: (id: string) => Promise; postOrganizationLicense: (data: FormData) => Promise; postOrganizationLicenseUpdate: (id: string, data: FormData) => Promise; diff --git a/src/models/request/organizationCreateRequest.ts b/src/models/request/organizationCreateRequest.ts index c7f7da6822..8366b4cff3 100644 --- a/src/models/request/organizationCreateRequest.ts +++ b/src/models/request/organizationCreateRequest.ts @@ -13,4 +13,11 @@ export class OrganizationCreateRequest { additionalStorageGb: number; premiumAccessAddon: boolean; collectionName: string; + taxIdNumber: string; + billingAddressLine1: string; + billingAddressLine2: string; + billingAddressCity: string; + billingAddressState: string; + billingAddressPostalCode: string; + billingAddressCountry: string; } diff --git a/src/models/request/organizationTaxInfoUpdateRequest.ts b/src/models/request/organizationTaxInfoUpdateRequest.ts new file mode 100644 index 0000000000..73df4ad50c --- /dev/null +++ b/src/models/request/organizationTaxInfoUpdateRequest.ts @@ -0,0 +1,9 @@ +import { TaxInfoUpdateRequest } from './taxInfoUpdateRequest'; + +export class OrganizationTaxInfoUpdateRequest extends TaxInfoUpdateRequest { + taxId: string; + line1: string; + line2: string; + city: string; + state: string; +} diff --git a/src/models/request/taxInfoUpdateRequest.ts b/src/models/request/taxInfoUpdateRequest.ts new file mode 100644 index 0000000000..5485164e7e --- /dev/null +++ b/src/models/request/taxInfoUpdateRequest.ts @@ -0,0 +1,4 @@ +export class TaxInfoUpdateRequest { + country: string; + postalCode: string; +} diff --git a/src/models/response/taxInfoResponse.ts b/src/models/response/taxInfoResponse.ts new file mode 100644 index 0000000000..9759bbe332 --- /dev/null +++ b/src/models/response/taxInfoResponse.ts @@ -0,0 +1,24 @@ +import { BaseResponse } from './baseResponse'; + +export class TaxInfoResponse extends BaseResponse { + taxId: string; + taxIdType: string; + line1: string; + line2: string; + city: string; + state: string; + country: string; + postalCode: string; + + constructor(response: any) { + super(response); + this.taxId = this.getResponseProperty('TaxIdNumber'); + this.taxIdType = this.getResponseProperty('TaxIdType'); + this.line1 = this.getResponseProperty('Line1'); + this.line2 = this.getResponseProperty('Line2'); + this.city = this.getResponseProperty('City'); + this.state = this.getResponseProperty('State'); + this.postalCode = this.getResponseProperty('PostalCode'); + this.country = this.getResponseProperty('Country'); + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 70a91f36c3..9acf387819 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -29,6 +29,7 @@ import { ImportOrganizationCiphersRequest } from '../models/request/importOrgani import { KdfRequest } from '../models/request/kdfRequest'; import { KeysRequest } from '../models/request/keysRequest'; import { OrganizationCreateRequest } from '../models/request/organizationCreateRequest'; +import { OrganizationTaxInfoUpdateRequest } from '../models/request/organizationTaxInfoUpdateRequest'; import { OrganizationUpdateRequest } from '../models/request/organizationUpdateRequest'; import { OrganizationUpgradeRequest } from '../models/request/organizationUpgradeRequest'; import { OrganizationUserAcceptRequest } from '../models/request/organizationUserAcceptRequest'; @@ -46,6 +47,7 @@ import { RegisterRequest } from '../models/request/registerRequest'; import { SeatRequest } from '../models/request/seatRequest'; import { SelectionReadOnlyRequest } from '../models/request/selectionReadOnlyRequest'; import { StorageRequest } from '../models/request/storageRequest'; +import { TaxInfoUpdateRequest } from '../models/request/taxInfoUpdateRequest'; import { TokenRequest } from '../models/request/tokenRequest'; import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; import { TwoFactorProviderRequest } from '../models/request/twoFactorProviderRequest'; @@ -95,6 +97,7 @@ import { ProfileResponse } from '../models/response/profileResponse'; import { SelectionReadOnlyResponse } from '../models/response/selectionReadOnlyResponse'; import { SubscriptionResponse } from '../models/response/subscriptionResponse'; import { SyncResponse } from '../models/response/syncResponse'; +import { TaxInfoResponse } from '../models/response/taxInfoResponse'; import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; import { TwoFactorDuoResponse } from '../models/response/twoFactorDuoResponse'; import { TwoFactorEmailResponse } from '../models/response/twoFactorEmailResponse'; @@ -220,11 +223,20 @@ export class ApiService implements ApiServiceAbstraction { return new SubscriptionResponse(r); } + async getTaxInfo(): Promise { + const r = await this.send('GET', '/accounts/tax', null, true, true); + return new TaxInfoResponse(r); + } + async putProfile(request: UpdateProfileRequest): Promise { const r = await this.send('PUT', '/accounts/profile', request, true, true); return new ProfileResponse(r); } + putTaxInfo(request: TaxInfoUpdateRequest): Promise { + return this.send('PUT', '/accounts/tax', request, true, false); + } + async postPrelogin(request: PreloginRequest): Promise { const r = await this.send('POST', '/accounts/prelogin', request, false, true); return new PreloginResponse(r); @@ -812,6 +824,11 @@ export class ApiService implements ApiServiceAbstraction { null, true, true); } + async getOrganizationTaxInfo(id: string): Promise { + const r = await this.send('GET', '/organizations/' + id + '/tax', null, true, true); + return new TaxInfoResponse(r); + } + async postOrganization(request: OrganizationCreateRequest): Promise { const r = await this.send('POST', '/organizations', request, true, true); return new OrganizationResponse(r); @@ -822,6 +839,10 @@ export class ApiService implements ApiServiceAbstraction { return new OrganizationResponse(r); } + async putOrganizationTaxInfo(id: string, request: OrganizationTaxInfoUpdateRequest): Promise { + return this.send('PUT', '/organizations/' + id + '/tax', request, true, false); + } + postLeaveOrganization(id: string): Promise { return this.send('POST', '/organizations/' + id + '/leave', null, true, false); } From 01fd7050818c1ff9b48790c251fd9d91fcb605e1 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Wed, 17 Jun 2020 19:46:28 -0400 Subject: [PATCH 0986/1626] extend payment request to collect tax info --- src/models/request/paymentRequest.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/models/request/paymentRequest.ts b/src/models/request/paymentRequest.ts index 7ace99cd3a..e3d420eafb 100644 --- a/src/models/request/paymentRequest.ts +++ b/src/models/request/paymentRequest.ts @@ -1,6 +1,7 @@ import { PaymentMethodType } from '../../enums/paymentMethodType'; +import { OrganizationTaxInfoUpdateRequest } from '../request/organizationTaxInfoUpdateRequest'; -export class PaymentRequest { +export class PaymentRequest extends OrganizationTaxInfoUpdateRequest { paymentMethodType: PaymentMethodType; paymentToken: string; } From fb70ca8fe91dd3ad29d398e61c0863d61995b1fb Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Wed, 17 Jun 2020 20:07:59 -0400 Subject: [PATCH 0987/1626] tax info extends payment request --- src/models/request/paymentRequest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/request/paymentRequest.ts b/src/models/request/paymentRequest.ts index e3d420eafb..0fe9c872ea 100644 --- a/src/models/request/paymentRequest.ts +++ b/src/models/request/paymentRequest.ts @@ -1,5 +1,5 @@ -import { PaymentMethodType } from '../../enums/paymentMethodType'; import { OrganizationTaxInfoUpdateRequest } from '../request/organizationTaxInfoUpdateRequest'; +import { PaymentMethodType } from '../../enums/paymentMethodType'; export class PaymentRequest extends OrganizationTaxInfoUpdateRequest { paymentMethodType: PaymentMethodType; From 983f0cbfa7665265f6d449c9ced131b9e3426c6c Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Wed, 17 Jun 2020 20:09:47 -0400 Subject: [PATCH 0988/1626] re-order import --- src/models/request/paymentRequest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/request/paymentRequest.ts b/src/models/request/paymentRequest.ts index 0fe9c872ea..e3d420eafb 100644 --- a/src/models/request/paymentRequest.ts +++ b/src/models/request/paymentRequest.ts @@ -1,5 +1,5 @@ -import { OrganizationTaxInfoUpdateRequest } from '../request/organizationTaxInfoUpdateRequest'; import { PaymentMethodType } from '../../enums/paymentMethodType'; +import { OrganizationTaxInfoUpdateRequest } from '../request/organizationTaxInfoUpdateRequest'; export class PaymentRequest extends OrganizationTaxInfoUpdateRequest { paymentMethodType: PaymentMethodType; From 7f6d7424e77bccd68b63506b1205ef0dfa7c21bd Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 18 Jun 2020 11:58:29 -0400 Subject: [PATCH 0989/1626] Do not process hostname for regex logins (#117) --- src/models/view/loginUriView.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/models/view/loginUriView.ts b/src/models/view/loginUriView.ts index 4e171ad967..afa6e200fd 100644 --- a/src/models/view/loginUriView.ts +++ b/src/models/view/loginUriView.ts @@ -58,6 +58,9 @@ export class LoginUriView implements View { } get hostname(): string { + if (this.match === UriMatchType.RegularExpression) { + return null; + } if (this._hostname == null && this.uri != null) { this._hostname = Utils.getHostname(this.uri); if (this._hostname === '') { From c3389afd4f868001522f382b4e2622278cb78513 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Thu, 25 Jun 2020 15:44:19 -0400 Subject: [PATCH 0990/1626] Add referenceId to register component --- src/angular/components/register.component.ts | 3 ++- src/models/request/registerRequest.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index ba1002d817..b3ad95d377 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -22,6 +22,7 @@ export class RegisterComponent { showPassword: boolean = false; formPromise: Promise; masterPasswordScore: number; + referenceId: string; protected successRoute = 'login'; private masterPasswordStrengthTimeout: any; @@ -110,7 +111,7 @@ export class RegisterComponent { const hashedPassword = await this.cryptoService.hashPassword(this.masterPassword, key); const keys = await this.cryptoService.makeKeyPair(encKey[0]); const request = new RegisterRequest(this.email, this.name, hashedPassword, - this.hint, encKey[1].encryptedString, kdf, kdfIterations); + this.hint, encKey[1].encryptedString, kdf, kdfIterations, this.referenceId); request.keys = new KeysRequest(keys[0], keys[1].encryptedString); const orgInvite = await this.stateService.get('orgInvitation'); if (orgInvite != null && orgInvite.token != null && orgInvite.organizationUserId != null) { diff --git a/src/models/request/registerRequest.ts b/src/models/request/registerRequest.ts index d8caa4201a..f96544e80e 100644 --- a/src/models/request/registerRequest.ts +++ b/src/models/request/registerRequest.ts @@ -13,9 +13,10 @@ export class RegisterRequest { organizationUserId: string; kdf: KdfType; kdfIterations: number; + referenceId: string; constructor(email: string, name: string, masterPasswordHash: string, masterPasswordHint: string, key: string, - kdf: KdfType, kdfIterations: number) { + kdf: KdfType, kdfIterations: number, referenceId: string) { this.name = name; this.email = email; this.masterPasswordHash = masterPasswordHash; @@ -23,5 +24,6 @@ export class RegisterRequest { this.key = key; this.kdf = kdf; this.kdfIterations = kdfIterations; + this.referenceId = referenceId; } } From 785b681f61f81690de6df55159ab07ae710bcfad Mon Sep 17 00:00:00 2001 From: Anthony Garera Date: Fri, 3 Jul 2020 22:45:38 -0400 Subject: [PATCH 0991/1626] Added current date variable. This is in relation to PR #1272 in bitwarden/browser (#120) --- src/angular/components/add-edit.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 87c7cd3f8c..a0909cb2b8 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -78,6 +78,7 @@ export class AddEditComponent implements OnInit { addFieldTypeOptions: any[]; uriMatchOptions: any[]; ownershipOptions: any[] = []; + currentDate = new Date(); protected writeableCollections: CollectionView[]; private previousCipherId: string; From d308245237fe3ddc97ba4e6c1680febd47cb9b58 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Mon, 6 Jul 2020 17:21:41 -0500 Subject: [PATCH 0992/1626] Added new BusinessPortal boolean to all necessary objects (#121) --- src/models/data/organizationData.ts | 2 ++ src/models/domain/organization.ts | 2 ++ src/models/response/profileOrganizationResponse.ts | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/models/data/organizationData.ts b/src/models/data/organizationData.ts index 626691c004..4b0fc7694c 100644 --- a/src/models/data/organizationData.ts +++ b/src/models/data/organizationData.ts @@ -16,6 +16,7 @@ export class OrganizationData { useTotp: boolean; use2fa: boolean; useApi: boolean; + useBusinessPortal: boolean; selfHost: boolean; usersGetPremium: boolean; seats: number; @@ -35,6 +36,7 @@ export class OrganizationData { this.useTotp = response.useTotp; this.use2fa = response.use2fa; this.useApi = response.useApi; + this.useBusinessPortal = response.useBusinessPortal; this.selfHost = response.selfHost; this.usersGetPremium = response.usersGetPremium; this.seats = response.seats; diff --git a/src/models/domain/organization.ts b/src/models/domain/organization.ts index 914f8308df..21011fb095 100644 --- a/src/models/domain/organization.ts +++ b/src/models/domain/organization.ts @@ -16,6 +16,7 @@ export class Organization { useTotp: boolean; use2fa: boolean; useApi: boolean; + useBusinessPortal: boolean; selfHost: boolean; usersGetPremium: boolean; seats: number; @@ -39,6 +40,7 @@ export class Organization { this.useTotp = obj.useTotp; this.use2fa = obj.use2fa; this.useApi = obj.useApi; + this.useBusinessPortal = obj.useBusinessPortal; this.selfHost = obj.selfHost; this.usersGetPremium = obj.usersGetPremium; this.seats = obj.seats; diff --git a/src/models/response/profileOrganizationResponse.ts b/src/models/response/profileOrganizationResponse.ts index 6a76878c5a..f14ba9714b 100644 --- a/src/models/response/profileOrganizationResponse.ts +++ b/src/models/response/profileOrganizationResponse.ts @@ -13,6 +13,7 @@ export class ProfileOrganizationResponse extends BaseResponse { useTotp: boolean; use2fa: boolean; useApi: boolean; + useBusinessPortal: boolean; selfHost: boolean; usersGetPremium: boolean; seats: number; @@ -34,6 +35,7 @@ export class ProfileOrganizationResponse extends BaseResponse { this.useTotp = this.getResponseProperty('UseTotp'); this.use2fa = this.getResponseProperty('Use2fa'); this.useApi = this.getResponseProperty('UseApi'); + this.useBusinessPortal = this.getResponseProperty('UseBusinessPortal'); this.selfHost = this.getResponseProperty('SelfHost'); this.usersGetPremium = this.getResponseProperty('UsersGetPremium'); this.seats = this.getResponseProperty('Seats'); From 0d3b32a10d27fe642cdc90b5059fa6218e30989d Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Thu, 9 Jul 2020 15:56:09 -0500 Subject: [PATCH 0993/1626] Prevent malformed URLs from loading current tab --- src/services/cipher.service.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 7b519a1a4f..93d5a1fbcd 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -333,9 +333,12 @@ export class CipherService implements CipherServiceAbstraction { this.settingsService.getEquivalentDomains().then((eqDomains: any[][]) => { let matches: any[] = []; eqDomains.forEach((eqDomain) => { - if (eqDomain.length && eqDomain.indexOf(domain) >= 0) { - matches = matches.concat(eqDomain); + try { + if (eqDomain.length && eqDomain.indexOf(domain) >= 0) { + matches = matches.concat(eqDomain); + } } + catch {} }); if (!matches.length) { From ebaa69a15bc220bf87208dfc673078ea3ad08316 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Thu, 9 Jul 2020 15:59:45 -0500 Subject: [PATCH 0994/1626] Formatting change. Inlined catch. --- src/services/cipher.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 93d5a1fbcd..eaf1ec0f83 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -337,8 +337,7 @@ export class CipherService implements CipherServiceAbstraction { if (eqDomain.length && eqDomain.indexOf(domain) >= 0) { matches = matches.concat(eqDomain); } - } - catch {} + } catch {} }); if (!matches.length) { From 57649f31c4b1ef210e4070f7e031758a9de4ee19 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Fri, 10 Jul 2020 10:01:15 -0500 Subject: [PATCH 0995/1626] Moved error checking to utils, where parse is --- src/misc/utils.ts | 11 ++++++++--- src/services/cipher.service.ts | 8 +++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/misc/utils.ts b/src/misc/utils.ts index e0b7041356..e9b8b4911d 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -204,9 +204,14 @@ export class Utils { } catch (e) { } } - const domain = tldjs != null && tldjs.getDomain != null ? tldjs.getDomain(uriString) : null; - if (domain != null) { - return domain; + try { + const domain = tldjs != null && tldjs.getDomain != null ? tldjs.getDomain(uriString) : null; + + if (domain != null) { + return domain; + } + } catch { + return null; } return null; diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index eaf1ec0f83..7b519a1a4f 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -333,11 +333,9 @@ export class CipherService implements CipherServiceAbstraction { this.settingsService.getEquivalentDomains().then((eqDomains: any[][]) => { let matches: any[] = []; eqDomains.forEach((eqDomain) => { - try { - if (eqDomain.length && eqDomain.indexOf(domain) >= 0) { - matches = matches.concat(eqDomain); - } - } catch {} + if (eqDomain.length && eqDomain.indexOf(domain) >= 0) { + matches = matches.concat(eqDomain); + } }); if (!matches.length) { From e23b0d14ebc5e9c3d6e83d1aae34e3651bbc2034 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 10 Jul 2020 15:18:00 -0500 Subject: [PATCH 0996/1626] Added custom field consideration to basic search function --- src/services/search.service.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index a7149cafba..b6041c72fa 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -164,6 +164,20 @@ export class SearchService implements SearchServiceAbstraction { if (c.login && c.login.uri != null && c.login.uri.toLowerCase().indexOf(query) > -1) { return true; } + + + if(c.hasFields){ + for (let field of c.fields) { + if (field.value.toLowerCase().indexOf(query) > -1) { + return true; + } + + if(field.name.toLowerCase().indexOf(query) > -1) { + return true; + } + } + } + return false; }); } From 031a30518c8499fb7e34629851ae916501087dbf Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 10 Jul 2020 15:19:14 -0500 Subject: [PATCH 0997/1626] Removed an extra line --- src/services/search.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index b6041c72fa..e1bdfc925a 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -165,7 +165,6 @@ export class SearchService implements SearchServiceAbstraction { return true; } - if(c.hasFields){ for (let field of c.fields) { if (field.value.toLowerCase().indexOf(query) > -1) { From 00426fe9be92b3ce6f1651c59aa881052b7f3ba4 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 10 Jul 2020 15:23:05 -0500 Subject: [PATCH 0998/1626] fix a formatting issue --- src/services/search.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index e1bdfc925a..e5bebf4368 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -165,7 +165,7 @@ export class SearchService implements SearchServiceAbstraction { return true; } - if(c.hasFields){ + if(c.hasFields) { for (let field of c.fields) { if (field.value.toLowerCase().indexOf(query) > -1) { return true; From dad0e2ce6e4880dd1288ae28e9793a2d78fb11b3 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 10 Jul 2020 15:40:32 -0500 Subject: [PATCH 0999/1626] fixed a formatting issue --- src/services/search.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index e5bebf4368..a96724f6e6 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -165,7 +165,7 @@ export class SearchService implements SearchServiceAbstraction { return true; } - if(c.hasFields) { + if (c.hasFields) { for (let field of c.fields) { if (field.value.toLowerCase().indexOf(query) > -1) { return true; From 2af179ba334662066a9b2299354cd48b59187ed7 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 10 Jul 2020 15:44:42 -0500 Subject: [PATCH 1000/1626] yet another formatting issue --- src/services/search.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index a96724f6e6..fa7510d359 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -171,7 +171,7 @@ export class SearchService implements SearchServiceAbstraction { return true; } - if(field.name.toLowerCase().indexOf(query) > -1) { + if (field.name.toLowerCase().indexOf(query) > -1) { return true; } } From 5dcd69bc62816c405480b2e112f0c873310fbdcd Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 10 Jul 2020 15:51:01 -0500 Subject: [PATCH 1001/1626] changed let to const --- src/services/search.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index fa7510d359..9787b19aa9 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -166,7 +166,7 @@ export class SearchService implements SearchServiceAbstraction { } if (c.hasFields) { - for (let field of c.fields) { + for (const field of c.fields) { if (field.value.toLowerCase().indexOf(query) > -1) { return true; } From 58ba1ce5b6183d73c1aeb51a1fe6a7f636fdc774 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Mon, 13 Jul 2020 13:35:37 -0500 Subject: [PATCH 1002/1626] Modified response to include port if exists --- src/misc/utils.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/misc/utils.ts b/src/misc/utils.ts index e9b8b4911d..119e9361ed 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -157,7 +157,13 @@ export class Utils { static getHostname(uriString: string): string { const url = Utils.getUrl(uriString); try { - return url != null && url.hostname !== '' ? url.hostname : null; + let hostname = url != null && url.hostname !== '' ? url.hostname : null; + + if(hostname != null && url.port !== '') { + hostname += ":" + url.port; + } + + return hostname; } catch { return null; } From 49b796ebd695dd69dee89809445dad0e454d2b1e Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Mon, 13 Jul 2020 13:39:38 -0500 Subject: [PATCH 1003/1626] Formatting change --- src/misc/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 119e9361ed..806aeb5a74 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -159,7 +159,7 @@ export class Utils { try { let hostname = url != null && url.hostname !== '' ? url.hostname : null; - if(hostname != null && url.port !== '') { + if (hostname != null && url.port !== '') { hostname += ":" + url.port; } From 82230112487cc487f7298af8b567b4f7beb24988 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Mon, 13 Jul 2020 15:47:55 -0500 Subject: [PATCH 1004/1626] Reverted prior change. Changed call to getHost --- src/misc/utils.ts | 8 +------- src/models/view/loginUriView.ts | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 806aeb5a74..e9b8b4911d 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -157,13 +157,7 @@ export class Utils { static getHostname(uriString: string): string { const url = Utils.getUrl(uriString); try { - let hostname = url != null && url.hostname !== '' ? url.hostname : null; - - if (hostname != null && url.port !== '') { - hostname += ":" + url.port; - } - - return hostname; + return url != null && url.hostname !== '' ? url.hostname : null; } catch { return null; } diff --git a/src/models/view/loginUriView.ts b/src/models/view/loginUriView.ts index afa6e200fd..ce3dfca0eb 100644 --- a/src/models/view/loginUriView.ts +++ b/src/models/view/loginUriView.ts @@ -62,7 +62,7 @@ export class LoginUriView implements View { return null; } if (this._hostname == null && this.uri != null) { - this._hostname = Utils.getHostname(this.uri); + this._hostname = Utils.getHost(this.uri); if (this._hostname === '') { this._hostname = null; } From b53046d0d9cf8a5bd4ed860b91133cafec824b73 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Tue, 14 Jul 2020 10:02:07 -0500 Subject: [PATCH 1005/1626] Added new _host property for consumption. --- src/models/view/loginUriView.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/models/view/loginUriView.ts b/src/models/view/loginUriView.ts index ce3dfca0eb..7ef9fa092e 100644 --- a/src/models/view/loginUriView.ts +++ b/src/models/view/loginUriView.ts @@ -26,6 +26,7 @@ export class LoginUriView implements View { private _uri: string = null; private _domain: string = null; private _hostname: string = null; + private _host: string = null; private _canLaunch: boolean = null; // tslint:enable @@ -62,7 +63,7 @@ export class LoginUriView implements View { return null; } if (this._hostname == null && this.uri != null) { - this._hostname = Utils.getHost(this.uri); + this._hostname = Utils.getHostname(this.uri); if (this._hostname === '') { this._hostname = null; } @@ -71,10 +72,28 @@ export class LoginUriView implements View { return this._hostname; } + get host(): string { + if (this.match === UriMatchType.RegularExpression) { + return null; + } + if (this._host == null && this.uri != null) { + this._host = Utils.getHost(this.uri); + if (this._host === '') { + this._host = null; + } + } + + return this._host; + } + get hostnameOrUri(): string { return this.hostname != null ? this.hostname : this.uri; } + get hostOrUri(): string { + return this.host != null ? this.host : this.uri; + } + get isWebsite(): boolean { return this.uri != null && (this.uri.indexOf('http://') === 0 || this.uri.indexOf('https://') === 0 || (this.uri.indexOf('://') < 0 && Utils.tldEndingRegex.test(this.uri))); From fefef546f0f9e11377203df47de44a54809ca655 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 16 Jul 2020 08:59:29 -0400 Subject: [PATCH 1006/1626] sso support (#127) * support for sso * created master password boolean * resetMasterPassword flows * throw on bad ctor for token request --- src/abstractions/api.service.ts | 1 + src/abstractions/auth.service.ts | 6 ++ src/angular/components/lock.component.ts | 28 ++++++- .../components/two-factor.component.ts | 6 +- src/misc/utils.ts | 8 ++ src/models/domain/authResult.ts | 1 + src/models/request/tokenRequest.ts | 31 +++++-- src/models/response/identityTokenResponse.ts | 8 ++ src/services/api.service.ts | 4 + src/services/auth.service.ts | 80 ++++++++++++++----- src/services/constants.service.ts | 4 + 11 files changed, 148 insertions(+), 29 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 337b260b9e..b345e74fce 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -141,6 +141,7 @@ export abstract class ApiService { postAccountKeys: (request: KeysRequest) => Promise; postAccountVerifyEmail: () => Promise; postAccountVerifyEmailToken: (request: VerifyEmailRequest) => Promise; + postAccountVerifyPassword: (request: PasswordVerificationRequest) => Promise; postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise; postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise; postAccountKdf: (request: KdfRequest) => Promise; diff --git a/src/abstractions/auth.service.ts b/src/abstractions/auth.service.ts index de7481ce60..21bba19633 100644 --- a/src/abstractions/auth.service.ts +++ b/src/abstractions/auth.service.ts @@ -6,10 +6,14 @@ import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; export abstract class AuthService { email: string; masterPasswordHash: string; + code: string; + codeVerifier: string; + ssoRedirectUrl: string; twoFactorProvidersData: Map; selectedTwoFactorProviderType: TwoFactorProviderType; logIn: (email: string, masterPassword: string) => Promise; + logInSso: (code: string, codeVerifier: string, redirectUrl: string) => Promise; logInTwoFactor: (twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean) => Promise; logInComplete: (email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType, @@ -18,4 +22,6 @@ export abstract class AuthService { getSupportedTwoFactorProviders: (win: Window) => any[]; getDefaultTwoFactorProvider: (u2fSupported: boolean) => TwoFactorProviderType; makePreloginKey: (masterPassword: string, email: string) => Promise; + authingWithSso: () => boolean; + authingWithPassword: () => boolean; } diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 107ab47191..0cf22362c3 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -1,6 +1,7 @@ import { OnInit } from '@angular/core'; import { Router } from '@angular/router'; +import { ApiService } from '../../abstractions/api.service'; import { CryptoService } from '../../abstractions/crypto.service'; import { EnvironmentService } from '../../abstractions/environment.service'; import { I18nService } from '../../abstractions/i18n.service'; @@ -16,6 +17,8 @@ import { ConstantsService } from '../../services/constants.service'; import { CipherString } from '../../models/domain/cipherString'; import { SymmetricCryptoKey } from '../../models/domain/symmetricCryptoKey'; +import { PasswordVerificationRequest } from '../../models/request/passwordVerificationRequest'; + import { Utils } from '../../misc/utils'; export class LockComponent implements OnInit { @@ -25,6 +28,7 @@ export class LockComponent implements OnInit { email: string; pinLock: boolean = false; webVaultHostname: string = ''; + formPromise: Promise; protected successRoute: string = 'vault'; protected onSuccessfulSubmit: () => void; @@ -36,7 +40,8 @@ export class LockComponent implements OnInit { protected platformUtilsService: PlatformUtilsService, protected messagingService: MessagingService, protected userService: UserService, protected cryptoService: CryptoService, protected storageService: StorageService, protected vaultTimeoutService: VaultTimeoutService, - protected environmentService: EnvironmentService, protected stateService: StateService) { } + protected environmentService: EnvironmentService, protected stateService: StateService, + protected apiService: ApiService) { } async ngOnInit() { this.pinSet = await this.vaultTimeoutService.isPinLockSet(); @@ -98,9 +103,26 @@ export class LockComponent implements OnInit { } else { const key = await this.cryptoService.makeKey(this.masterPassword, this.email, kdf, kdfIterations); const keyHash = await this.cryptoService.hashPassword(this.masterPassword, key); - const storedKeyHash = await this.cryptoService.getKeyHash(); - if (storedKeyHash != null && keyHash != null && storedKeyHash === keyHash) { + let passwordValid = false; + + if (keyHash != null) { + const storedKeyHash = await this.cryptoService.getKeyHash(); + if (storedKeyHash != null) { + passwordValid = storedKeyHash === keyHash; + } else { + const request = new PasswordVerificationRequest(); + request.masterPasswordHash = keyHash; + try { + this.formPromise = this.apiService.postAccountVerifyPassword(request); + await this.formPromise; + passwordValid = true; + await this.cryptoService.setKeyHash(keyHash); + } catch { } + } + } + + if (passwordValid) { if (this.pinSet[0]) { const protectedPin = await this.storageService.get(ConstantsService.protectedPin); const encKey = await this.cryptoService.getEncKey(key); diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 9d3379405c..94a1e1553a 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -52,12 +52,16 @@ export class TwoFactorComponent implements OnInit, OnDestroy { } async ngOnInit() { - if (this.authService.email == null || this.authService.masterPasswordHash == null || + if ((!this.authService.authingWithSso() && !this.authService.authingWithPassword()) || this.authService.twoFactorProvidersData == null) { this.router.navigate([this.loginRoute]); return; } + if (this.authService.authingWithSso()) { + this.successRoute = 'lock'; + } + if (this.initU2f && this.win != null && this.u2fSupported) { let customWebVaultUrl: string = null; if (this.environmentService.baseUrl != null) { diff --git a/src/misc/utils.ts b/src/misc/utils.ts index e9b8b4911d..4d9bf8533e 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -89,6 +89,14 @@ export class Utils { } } + static fromBufferToUrlB64(buffer: ArrayBuffer): string { + const output = this.fromBufferToB64(buffer) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + return output; + } + static fromBufferToUtf8(buffer: ArrayBuffer): string { if (Utils.isNode || Utils.isNativeScript) { return Buffer.from(buffer).toString('utf8'); diff --git a/src/models/domain/authResult.ts b/src/models/domain/authResult.ts index cb4bb57c65..0f5b95a521 100644 --- a/src/models/domain/authResult.ts +++ b/src/models/domain/authResult.ts @@ -2,5 +2,6 @@ import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; export class AuthResult { twoFactor: boolean = false; + resetMasterPassword: boolean = false; twoFactorProviders: Map = null; } diff --git a/src/models/request/tokenRequest.ts b/src/models/request/tokenRequest.ts index c9bcd5ad36..f0ff702428 100644 --- a/src/models/request/tokenRequest.ts +++ b/src/models/request/tokenRequest.ts @@ -5,15 +5,24 @@ import { DeviceRequest } from './deviceRequest'; export class TokenRequest { email: string; masterPasswordHash: string; + code: string; + codeVerifier: string; + redirectUri: string; token: string; provider: TwoFactorProviderType; remember: boolean; device?: DeviceRequest; - constructor(email: string, masterPasswordHash: string, provider: TwoFactorProviderType, + constructor(credentials: string[], codes: string[], provider: TwoFactorProviderType, token: string, remember: boolean, device?: DeviceRequest) { - this.email = email; - this.masterPasswordHash = masterPasswordHash; + if (credentials != null && credentials.length > 1) { + this.email = credentials[0]; + this.masterPasswordHash = credentials[1]; + } else if (codes != null && codes.length > 2) { + this.code = codes[0]; + this.codeVerifier = codes[1]; + this.redirectUri = codes[2]; + } this.token = token; this.provider = provider; this.remember = remember; @@ -22,13 +31,23 @@ export class TokenRequest { toIdentityToken(clientId: string) { const obj: any = { - grant_type: 'password', - username: this.email, - password: this.masterPasswordHash, scope: 'api offline_access', client_id: clientId, }; + if (this.masterPasswordHash != null && this.email != null) { + obj.grant_type = 'password'; + obj.username = this.email; + obj.password = this.masterPasswordHash; + } else if (this.code != null && this.codeVerifier != null && this.redirectUri != null) { + obj.grant_type = 'authorization_code'; + obj.code = this.code; + obj.code_verifier = this.codeVerifier; + obj.redirect_uri = this.redirectUri; + } else { + throw new Error('must provide credentials or codes'); + } + if (this.device) { obj.deviceType = this.device.type; obj.deviceIdentifier = this.device.identifier; diff --git a/src/models/response/identityTokenResponse.ts b/src/models/response/identityTokenResponse.ts index 20adaff718..7ce1afba0c 100644 --- a/src/models/response/identityTokenResponse.ts +++ b/src/models/response/identityTokenResponse.ts @@ -1,14 +1,19 @@ import { BaseResponse } from './baseResponse'; +import { KdfType } from '../../enums/kdfType'; + export class IdentityTokenResponse extends BaseResponse { accessToken: string; expiresIn: number; refreshToken: string; tokenType: string; + resetMasterPassword: boolean; privateKey: string; key: string; twoFactorToken: string; + kdf: KdfType; + kdfIterations: number; constructor(response: any) { super(response); @@ -17,8 +22,11 @@ export class IdentityTokenResponse extends BaseResponse { this.refreshToken = response.refresh_token; this.tokenType = response.token_type; + this.resetMasterPassword = this.getResponseProperty('ResetMasterPassword'); this.privateKey = this.getResponseProperty('PrivateKey'); this.key = this.getResponseProperty('Key'); this.twoFactorToken = this.getResponseProperty('TwoFactorToken'); + this.kdf = this.getResponseProperty('Kdf'); + this.kdfIterations = this.getResponseProperty('KdfIterations'); } } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index f86a4efcff..8b9249429e 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -321,6 +321,10 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/accounts/verify-email-token', request, false, false); } + postAccountVerifyPassword(request: PasswordVerificationRequest): Promise { + return this.send('POST', '/accounts/verify-password', request, true, false); + } + postAccountRecoverDelete(request: DeleteRecoverRequest): Promise { return this.send('POST', '/accounts/delete-recover', request, false, false); } diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index ad379f29fd..9a8d0ec3e0 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -77,12 +77,13 @@ export const TwoFactorProviders = { export class AuthService implements AuthServiceAbstraction { email: string; masterPasswordHash: string; + code: string; + codeVerifier: string; + ssoRedirectUrl: string; twoFactorProvidersData: Map; selectedTwoFactorProviderType: TwoFactorProviderType = null; private key: SymmetricCryptoKey; - private kdf: KdfType; - private kdfIterations: number; constructor(private cryptoService: CryptoService, private apiService: ApiService, private userService: UserService, private tokenService: TokenService, @@ -116,13 +117,19 @@ export class AuthService implements AuthServiceAbstraction { this.selectedTwoFactorProviderType = null; const key = await this.makePreloginKey(masterPassword, email); const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); - return await this.logInHelper(email, hashedPassword, key); + return await this.logInHelper(email, hashedPassword, null, null, null, key, + null, null, null); + } + + async logInSso(code: string, codeVerifier: string, redirectUrl: string): Promise { + this.selectedTwoFactorProviderType = null; + return await this.logInHelper(null, null, code, codeVerifier, redirectUrl, null, null, null, null); } async logInTwoFactor(twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean): Promise { - return await this.logInHelper(this.email, this.masterPasswordHash, this.key, twoFactorProvider, - twoFactorToken, remember); + return await this.logInHelper(this.email, this.masterPasswordHash, this.code, this.codeVerifier, + this.ssoRedirectUrl, this.key, twoFactorProvider, twoFactorToken, remember); } async logInComplete(email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType, @@ -130,7 +137,8 @@ export class AuthService implements AuthServiceAbstraction { this.selectedTwoFactorProviderType = null; const key = await this.makePreloginKey(masterPassword, email); const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); - return await this.logInHelper(email, hashedPassword, key, twoFactorProvider, twoFactorToken, remember); + return await this.logInHelper(email, hashedPassword, null, null, null, key, twoFactorProvider, twoFactorToken, + remember); } logOut(callback: Function) { @@ -201,37 +209,59 @@ export class AuthService implements AuthServiceAbstraction { async makePreloginKey(masterPassword: string, email: string): Promise { email = email.trim().toLowerCase(); - this.kdf = null; - this.kdfIterations = null; + let kdf: KdfType = null; + let kdfIterations: number = null; try { const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email)); if (preloginResponse != null) { - this.kdf = preloginResponse.kdf; - this.kdfIterations = preloginResponse.kdfIterations; + kdf = preloginResponse.kdf; + kdfIterations = preloginResponse.kdfIterations; } } catch (e) { if (e == null || e.statusCode !== 404) { throw e; } } - return this.cryptoService.makeKey(masterPassword, email, this.kdf, this.kdfIterations); + return this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations); } - private async logInHelper(email: string, hashedPassword: string, key: SymmetricCryptoKey, - twoFactorProvider?: TwoFactorProviderType, twoFactorToken?: string, remember?: boolean): Promise { + authingWithSso(): boolean { + return this.code != null && this.codeVerifier != null && this.ssoRedirectUrl != null; + } + + authingWithPassword(): boolean { + return this.email != null && this.masterPasswordHash != null; + } + + private async logInHelper(email: string, hashedPassword: string, code: string, codeVerifier: string, + redirectUrl: string, key: SymmetricCryptoKey, twoFactorProvider?: TwoFactorProviderType, + twoFactorToken?: string, remember?: boolean): Promise { const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email); const appId = await this.appIdService.getAppId(); const deviceRequest = new DeviceRequest(appId, this.platformUtilsService); + let emailPassword: string[] = []; + let codeCodeVerifier: string[] = []; + if (email != null && hashedPassword != null) { + emailPassword = [email, hashedPassword]; + } else { + emailPassword = null; + } + if (code != null && codeVerifier != null && redirectUrl != null) { + codeCodeVerifier = [code, codeVerifier, redirectUrl]; + } else { + codeCodeVerifier = null; + } + let request: TokenRequest; if (twoFactorToken != null && twoFactorProvider != null) { - request = new TokenRequest(email, hashedPassword, twoFactorProvider, twoFactorToken, remember, + request = new TokenRequest(emailPassword, codeCodeVerifier, twoFactorProvider, twoFactorToken, remember, deviceRequest); } else if (storedTwoFactorToken != null) { - request = new TokenRequest(email, hashedPassword, TwoFactorProviderType.Remember, + request = new TokenRequest(emailPassword, codeCodeVerifier, TwoFactorProviderType.Remember, storedTwoFactorToken, false, deviceRequest); } else { - request = new TokenRequest(email, hashedPassword, null, null, false, deviceRequest); + request = new TokenRequest(emailPassword, codeCodeVerifier, null, null, false, deviceRequest); } const response = await this.apiService.postIdentityToken(request); @@ -245,6 +275,9 @@ export class AuthService implements AuthServiceAbstraction { const twoFactorResponse = response as IdentityTwoFactorResponse; this.email = email; this.masterPasswordHash = hashedPassword; + this.code = code; + this.codeVerifier = codeVerifier; + this.ssoRedirectUrl = redirectUrl; this.key = this.setCryptoKeys ? key : null; this.twoFactorProvidersData = twoFactorResponse.twoFactorProviders2; result.twoFactorProviders = twoFactorResponse.twoFactorProviders2; @@ -252,16 +285,21 @@ export class AuthService implements AuthServiceAbstraction { } const tokenResponse = response as IdentityTokenResponse; + result.resetMasterPassword = tokenResponse.resetMasterPassword; if (tokenResponse.twoFactorToken != null) { await this.tokenService.setTwoFactorToken(tokenResponse.twoFactorToken, email); } await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken); await this.userService.setInformation(this.tokenService.getUserId(), this.tokenService.getEmail(), - this.kdf, this.kdfIterations); + tokenResponse.kdf, tokenResponse.kdfIterations); if (this.setCryptoKeys) { - await this.cryptoService.setKey(key); - await this.cryptoService.setKeyHash(hashedPassword); + if (key != null) { + await this.cryptoService.setKey(key); + } + if (hashedPassword != null) { + await this.cryptoService.setKeyHash(hashedPassword); + } await this.cryptoService.setEncKey(tokenResponse.key); // User doesn't have a key pair yet (old account), let's generate one for them @@ -284,8 +322,12 @@ export class AuthService implements AuthServiceAbstraction { } private clearState(): void { + this.key = null; this.email = null; this.masterPasswordHash = null; + this.code = null; + this.codeVerifier = null; + this.ssoRedirectUrl = null; this.twoFactorProvidersData = null; this.selectedTwoFactorProviderType = null; } diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index 396ebffa3f..752648fd79 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -23,6 +23,8 @@ export class ConstantsService { static readonly protectedPin: string = 'protectedPin'; static readonly clearClipboardKey: string = 'clearClipboardKey'; static readonly eventCollectionKey: string = 'eventCollection'; + static readonly ssoCodeVerifierKey: string = 'ssoCodeVerifier'; + static readonly ssoStateKey: string = 'ssoState'; readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; readonly disableGaKey: string = ConstantsService.disableGaKey; @@ -47,4 +49,6 @@ export class ConstantsService { readonly protectedPin: string = ConstantsService.protectedPin; readonly clearClipboardKey: string = ConstantsService.clearClipboardKey; readonly eventCollectionKey: string = ConstantsService.eventCollectionKey; + readonly ssoCodeVerifierKey: string = ConstantsService.ssoCodeVerifierKey; + readonly ssoStateKey: string = ConstantsService.ssoStateKey; } From b95e7aeb6229fe388150663c3b7f6a898ddf176b Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Thu, 16 Jul 2020 10:05:20 -0400 Subject: [PATCH 1007/1626] Updated contribution guidance for developers --- CONTRIBUTING.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 210e7f5120..ec7f6e2f9a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1 +1,62 @@ Code contributions are welcome! Please commit any pull requests against the `master` branch. + +# Setting up your Local Dev environment for jslib +In order to easily test, check and develop against local changes to jslib across each of the TypeScript/JavaScript clients it is recommended to use symlinks for the submodule so that you only have to make the change once and don't need to x-copy or wait for a commit+merge to checkout, pull and test against your other repos. + +## Prerequisites +1. git bash or other git command line + +## Clone Repos +In order for this to work well, you need to use a consistent relative directory structure. Repos should be cloned in the following way: + +* `./`; we'll call this `/dev` ('cause why not) + * jslib - `git clone https://github.com/bitwarden/jslib.git` (/dev/jslib) + * web - `git clone --recurse-submodules https://github.com/bitwarden/web.git` (/dev/web) + * desktop - `git clone --recurse-submodules https://github.com/bitwarden/desktop.git` (/dev/desktop) + * browser - `git clone --recurse-submodules https://github.com/bitwarden/browser.git` (/dev/browser) + * cli - `git clone --recurse-submodules https://github.com/bitwarden/cli` (/dev/cli) + +You should notice web, desktop, browser and cli each reference jslib as a git submodule. If you've already cloned the repos but didn't use `--recurse-submodules` then you'll need to init those: + +`npm run sub:init` + +## Configure Symlinks +Be aware that using git clone will make symlinks added to your repo be seen by git as plain text file paths, lets make sure this is set to true to prevent that. In the project root run, `git config core.symlinks true`. + +For each project other than jslib, run the following: + +For macOS/Linux: `npm run symlink:mac` + +For Windows: `npm run symlink:win` + +## Updates and Cleanup +* Need to update parent repo that has jslib as a submodule to latest from actual jslib repo? + * Create branch from master (`git checkout -b update-jslib`) + * From new local branch: `npm run sub:pull` (`git submodule foreach git pull origin master`) + * Follow Pull Request notes for commit/push instructions + * Once merged, pull master, rebase your feature branch and then do npm run sub:update to catch your submodule up +* Discard changes made to a submodule + * `git submodule foreach git reset —hard` + + +## Merge Conflicts +At times when you need to perform a `git merge master` into your feature or local branch, and there are conflicting version references to the *jslib* repo from your other clients, you will not be able to use the traditional merge or stage functions you would normally use for a file. + +To resolve you must use either `git reset` or update the index directly using `git update-index`. You can use (depending on whether you have symlink'd jslib) one of the following: + +```bash +git reset master -- jslib +git reset master@{upstream} -- jslib +git reset HEAD -- jslib +git reset MERGE_HEAD -- jslib +``` + +Those should automatically stage the change and reset the jslib submodule back to where it needs to be (generally at the latest version from `master`). + +The other option is to update the index directly using the plumbing command git update-index. To do that, you need to know that an object of type gitlink (i.e., directory entry in a parent repository that points to a submodule) is 0160000. You can figure it out from `git ls-files -s` or the following reference (see "1110 (gitlink)" under 4-bit object type): https://github.com/gitster/git/blob/master/Documentation/technical/index-format.txt + +To use that approach, figure out the hash you want to set the submodule to, then run, e.g.: + +`git update-index --cacheinfo 0160000,533da4ea00703f4ad6d5518e1ce81d20261c40c0,jslib` + +see: [https://stackoverflow.com/questions/26617838/how-to-resolve-git-submodule-conflict-if-submodule-is-not-initialized](https://stackoverflow.com/questions/26617838/how-to-resolve-git-submodule-conflict-if-submodule-is-not-initialized) From 80c5ded7855862d82539a57c0d65af3947da328f Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Thu, 16 Jul 2020 15:39:19 -0500 Subject: [PATCH 1008/1626] moved some cipher selection logic to base component --- src/angular/components/ciphers.component.ts | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/angular/components/ciphers.component.ts b/src/angular/components/ciphers.component.ts index 50beab2f54..0b0b8b1978 100644 --- a/src/angular/components/ciphers.component.ts +++ b/src/angular/components/ciphers.component.ts @@ -31,6 +31,8 @@ export class CiphersComponent { private pagedCiphersCount = 0; private refreshing = false; + private maxCheckedCount = 500; + constructor(protected searchService: SearchService) { } async load(filter: (cipher: CipherView) => boolean = null, deleted: boolean = false) { @@ -126,4 +128,29 @@ export class CiphersComponent { this.pagedCiphers = []; this.loadMore(); } + + selectAll(select: boolean) { + if (select) { + this.selectAll(false); + } + const selectCount = select && this.ciphers.length > this.maxCheckedCount ? this.maxCheckedCount : this.ciphers.length; + for (let i = 0; i < selectCount; i++) { + this.checkCipher(this.ciphers[i], select); + } + } + + checkCipher(c: CipherView, select?: boolean) { + (c as any).checked = select == null ? !(c as any).checked : select; + } + + getSelected(): CipherView[] { + if (this.ciphers == null) { + return []; + } + return this.ciphers.filter((c) => !!(c as any).checked); + } + + getSelectedIds(): string[] { + return this.getSelected().map((c) => c.id); + } } From 5a95c429379edc38fb49a55d2e9c600e64609079 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Thu, 16 Jul 2020 15:44:39 -0500 Subject: [PATCH 1009/1626] removed leaky code --- src/services/search.service.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 9787b19aa9..a7149cafba 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -164,19 +164,6 @@ export class SearchService implements SearchServiceAbstraction { if (c.login && c.login.uri != null && c.login.uri.toLowerCase().indexOf(query) > -1) { return true; } - - if (c.hasFields) { - for (const field of c.fields) { - if (field.value.toLowerCase().indexOf(query) > -1) { - return true; - } - - if (field.name.toLowerCase().indexOf(query) > -1) { - return true; - } - } - } - return false; }); } From 137ca87668ed967199f3d65f1d7e10e691bbd20c Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Thu, 16 Jul 2020 16:10:00 -0500 Subject: [PATCH 1010/1626] broke up a long line --- src/angular/components/ciphers.component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/angular/components/ciphers.component.ts b/src/angular/components/ciphers.component.ts index 0b0b8b1978..674f9b3efe 100644 --- a/src/angular/components/ciphers.component.ts +++ b/src/angular/components/ciphers.component.ts @@ -133,7 +133,9 @@ export class CiphersComponent { if (select) { this.selectAll(false); } - const selectCount = select && this.ciphers.length > this.maxCheckedCount ? this.maxCheckedCount : this.ciphers.length; + const selectCount = select && this.ciphers.length > this.maxCheckedCount + ? this.maxCheckedCount + : this.ciphers.length; for (let i = 0; i < selectCount; i++) { this.checkCipher(this.ciphers[i], select); } From 9ca79c49294ecbea10f68220d0da87dd9bf15f3f Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Fri, 17 Jul 2020 16:05:58 -0400 Subject: [PATCH 1011/1626] Reference id to data conversion --- src/angular/components/register.component.ts | 5 +++-- src/models/domain/referenceEventData.ts | 5 +++++ src/models/request/registerRequest.ts | 7 ++++--- 3 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 src/models/domain/referenceEventData.ts diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index b3ad95d377..29a9ee04ed 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -12,6 +12,7 @@ import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { StateService } from '../../abstractions/state.service'; import { KdfType } from '../../enums/kdfType'; +import { ReferenceEventData } from '../../models/domain/referenceEventData'; export class RegisterComponent { name: string = ''; @@ -22,7 +23,7 @@ export class RegisterComponent { showPassword: boolean = false; formPromise: Promise; masterPasswordScore: number; - referenceId: string; + referenceData: ReferenceEventData; protected successRoute = 'login'; private masterPasswordStrengthTimeout: any; @@ -111,7 +112,7 @@ export class RegisterComponent { const hashedPassword = await this.cryptoService.hashPassword(this.masterPassword, key); const keys = await this.cryptoService.makeKeyPair(encKey[0]); const request = new RegisterRequest(this.email, this.name, hashedPassword, - this.hint, encKey[1].encryptedString, kdf, kdfIterations, this.referenceId); + this.hint, encKey[1].encryptedString, kdf, kdfIterations, this.referenceData); request.keys = new KeysRequest(keys[0], keys[1].encryptedString); const orgInvite = await this.stateService.get('orgInvitation'); if (orgInvite != null && orgInvite.token != null && orgInvite.organizationUserId != null) { diff --git a/src/models/domain/referenceEventData.ts b/src/models/domain/referenceEventData.ts new file mode 100644 index 0000000000..78ffcbfaf5 --- /dev/null +++ b/src/models/domain/referenceEventData.ts @@ -0,0 +1,5 @@ +export class ReferenceEventData { + id: string; + layout: string; + flow: string; +} diff --git a/src/models/request/registerRequest.ts b/src/models/request/registerRequest.ts index f96544e80e..ebe998435a 100644 --- a/src/models/request/registerRequest.ts +++ b/src/models/request/registerRequest.ts @@ -1,6 +1,7 @@ import { KeysRequest } from './keysRequest'; import { KdfType } from '../../enums/kdfType'; +import { ReferenceEventData } from '../domain/referenceEventData'; export class RegisterRequest { name: string; @@ -13,10 +14,10 @@ export class RegisterRequest { organizationUserId: string; kdf: KdfType; kdfIterations: number; - referenceId: string; + referenceData: ReferenceEventData; constructor(email: string, name: string, masterPasswordHash: string, masterPasswordHint: string, key: string, - kdf: KdfType, kdfIterations: number, referenceId: string) { + kdf: KdfType, kdfIterations: number, referenceData: ReferenceEventData) { this.name = name; this.email = email; this.masterPasswordHash = masterPasswordHash; @@ -24,6 +25,6 @@ export class RegisterRequest { this.key = key; this.kdf = kdf; this.kdfIterations = kdfIterations; - this.referenceId = referenceId; + this.referenceData = referenceData; } } From 2a38c8a9de362abbc96030d14abf646f53ef8a87 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Sun, 19 Jul 2020 22:26:45 -0500 Subject: [PATCH 1012/1626] added api service methods for delete many w/admin calls --- src/abstractions/api.service.ts | 2 ++ src/services/api.service.ts | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 337b260b9e..a0adc7e50a 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -162,6 +162,7 @@ export abstract class ApiService { deleteCipher: (id: string) => Promise; deleteCipherAdmin: (id: string) => Promise; deleteManyCiphers: (request: CipherBulkDeleteRequest) => Promise; + deleteManyCiphersAdmin: (request: CipherBulkDeleteRequest) => Promise; putMoveCiphers: (request: CipherBulkMoveRequest) => Promise; putShareCipher: (id: string, request: CipherShareRequest) => Promise; putShareCiphers: (request: CipherBulkShareRequest) => Promise; @@ -173,6 +174,7 @@ export abstract class ApiService { putDeleteCipher: (id: string) => Promise; putDeleteCipherAdmin: (id: string) => Promise; putDeleteManyCiphers: (request: CipherBulkDeleteRequest) => Promise; + putDeleteManyCiphersAdmin: (request: CipherBulkDeleteRequest) => Promise; putRestoreCipher: (id: string) => Promise; putRestoreCipherAdmin: (id: string) => Promise; putRestoreManyCiphers: (request: CipherBulkRestoreRequest) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index f86a4efcff..02d0bd4be7 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -414,6 +414,10 @@ export class ApiService implements ApiServiceAbstraction { return this.send('DELETE', '/ciphers', request, true, false); } + deleteManyCiphersAdmin(request: CipherBulkDeleteRequest): Promise { + return this.send('DELETE', '/ciphers/admin', request, true, false); + } + putMoveCiphers(request: CipherBulkMoveRequest): Promise { return this.send('PUT', '/ciphers/move', request, true, false); } @@ -463,6 +467,10 @@ export class ApiService implements ApiServiceAbstraction { return this.send('PUT', '/ciphers/delete', request, true, false); } + putDeleteManyCiphersAdmin(request: CipherBulkDeleteRequest): Promise { + return this.send('PUT', '/ciphers/delete-admin', request, true, false); + } + putRestoreCipher(id: string): Promise { return this.send('PUT', '/ciphers/' + id + '/restore', null, true, false); } From b599c2e74f32534dd85dc94593d38c18f2414842 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 20 Jul 2020 14:20:32 -0400 Subject: [PATCH 1013/1626] support url header for firefox import (#132) --- src/importers/firefoxCsvImporter.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/importers/firefoxCsvImporter.ts b/src/importers/firefoxCsvImporter.ts index 8c5b1c1313..a7d6a5b356 100644 --- a/src/importers/firefoxCsvImporter.ts +++ b/src/importers/firefoxCsvImporter.ts @@ -14,10 +14,11 @@ export class FirefoxCsvImporter extends BaseImporter implements Importer { results.forEach((value) => { const cipher = this.initLoginCipher(); - cipher.name = this.getValueOrDefault(this.nameFromUrl(value.hostname), '--'); + const url = this.getValueOrDefault(value.url, this.getValueOrDefault(value.hostname)); + cipher.name = this.getValueOrDefault(this.nameFromUrl(url), '--'); cipher.login.username = this.getValueOrDefault(value.username); cipher.login.password = this.getValueOrDefault(value.password); - cipher.login.uris = this.makeUriArray(value.hostname); + cipher.login.uris = this.makeUriArray(url); this.cleanupCipher(cipher); result.ciphers.push(cipher); }); From 7771b2293dbfc08fc867bb18d3c84bfe88d67ee2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 20 Jul 2020 15:00:33 -0400 Subject: [PATCH 1014/1626] parse otp for keepass import (#133) --- src/importers/keepass2XmlImporter.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/importers/keepass2XmlImporter.ts b/src/importers/keepass2XmlImporter.ts index 1ad213e238..a485d2265e 100644 --- a/src/importers/keepass2XmlImporter.ts +++ b/src/importers/keepass2XmlImporter.ts @@ -68,6 +68,8 @@ export class KeePass2XmlImporter extends BaseImporter implements Importer { cipher.login.username = value; } else if (key === 'Password') { cipher.login.password = value; + } else if (key === 'otp') { + cipher.login.totp = value.replace('key=', ''); } else if (key === 'Title') { cipher.name = value; } else if (key === 'Notes') { From 97d24f5abffccd657655c17da05ca932649c9d13 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Mon, 20 Jul 2020 15:21:01 -0400 Subject: [PATCH 1015/1626] reference event data model changes --- src/angular/components/register.component.ts | 4 ++-- .../referenceEventRequest.ts} | 2 +- src/models/request/registerRequest.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/models/{domain/referenceEventData.ts => request/referenceEventRequest.ts} (60%) diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index 29a9ee04ed..0ba73829d2 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -12,7 +12,7 @@ import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { StateService } from '../../abstractions/state.service'; import { KdfType } from '../../enums/kdfType'; -import { ReferenceEventData } from '../../models/domain/referenceEventData'; +import { ReferenceEventRequest } from '../../models/request/referenceEventRequest'; export class RegisterComponent { name: string = ''; @@ -23,7 +23,7 @@ export class RegisterComponent { showPassword: boolean = false; formPromise: Promise; masterPasswordScore: number; - referenceData: ReferenceEventData; + referenceData: ReferenceEventRequest; protected successRoute = 'login'; private masterPasswordStrengthTimeout: any; diff --git a/src/models/domain/referenceEventData.ts b/src/models/request/referenceEventRequest.ts similarity index 60% rename from src/models/domain/referenceEventData.ts rename to src/models/request/referenceEventRequest.ts index 78ffcbfaf5..4cd8c507f6 100644 --- a/src/models/domain/referenceEventData.ts +++ b/src/models/request/referenceEventRequest.ts @@ -1,4 +1,4 @@ -export class ReferenceEventData { +export class ReferenceEventRequest { id: string; layout: string; flow: string; diff --git a/src/models/request/registerRequest.ts b/src/models/request/registerRequest.ts index ebe998435a..a93ac69bba 100644 --- a/src/models/request/registerRequest.ts +++ b/src/models/request/registerRequest.ts @@ -1,7 +1,7 @@ import { KeysRequest } from './keysRequest'; +import { ReferenceEventRequest } from './referenceEventRequest'; import { KdfType } from '../../enums/kdfType'; -import { ReferenceEventData } from '../domain/referenceEventData'; export class RegisterRequest { name: string; @@ -14,10 +14,10 @@ export class RegisterRequest { organizationUserId: string; kdf: KdfType; kdfIterations: number; - referenceData: ReferenceEventData; + referenceData: ReferenceEventRequest; constructor(email: string, name: string, masterPasswordHash: string, masterPasswordHint: string, key: string, - kdf: KdfType, kdfIterations: number, referenceData: ReferenceEventData) { + kdf: KdfType, kdfIterations: number, referenceData: ReferenceEventRequest) { this.name = name; this.email = email; this.masterPasswordHash = masterPasswordHash; From 6e79dfa01a785fb239c7f0026dcd427db981eb55 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Mon, 20 Jul 2020 15:38:56 -0400 Subject: [PATCH 1016/1626] fixed import groupings/order --- src/angular/components/register.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index 0ba73829d2..b3f689fcd0 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -1,6 +1,7 @@ import { Router } from '@angular/router'; import { KeysRequest } from '../../models/request/keysRequest'; +import { ReferenceEventRequest } from '../../models/request/referenceEventRequest'; import { RegisterRequest } from '../../models/request/registerRequest'; import { ApiService } from '../../abstractions/api.service'; @@ -12,7 +13,6 @@ import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { StateService } from '../../abstractions/state.service'; import { KdfType } from '../../enums/kdfType'; -import { ReferenceEventRequest } from '../../models/request/referenceEventRequest'; export class RegisterComponent { name: string = ''; From 46551df51fa0517c9d9a5d163238de0333ec9551 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Mon, 20 Jul 2020 17:48:27 -0500 Subject: [PATCH 1017/1626] added org id to the bulk delete request model --- src/models/request/cipherBulkDeleteRequest.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/models/request/cipherBulkDeleteRequest.ts b/src/models/request/cipherBulkDeleteRequest.ts index fb19523a20..97fab41b46 100644 --- a/src/models/request/cipherBulkDeleteRequest.ts +++ b/src/models/request/cipherBulkDeleteRequest.ts @@ -1,7 +1,9 @@ export class CipherBulkDeleteRequest { ids: string[]; + organizationId: string; - constructor(ids: string[]) { + constructor(ids: string[], organizationId?: string) { this.ids = ids == null ? [] : ids; + this.organizationId = organizationId; } } From c62f5287cd259c6385c6e79193e0e5e1746c7a3c Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Thu, 23 Jul 2020 19:32:20 +0200 Subject: [PATCH 1018/1626] Desktop biometrics support (#119) * Initial work on windows hello support * Switch to use windows.security.credentials.ui UserConsentVerifier * Fix linting warnings * Remove unessesary supportsBiometric from lock screen * Rename biometric.main to windows.biometric.main. Add abstraction for biometric. * Add support for dynamic biometric text. * Add untested darwin implementation * Rename fingerprintUnlock to biometric * Add new functions to cliPlatformUtils.service.ts. * Hide login if biometric is not supported * Export default for biometric.*.main.ts * Remove @nodert-win10-rs4/windows.security.credentials * Add build requirements to readme * Auto prompt biometric when starting the application. * Ensure we support biometric before trying to auto prompt. * Fix review comments and linting errors --- README.md | 11 ++ package-lock.json | 15 +++ package.json | 1 + src/abstractions/biometric.main.ts | 5 + src/abstractions/platformUtils.service.ts | 2 + src/abstractions/vaultTimeout.service.ts | 2 + src/angular/components/lock.component.ts | 25 +++++ src/angular/services/auth-guard.service.ts | 2 +- src/cli/services/cliPlatformUtils.service.ts | 8 ++ src/electron/biometric.darwin.main.ts | 32 ++++++ src/electron/biometric.windows.main.ts | 46 ++++++++ src/electron/electronConstants.ts | 1 + .../services/electronPlatformUtils.service.ts | 18 +++- src/globals.d.ts | 101 ++++++++++++++++++ src/services/auth.service.ts | 4 +- src/services/constants.service.ts | 4 + src/services/crypto.service.ts | 6 +- src/services/vaultTimeout.service.ts | 21 ++++ tsconfig.json | 2 +- 19 files changed, 300 insertions(+), 6 deletions(-) create mode 100644 src/abstractions/biometric.main.ts create mode 100644 src/electron/biometric.darwin.main.ts create mode 100644 src/electron/biometric.windows.main.ts diff --git a/README.md b/README.md index d376564923..5096a355f4 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,14 @@ # Bitwarden JavaScript Library Common code referenced across Bitwarden JavaScript projects. + +## Requirements + +* Git +* node-gyp + +### Windows + +* *Microsoft Build Tools 2015* in Visual Studio Installer +* [Windows 10 SDK 17134](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive/) +either by downloading it seperately or through the Visual Studio Installer. diff --git a/package-lock.json b/package-lock.json index 95111a5b5a..b36e8bd6fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -254,6 +254,21 @@ "msgpack5": "^4.0.2" } }, + "@nodert-win10-rs4/windows.security.credentials.ui": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@nodert-win10-rs4/windows.security.credentials.ui/-/windows.security.credentials.ui-0.4.4.tgz", + "integrity": "sha512-P+EsJw5MCQXTxp7mwXfNDvIzIYsB6ple+HNg01QjPWg/PJfAodPuxL6XM7l0sPtYHsDYnfnvoefZMdZRa2Z1ig==", + "requires": { + "nan": "^2.14.1" + }, + "dependencies": { + "nan": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==" + } + } + }, "@types/commander": { "version": "2.12.2", "resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.2.tgz", diff --git a/package.json b/package.json index 5fec1f5667..71bf68e843 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "@angular/upgrade": "7.2.1", "@microsoft/signalr": "3.1.0", "@microsoft/signalr-protocol-msgpack": "3.1.0", + "@nodert-win10-rs4/windows.security.credentials.ui": "^0.4.4", "big-integer": "1.6.36", "chalk": "2.4.1", "commander": "2.18.0", diff --git a/src/abstractions/biometric.main.ts b/src/abstractions/biometric.main.ts new file mode 100644 index 0000000000..0f85cc1f55 --- /dev/null +++ b/src/abstractions/biometric.main.ts @@ -0,0 +1,5 @@ +export abstract class BiometricMain { + init: () => Promise; + supportsBiometric: () => Promise; + requestCreate: () => Promise; +} diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index 070abf0027..f40e07504f 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -32,4 +32,6 @@ export abstract class PlatformUtilsService { isSelfHost: () => boolean; copyToClipboard: (text: string, options?: any) => void; readFromClipboard: (options?: any) => Promise; + supportsBiometric: () => Promise; + authenticateBiometric: () => Promise; } diff --git a/src/abstractions/vaultTimeout.service.ts b/src/abstractions/vaultTimeout.service.ts index 4c0ad3d0d2..bd8ff2f743 100644 --- a/src/abstractions/vaultTimeout.service.ts +++ b/src/abstractions/vaultTimeout.service.ts @@ -1,6 +1,7 @@ import { CipherString } from '../models/domain/cipherString'; export abstract class VaultTimeoutService { + biometricLocked: boolean; pinProtectedKey: CipherString; isLocked: () => Promise; checkVaultTimeout: () => Promise; @@ -8,5 +9,6 @@ export abstract class VaultTimeoutService { logOut: () => Promise; setVaultTimeoutOptions: (vaultTimeout: number, vaultTimeoutAction: string) => Promise; isPinLockSet: () => Promise<[boolean, boolean]>; + isBiometricLockSet: () => Promise; clear: () => Promise; } diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 0cf22362c3..85b0b2b167 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -1,5 +1,6 @@ import { OnInit } from '@angular/core'; import { Router } from '@angular/router'; +import { first } from 'rxjs/operators'; import { ApiService } from '../../abstractions/api.service'; import { CryptoService } from '../../abstractions/crypto.service'; @@ -29,6 +30,9 @@ export class LockComponent implements OnInit { pinLock: boolean = false; webVaultHostname: string = ''; formPromise: Promise; + supportsBiometric: boolean; + biometricLock: boolean; + biometricText: string; protected successRoute: string = 'vault'; protected onSuccessfulSubmit: () => void; @@ -46,12 +50,20 @@ export class LockComponent implements OnInit { async ngOnInit() { this.pinSet = await this.vaultTimeoutService.isPinLockSet(); this.pinLock = (this.pinSet[0] && this.vaultTimeoutService.pinProtectedKey != null) || this.pinSet[1]; + this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); + this.biometricLock = await this.vaultTimeoutService.isBiometricLockSet(); + this.biometricText = await this.storageService.get(ConstantsService.biometricText); this.email = await this.userService.getEmail(); let vaultUrl = this.environmentService.getWebVaultUrl(); if (vaultUrl == null) { vaultUrl = 'https://bitwarden.com'; } this.webVaultHostname = Utils.getHostname(vaultUrl); + this.router.routerState.root.queryParams.pipe(first()).subscribe((params) => { + if (this.supportsBiometric && params.promptBiometric) { + this.unlockBiometric(); + } + }); } async submit() { @@ -146,6 +158,18 @@ export class LockComponent implements OnInit { } } + async unlockBiometric() { + if (!this.biometricLock) { + return; + } + const success = await this.platformUtilsService.authenticateBiometric(); + + this.vaultTimeoutService.biometricLocked = !success; + if (success) { + await this.doContinue(); + } + } + togglePassword() { this.platformUtilsService.eventTrack('Toggled Master Password on Unlock'); this.showPassword = !this.showPassword; @@ -158,6 +182,7 @@ export class LockComponent implements OnInit { } private async doContinue() { + this.vaultTimeoutService.biometricLocked = false; const disableFavicon = await this.storageService.get(ConstantsService.disableFaviconKey); await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon); this.messagingService.send('unlocked'); diff --git a/src/angular/services/auth-guard.service.ts b/src/angular/services/auth-guard.service.ts index 0401c37875..b459d62053 100644 --- a/src/angular/services/auth-guard.service.ts +++ b/src/angular/services/auth-guard.service.ts @@ -27,7 +27,7 @@ export class AuthGuardService implements CanActivate { if (routerState != null) { this.messagingService.send('lockedUrl', { url: routerState.url }); } - this.router.navigate(['lock']); + this.router.navigate(['lock'], { queryParams: { promptBiometric: true }}); return false; } diff --git a/src/cli/services/cliPlatformUtils.service.ts b/src/cli/services/cliPlatformUtils.service.ts index d7564c9b84..23a0aa01d0 100644 --- a/src/cli/services/cliPlatformUtils.service.ts +++ b/src/cli/services/cliPlatformUtils.service.ts @@ -129,4 +129,12 @@ export class CliPlatformUtilsService implements PlatformUtilsService { readFromClipboard(options?: any): Promise { throw new Error('Not implemented.'); } + + supportsBiometric(): Promise { + return Promise.resolve(false); + } + + authenticateBiometric(): Promise { + return Promise.resolve(false); + } } diff --git a/src/electron/biometric.darwin.main.ts b/src/electron/biometric.darwin.main.ts new file mode 100644 index 0000000000..ab449654ef --- /dev/null +++ b/src/electron/biometric.darwin.main.ts @@ -0,0 +1,32 @@ +import { I18nService, StorageService } from '../abstractions'; + +import { ipcMain, systemPreferences } from 'electron'; +import { BiometricMain } from '../abstractions/biometric.main'; +import { ConstantsService } from '../services'; +import { ElectronConstants } from './electronConstants'; + +export default class BiometricDarwinMain implements BiometricMain { + constructor(private storageService: StorageService, private i18nservice: I18nService) {} + + async init() { + this.storageService.save(ElectronConstants.enableBiometric, await this.supportsBiometric()); + this.storageService.save(ConstantsService.biometricText, 'unlockWithTouchId'); + + ipcMain.on('biometric', async (event: any, message: any) => { + event.returnValue = await this.requestCreate(); + }); + } + + supportsBiometric(): Promise { + return Promise.resolve(systemPreferences.canPromptTouchID()); + } + + async requestCreate(): Promise { + try { + await systemPreferences.promptTouchID(this.i18nservice.t('touchIdConsentMessage')); + return true; + } catch { + return false; + } + } +} diff --git a/src/electron/biometric.windows.main.ts b/src/electron/biometric.windows.main.ts new file mode 100644 index 0000000000..5a0d089965 --- /dev/null +++ b/src/electron/biometric.windows.main.ts @@ -0,0 +1,46 @@ +import * as util from 'util'; + +import { + UserConsentVerificationResult, + UserConsentVerifier, + UserConsentVerifierAvailability, +} from '@nodert-win10-rs4/windows.security.credentials.ui'; +import { I18nService, StorageService } from '../abstractions'; + +import { ipcMain } from 'electron'; +import { BiometricMain } from '../abstractions/biometric.main'; +import { ConstantsService } from '../services'; +import { ElectronConstants } from './electronConstants'; + +const requestVerification: any = util.promisify(UserConsentVerifier.requestVerificationAsync); +const checkAvailability: any = util.promisify(UserConsentVerifier.checkAvailabilityAsync); + +const AllowedAvailabilities = [ + UserConsentVerifierAvailability.available, + UserConsentVerifierAvailability.deviceBusy, +]; + +export default class BiometricWindowsMain implements BiometricMain { + constructor(private storageService: StorageService, private i18nservice: I18nService) {} + + async init() { + this.storageService.save(ElectronConstants.enableBiometric, await this.supportsBiometric()); + this.storageService.save(ConstantsService.biometricText, 'unlockWithWindowsHello'); + + ipcMain.on('biometric', async (event: any, message: any) => { + event.returnValue = await this.requestCreate(); + }); + } + + async supportsBiometric(): Promise { + const availability = await checkAvailability(); + + return AllowedAvailabilities.includes(availability); + } + + async requestCreate(): Promise { + const verification = await requestVerification(this.i18nservice.t('windowsHelloConsentMessage')); + + return verification === UserConsentVerificationResult.verified; + } +} diff --git a/src/electron/electronConstants.ts b/src/electron/electronConstants.ts index f444b0a1d8..8d88ef6b52 100644 --- a/src/electron/electronConstants.ts +++ b/src/electron/electronConstants.ts @@ -5,4 +5,5 @@ export class ElectronConstants { static readonly enableStartToTrayKey: string = 'enableStartToTrayKey'; static readonly enableAlwaysOnTopKey: string = 'enableAlwaysOnTopKey'; static readonly minimizeOnCopyToClipboardKey: string = 'minimizeOnCopyToClipboardKey'; + static readonly enableBiometric: string = 'enabledBiometric'; } diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index ad08b170e5..dcefdc8883 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -1,5 +1,6 @@ import { clipboard, + ipcRenderer, remote, shell, } from 'electron'; @@ -15,8 +16,10 @@ import { DeviceType } from '../../enums/deviceType'; import { I18nService } from '../../abstractions/i18n.service'; import { MessagingService } from '../../abstractions/messaging.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { StorageService } from '../../abstractions/storage.service'; import { AnalyticsIds } from '../../misc/analytics'; +import { ElectronConstants } from '../electronConstants'; export class ElectronPlatformUtilsService implements PlatformUtilsService { identityClientId: string; @@ -25,7 +28,7 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { private analyticsIdCache: string = null; constructor(private i18nService: I18nService, private messagingService: MessagingService, - private isDesktopApp: boolean) { + private isDesktopApp: boolean, private storageService: StorageService) { this.identityClientId = isDesktopApp ? 'desktop' : 'connector'; } @@ -203,4 +206,17 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { const type = options ? options.type : null; return Promise.resolve(clipboard.readText(type)); } + + supportsBiometric(): Promise { + return this.storageService.get(ElectronConstants.enableBiometric); + } + + authenticateBiometric(): Promise { + return new Promise((resolve) => { + const val = ipcRenderer.sendSync('biometric', { + action: 'authenticate', + }); + resolve(val); + }); + } } diff --git a/src/globals.d.ts b/src/globals.d.ts index 8116ab173f..4c123c15a8 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -1,3 +1,104 @@ declare function escape(s: string): string; declare function unescape(s: string): string; declare module 'duo_web_sdk'; + +/* tslint:disable */ +declare module '@nodert-win10-rs4/windows.security.credentials.ui' { + export enum AuthenticationProtocol { + basic, + digest, + ntlm, + kerberos, + negotiate, + credSsp, + custom, + } + + export enum CredentialSaveOption { + unselected, + selected, + hidden, + } + + export enum UserConsentVerifierAvailability { + available, + deviceNotPresent, + notConfiguredForUser, + disabledByPolicy, + deviceBusy, + } + + export enum UserConsentVerificationResult { + verified, + deviceNotPresent, + notConfiguredForUser, + disabledByPolicy, + deviceBusy, + retriesExhausted, + canceled, + } + + export class CredentialPickerOptions { + targetName: String; + previousCredential: Object; + message: String; + errorCode: Number; + customAuthenticationProtocol: String; + credentialSaveOption: CredentialSaveOption; + caption: String; + callerSavesCredential: Boolean; + authenticationProtocol: AuthenticationProtocol; + alwaysDisplayDialog: Boolean; + constructor(); + } + + export class CredentialPickerResults { + credential: Object; + credentialDomainName: String; + credentialPassword: String; + credentialSaveOption: CredentialSaveOption; + credentialSaved: Boolean; + credentialUserName: String; + errorCode: Number; + constructor(); + } + + export class CredentialPicker { + constructor(); + + static pickAsync( + options: CredentialPickerOptions, + callback: (error: Error, result: CredentialPickerResults) => void + ): void; + static pickAsync( + targetName: String, + message: String, + callback: (error: Error, result: CredentialPickerResults) => void + ): void; + static pickAsync( + targetName: String, + message: String, + caption: String, + callback: (error: Error, result: CredentialPickerResults) => void + ): void; + } + + export class UserConsentVerifier { + constructor(); + + static checkAvailabilityAsync( + callback: ( + error: Error, + result: UserConsentVerifierAvailability + ) => void + ): void; + + static requestVerificationAsync( + message: String, + callback: ( + error: Error, + result: UserConsentVerificationResult + ) => void + ): void; + } +} diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 9a8d0ec3e0..16025dc088 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -22,6 +22,7 @@ import { MessagingService } from '../abstractions/messaging.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { TokenService } from '../abstractions/token.service'; import { UserService } from '../abstractions/user.service'; +import { VaultTimeoutService } from '../abstractions/vaultTimeout.service'; export const TwoFactorProviders = { [TwoFactorProviderType.Authenticator]: { @@ -89,7 +90,7 @@ export class AuthService implements AuthServiceAbstraction { private userService: UserService, private tokenService: TokenService, private appIdService: AppIdService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private messagingService: MessagingService, - private setCryptoKeys = true) { } + private vaultTimeoutService: VaultTimeoutService, private setCryptoKeys = true) { } init() { TwoFactorProviders[TwoFactorProviderType.Email].name = this.i18nService.t('emailTitle'); @@ -317,6 +318,7 @@ export class AuthService implements AuthServiceAbstraction { await this.cryptoService.setEncPrivateKey(tokenResponse.privateKey); } + this.vaultTimeoutService.biometricLocked = false; this.messagingService.send('loggedIn'); return result; } diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index 752648fd79..7e077933d4 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -25,6 +25,8 @@ export class ConstantsService { static readonly eventCollectionKey: string = 'eventCollection'; static readonly ssoCodeVerifierKey: string = 'ssoCodeVerifier'; static readonly ssoStateKey: string = 'ssoState'; + static readonly biometricUnlockKey: string = 'biometric'; + static readonly biometricText: string = 'biometricText'; readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; readonly disableGaKey: string = ConstantsService.disableGaKey; @@ -51,4 +53,6 @@ export class ConstantsService { readonly eventCollectionKey: string = ConstantsService.eventCollectionKey; readonly ssoCodeVerifierKey: string = ConstantsService.ssoCodeVerifierKey; readonly ssoStateKey: string = ConstantsService.ssoStateKey; + readonly biometricUnlockKey: string = ConstantsService.biometricUnlockKey; + readonly biometricText: string = ConstantsService.biometricText; } diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 5fa86444e3..2037a57d85 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -42,7 +42,8 @@ export class CryptoService implements CryptoServiceAbstraction { this.key = key; const option = await this.storageService.get(ConstantsService.vaultTimeoutKey); - if (option != null) { + const biometric = await this.storageService.get(ConstantsService.biometricUnlockKey); + if (option != null && !biometric) { // if we have a lock option set, we do not store the key return; } @@ -291,7 +292,8 @@ export class CryptoService implements CryptoServiceAbstraction { async toggleKey(): Promise { const key = await this.getKey(); const option = await this.storageService.get(ConstantsService.vaultTimeoutKey); - if (option != null || option === 0) { + const biometric = await this.storageService.get(ConstantsService.biometricUnlockKey); + if (!biometric && (option != null || option === 0)) { // if we have a lock option set, clear the key await this.clearKey(); this.key = key; diff --git a/src/services/vaultTimeout.service.ts b/src/services/vaultTimeout.service.ts index 2601dc5454..f801b554b8 100644 --- a/src/services/vaultTimeout.service.ts +++ b/src/services/vaultTimeout.service.ts @@ -16,6 +16,7 @@ import { CipherString } from '../models/domain/cipherString'; export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { pinProtectedKey: CipherString = null; + biometricLocked: boolean = true; private inited = false; @@ -42,6 +43,11 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { // Keys aren't stored for a device that is locked or logged out. async isLocked(): Promise { const hasKey = await this.cryptoService.hasKey(); + if (hasKey) { + if (await this.isBiometricLockSet() && this.biometricLocked) { + return true; + } + } return !hasKey; } @@ -91,6 +97,17 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { return; } + if (allowSoftLock) { + const biometricLocked = await this.isBiometricLockSet(); + if (biometricLocked) { + this.messagingService.send('locked'); + if (this.lockedCallback != null) { + await this.lockedCallback(); + } + return; + } + } + await Promise.all([ this.cryptoService.clearKey(), this.cryptoService.clearOrgKeys(true), @@ -127,6 +144,10 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { return [protectedPin != null, pinProtectedKey != null]; } + async isBiometricLockSet(): Promise { + return await this.storageService.get(ConstantsService.biometricUnlockKey); + } + clear(): Promise { this.pinProtectedKey = null; return this.storageService.remove(ConstantsService.protectedPin); diff --git a/tsconfig.json b/tsconfig.json index 7ed555a337..960333221f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ "noImplicitAny": true, "target": "ES6", "module": "commonjs", - "lib": ["es5", "es6", "dom"], + "lib": ["es5", "es6", "es7", "dom"], "sourceMap": true, "declaration": true, "allowSyntheticDefaultImports": true, From f7ae8532b9afc8937e24e3e0f4ca653b47f859dc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 23 Jul 2020 17:02:59 -0400 Subject: [PATCH 1019/1626] build with github actions --- .github/workflows/build.yml | 55 +++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000..8f5cb8a927 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,55 @@ +name: Build + +on: push + +jobs: + cloc: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v2 + + - name: Set up cloc + run: | + sudo apt-get update + sudo apt-get -y install cloc + + - name: Print lines of code + run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git + + build: + + runs-on: windows-latest + + steps: + - name: Set up Node + uses: actions/setup-node@v1 + with: + node-version: '10.x' + + - name: Print environment + run: | + node --version + npm --version + + - name: Checkout repo + uses: actions/checkout@v2 + + - name: Install Node dependencies + run: npm install + + - name: Run linter + run: npm run lint + + - name: Build + run: npm run build + + - name: Run tests + run: npm run test + + - name: Upload test coverage + uses: actions/upload-artifact@v2-preview + with: + name: test-coverage + path: coverage/ From 1fb95726ef2d0578c1e632e155dfb16552594c39 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 23 Jul 2020 17:13:57 -0400 Subject: [PATCH 1020/1626] fix build and run on os matrix --- .github/workflows/build.yml | 8 ++++++-- spec/support/karma.conf.js | 8 +++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8f5cb8a927..aca57ec45b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,11 @@ jobs: build: - runs-on: windows-latest + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [windows-latest, macos-latest, ubuntu-latest] steps: - name: Set up Node @@ -48,7 +52,7 @@ jobs: - name: Run tests run: npm run test - - name: Upload test coverage + - name: Upload test coverage artifact uses: actions/upload-artifact@v2-preview with: name: test-coverage diff --git a/spec/support/karma.conf.js b/spec/support/karma.conf.js index 0e3870d8b9..8ba0ec4170 100644 --- a/spec/support/karma.conf.js +++ b/spec/support/karma.conf.js @@ -74,9 +74,15 @@ module.exports = (config) => { removeBrowser('Opera'); removeBrowser('SafariTechPreview'); - if (process.env.APPVEYOR === 'True') { + var ci = process.env.CI === 'True' || process.env.CI === 'true'; + var githubAction = process.env.GITHUB_WORKFLOW != null && process.env.GITHUB_WORKFLOW !== ''; + var appveyor = process.env.APPVEYOR === 'True'; + if (githubAction || appveyor) { removeBrowser('Edge'); } + if (githubAction) { + removeBrowser('Firefox'); + } return result; } From cb966c0de8c7d44da6e4c493c3383960728576d4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 23 Jul 2020 17:17:42 -0400 Subject: [PATCH 1021/1626] dont run headless ubuntu --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aca57ec45b..acd6351490 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: strategy: matrix: - os: [windows-latest, macos-latest, ubuntu-latest] + os: [windows-latest, macos-latest] steps: - name: Set up Node From 566e88f52a015f7d1328e56ec45b610674d8c2d3 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 23 Jul 2020 17:23:38 -0400 Subject: [PATCH 1022/1626] only run chrome tests on github action --- spec/support/karma.conf.js | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/support/karma.conf.js b/spec/support/karma.conf.js index 8ba0ec4170..4ec04a07df 100644 --- a/spec/support/karma.conf.js +++ b/spec/support/karma.conf.js @@ -82,6 +82,7 @@ module.exports = (config) => { } if (githubAction) { removeBrowser('Firefox'); + removeBrowser('Safari'); } return result; From 1f1dfec0fe926f40da05a522dff1364ec8b76b8c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 23 Jul 2020 17:23:59 -0400 Subject: [PATCH 1023/1626] move prompy on init to desktop (#135) --- src/angular/components/lock.component.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 85b0b2b167..0f4b901a0b 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -1,6 +1,5 @@ import { OnInit } from '@angular/core'; import { Router } from '@angular/router'; -import { first } from 'rxjs/operators'; import { ApiService } from '../../abstractions/api.service'; import { CryptoService } from '../../abstractions/crypto.service'; @@ -59,11 +58,6 @@ export class LockComponent implements OnInit { vaultUrl = 'https://bitwarden.com'; } this.webVaultHostname = Utils.getHostname(vaultUrl); - this.router.routerState.root.queryParams.pipe(first()).subscribe((params) => { - if (this.supportsBiometric && params.promptBiometric) { - this.unlockBiometric(); - } - }); } async submit() { From fe167beda9aa87b792fde2aa05db9cf167388f9a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 24 Jul 2020 09:49:59 -0400 Subject: [PATCH 1024/1626] CI for ubuntu as well --- .github/workflows/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index acd6351490..b399438717 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: strategy: matrix: - os: [windows-latest, macos-latest] + os: [windows-latest, macos-latest, ubuntu-latest] steps: - name: Set up Node @@ -50,9 +50,11 @@ jobs: run: npm run build - name: Run tests + if: runner.os != 'Linux' run: npm run test - name: Upload test coverage artifact + if: runner.os != 'Linux' uses: actions/upload-artifact@v2-preview with: name: test-coverage From 261a20031fdaf63d94587d849a162dca28d82783 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 31 Jul 2020 11:55:14 -0400 Subject: [PATCH 1025/1626] suth service support for complete sso login (#136) --- src/abstractions/auth.service.ts | 2 ++ src/services/auth.service.ts | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/src/abstractions/auth.service.ts b/src/abstractions/auth.service.ts index 21bba19633..7a7b23f73a 100644 --- a/src/abstractions/auth.service.ts +++ b/src/abstractions/auth.service.ts @@ -18,6 +18,8 @@ export abstract class AuthService { remember?: boolean) => Promise; logInComplete: (email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean) => Promise; + logInSsoComplete: (code: string, codeVerifier: string, redirectUrl: string, + twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean) => Promise; logOut: (callback: Function) => void; getSupportedTwoFactorProviders: (win: Window) => any[]; getDefaultTwoFactorProvider: (u2fSupported: boolean) => TwoFactorProviderType; diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 16025dc088..3ceb1b84c3 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -142,6 +142,13 @@ export class AuthService implements AuthServiceAbstraction { remember); } + async logInSsoComplete(code: string, codeVerifier: string, redirectUrl: string, + twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean): Promise { + this.selectedTwoFactorProviderType = null; + return await this.logInHelper(null, null, code, codeVerifier, redirectUrl, null, + twoFactorProvider, twoFactorToken, remember); + } + logOut(callback: Function) { callback(); this.messagingService.send('loggedOut'); From b2fbc475c717c8f76aff1e4d918c3c616cfec44c Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Fri, 31 Jul 2020 19:42:13 -0400 Subject: [PATCH 1026/1626] try catch nodeRT module via require --- src/abstractions/biometric.main.ts | 1 + src/electron/biometric.darwin.main.ts | 2 + src/electron/biometric.windows.main.ts | 97 +++++++++++++++++++----- src/globals.d.ts | 101 ------------------------- 4 files changed, 79 insertions(+), 122 deletions(-) diff --git a/src/abstractions/biometric.main.ts b/src/abstractions/biometric.main.ts index 0f85cc1f55..eff805ea32 100644 --- a/src/abstractions/biometric.main.ts +++ b/src/abstractions/biometric.main.ts @@ -1,4 +1,5 @@ export abstract class BiometricMain { + isError: boolean; init: () => Promise; supportsBiometric: () => Promise; requestCreate: () => Promise; diff --git a/src/electron/biometric.darwin.main.ts b/src/electron/biometric.darwin.main.ts index ab449654ef..11ebc23565 100644 --- a/src/electron/biometric.darwin.main.ts +++ b/src/electron/biometric.darwin.main.ts @@ -6,6 +6,8 @@ import { ConstantsService } from '../services'; import { ElectronConstants } from './electronConstants'; export default class BiometricDarwinMain implements BiometricMain { + isError: boolean = false; + constructor(private storageService: StorageService, private i18nservice: I18nService) {} async init() { diff --git a/src/electron/biometric.windows.main.ts b/src/electron/biometric.windows.main.ts index 5a0d089965..c8948bd4f1 100644 --- a/src/electron/biometric.windows.main.ts +++ b/src/electron/biometric.windows.main.ts @@ -1,10 +1,3 @@ -import * as util from 'util'; - -import { - UserConsentVerificationResult, - UserConsentVerifier, - UserConsentVerifierAvailability, -} from '@nodert-win10-rs4/windows.security.credentials.ui'; import { I18nService, StorageService } from '../abstractions'; import { ipcMain } from 'electron'; @@ -12,19 +5,23 @@ import { BiometricMain } from '../abstractions/biometric.main'; import { ConstantsService } from '../services'; import { ElectronConstants } from './electronConstants'; -const requestVerification: any = util.promisify(UserConsentVerifier.requestVerificationAsync); -const checkAvailability: any = util.promisify(UserConsentVerifier.checkAvailabilityAsync); - -const AllowedAvailabilities = [ - UserConsentVerifierAvailability.available, - UserConsentVerifierAvailability.deviceBusy, -]; - export default class BiometricWindowsMain implements BiometricMain { - constructor(private storageService: StorageService, private i18nservice: I18nService) {} + isError: boolean = false; + + private windowsSecurityCredentialsUiModule: any; + + constructor(private storageService: StorageService, private i18nservice: I18nService) { } async init() { - this.storageService.save(ElectronConstants.enableBiometric, await this.supportsBiometric()); + this.windowsSecurityCredentialsUiModule = this.getWindowsSecurityCredentialsUiModule(); + let supportsBiometric = false; + try { + supportsBiometric = await this.supportsBiometric(); + } catch { + // store error state so we can let the user know on the settings page + this.isError = true; + } + this.storageService.save(ElectronConstants.enableBiometric, supportsBiometric); this.storageService.save(ConstantsService.biometricText, 'unlockWithWindowsHello'); ipcMain.on('biometric', async (event: any, message: any) => { @@ -33,14 +30,72 @@ export default class BiometricWindowsMain implements BiometricMain { } async supportsBiometric(): Promise { - const availability = await checkAvailability(); + const availability = await this.checkAvailabilityAsync(); - return AllowedAvailabilities.includes(availability); + return this.getAllowedAvailabilities().includes(availability); } async requestCreate(): Promise { - const verification = await requestVerification(this.i18nservice.t('windowsHelloConsentMessage')); + const module = this.getWindowsSecurityCredentialsUiModule(); + if (module == null) { + return false; + } - return verification === UserConsentVerificationResult.verified; + const verification = await this.requestVerificationAsync(this.i18nservice.t('windowsHelloConsentMessage')); + + return verification === module.UserConsentVerificationResult.verified; + } + + getWindowsSecurityCredentialsUiModule(): any { + try { + return null; + /* + return this._windowsSecurityCredentialsUiModule || + require('@nodert-win10-rs4/windows.security.credentials.ui'); + */ + } catch { + this.isError = true; + } + } + + async checkAvailabilityAsync(): Promise { + const module = this.getWindowsSecurityCredentialsUiModule(); + if (module != null) { + return new Promise((resolve, reject) => { + module.UserConsentVerifier.checkAvailabilityAsync((error: Error, result: any) => { + if (error) { + return resolve(null); + } + return resolve(result); + }); + }); + } + return Promise.resolve(null); + } + + async requestVerificationAsync(message: string): Promise { + const module = this.getWindowsSecurityCredentialsUiModule(); + if (module != null) { + return new Promise((resolve, reject) => { + module.UserConsentVerifier.requestVerificationAsync(message, (error: Error, result: any) => { + if (error) { + return resolve(null); + } + return resolve(result); + }); + }); + } + return Promise.resolve(null); + } + + getAllowedAvailabilities(): any[] { + const module = this.getWindowsSecurityCredentialsUiModule(); + if (module != null) { + return [ + module.UserConsentVerifierAvailability.available, + module.UserConsentVerifierAvailability.deviceBusy, + ]; + } + return []; } } diff --git a/src/globals.d.ts b/src/globals.d.ts index 4c123c15a8..8116ab173f 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -1,104 +1,3 @@ declare function escape(s: string): string; declare function unescape(s: string): string; declare module 'duo_web_sdk'; - -/* tslint:disable */ -declare module '@nodert-win10-rs4/windows.security.credentials.ui' { - export enum AuthenticationProtocol { - basic, - digest, - ntlm, - kerberos, - negotiate, - credSsp, - custom, - } - - export enum CredentialSaveOption { - unselected, - selected, - hidden, - } - - export enum UserConsentVerifierAvailability { - available, - deviceNotPresent, - notConfiguredForUser, - disabledByPolicy, - deviceBusy, - } - - export enum UserConsentVerificationResult { - verified, - deviceNotPresent, - notConfiguredForUser, - disabledByPolicy, - deviceBusy, - retriesExhausted, - canceled, - } - - export class CredentialPickerOptions { - targetName: String; - previousCredential: Object; - message: String; - errorCode: Number; - customAuthenticationProtocol: String; - credentialSaveOption: CredentialSaveOption; - caption: String; - callerSavesCredential: Boolean; - authenticationProtocol: AuthenticationProtocol; - alwaysDisplayDialog: Boolean; - constructor(); - } - - export class CredentialPickerResults { - credential: Object; - credentialDomainName: String; - credentialPassword: String; - credentialSaveOption: CredentialSaveOption; - credentialSaved: Boolean; - credentialUserName: String; - errorCode: Number; - constructor(); - } - - export class CredentialPicker { - constructor(); - - static pickAsync( - options: CredentialPickerOptions, - callback: (error: Error, result: CredentialPickerResults) => void - ): void; - static pickAsync( - targetName: String, - message: String, - callback: (error: Error, result: CredentialPickerResults) => void - ): void; - static pickAsync( - targetName: String, - message: String, - caption: String, - callback: (error: Error, result: CredentialPickerResults) => void - ): void; - } - - export class UserConsentVerifier { - constructor(); - - static checkAvailabilityAsync( - callback: ( - error: Error, - result: UserConsentVerifierAvailability - ) => void - ): void; - - static requestVerificationAsync( - message: String, - callback: ( - error: Error, - result: UserConsentVerificationResult - ) => void - ): void; - } -} From c05dbe9743e8c7d28a51179f4a0f643a6b4e8483 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Fri, 31 Jul 2020 19:54:38 -0400 Subject: [PATCH 1027/1626] remove trailing whitespace --- src/electron/biometric.windows.main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/electron/biometric.windows.main.ts b/src/electron/biometric.windows.main.ts index c8948bd4f1..88deb5f051 100644 --- a/src/electron/biometric.windows.main.ts +++ b/src/electron/biometric.windows.main.ts @@ -7,7 +7,7 @@ import { ElectronConstants } from './electronConstants'; export default class BiometricWindowsMain implements BiometricMain { isError: boolean = false; - + private windowsSecurityCredentialsUiModule: any; constructor(private storageService: StorageService, private i18nservice: I18nService) { } From 8d01ec7e42b46e75f6d1f92b2fbdab319bf576f8 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Fri, 31 Jul 2020 19:59:31 -0400 Subject: [PATCH 1028/1626] removed temporary test code --- src/electron/biometric.windows.main.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/electron/biometric.windows.main.ts b/src/electron/biometric.windows.main.ts index 88deb5f051..84ae41ae83 100644 --- a/src/electron/biometric.windows.main.ts +++ b/src/electron/biometric.windows.main.ts @@ -48,14 +48,14 @@ export default class BiometricWindowsMain implements BiometricMain { getWindowsSecurityCredentialsUiModule(): any { try { - return null; - /* - return this._windowsSecurityCredentialsUiModule || - require('@nodert-win10-rs4/windows.security.credentials.ui'); - */ + if (this.windowsSecurityCredentialsUiModule == null) { + this.windowsSecurityCredentialsUiModule = require('@nodert-win10-rs4/windows.security.credentials.ui'); + } + return this.windowsSecurityCredentialsUiModule; } catch { this.isError = true; } + return null; } async checkAvailabilityAsync(): Promise { From f301b92dc3f2df68335ca4a9de6511c0b3452af4 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Sat, 1 Aug 2020 08:42:24 -0500 Subject: [PATCH 1029/1626] [SSO] Merge feature/sso into master (#139) * [SSO] Reset Master Password (#134) * Initial commit of reset master password (sso) * Updated line length error * Updated import line again * Added trailing comma * restored reference data for RegisterRequest * Updated tracking boolean name // added success route update based on passed boolean * Added new API // reverted Register // deleted reset // added change pw and sso * Changed redirect URI to protected to override in sub-class * Updated api to setPassword // Updated request model name // Updated change password refs // Updated formatting * Encoded necessary parts of authorize url // Added default catch error message * Refactored methods inside change password base component // removed unnecesary query param for sso * [lint] Fixed error (#137) * Cleaned lint error * Fixed sso lint error --- src/abstractions/api.service.ts | 2 + .../components/change-password.component.ts | 157 ++++++++++++++++++ src/angular/components/sso.component.ts | 125 ++++++++++++++ .../components/two-factor.component.ts | 3 +- src/models/request/setPasswordRequest.ts | 4 + src/services/api.service.ts | 5 + 6 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 src/angular/components/change-password.component.ts create mode 100644 src/angular/components/sso.component.ts create mode 100644 src/models/request/setPasswordRequest.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index b345e74fce..81a687b6b1 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -42,6 +42,7 @@ import { PreloginRequest } from '../models/request/preloginRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { SeatRequest } from '../models/request/seatRequest'; import { SelectionReadOnlyRequest } from '../models/request/selectionReadOnlyRequest'; +import { SetPasswordRequest } from '../models/request/setPasswordRequest'; import { StorageRequest } from '../models/request/storageRequest'; import { TaxInfoUpdateRequest } from '../models/request/taxInfoUpdateRequest'; import { TokenRequest } from '../models/request/tokenRequest'; @@ -125,6 +126,7 @@ export abstract class ApiService { postEmailToken: (request: EmailTokenRequest) => Promise; postEmail: (request: EmailRequest) => Promise; postPassword: (request: PasswordRequest) => Promise; + setPassword: (request: SetPasswordRequest) => Promise; postSecurityStamp: (request: PasswordVerificationRequest) => Promise; deleteAccount: (request: PasswordVerificationRequest) => Promise; getAccountRevisionDate: () => Promise; diff --git a/src/angular/components/change-password.component.ts b/src/angular/components/change-password.component.ts new file mode 100644 index 0000000000..830c0406d2 --- /dev/null +++ b/src/angular/components/change-password.component.ts @@ -0,0 +1,157 @@ +import { + OnInit, +} from '@angular/core'; + +import { + Router, +} from '@angular/router'; + +import { ApiService } from '../../abstractions/api.service'; +import { CipherService } from '../../abstractions/cipher.service'; +import { CryptoService } from '../../abstractions/crypto.service'; +import { FolderService } from '../../abstractions/folder.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { MessagingService } from '../../abstractions/messaging.service'; +import { PasswordGenerationService } from '../../abstractions/passwordGeneration.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { PolicyService } from '../../abstractions/policy.service'; +import { SyncService } from '../../abstractions/sync.service'; +import { UserService } from '../../abstractions/user.service'; + +import { CipherString } from '../../models/domain/cipherString'; +import { MasterPasswordPolicyOptions } from '../../models/domain/masterPasswordPolicyOptions'; +import { SymmetricCryptoKey } from '../../models/domain/symmetricCryptoKey'; + +export class ChangePasswordComponent implements OnInit { + newMasterPassword: string; + confirmNewMasterPassword: string; + formPromise: Promise; + masterPasswordScore: number; + enforcedPolicyOptions: MasterPasswordPolicyOptions; + + private masterPasswordStrengthTimeout: any; + private email: string; + + constructor(protected apiService: ApiService, protected i18nService: I18nService, + protected cryptoService: CryptoService, protected messagingService: MessagingService, + protected userService: UserService, protected passwordGenerationService: PasswordGenerationService, + protected platformUtilsService: PlatformUtilsService, protected folderService: FolderService, + protected cipherService: CipherService, protected syncService: SyncService, + protected policyService: PolicyService, protected router: Router) { } + + async ngOnInit() { + this.email = await this.userService.getEmail(); + this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(); + } + + getPasswordScoreAlertDisplay() { + if (this.enforcedPolicyOptions == null) { + return ''; + } + + let str: string; + switch (this.enforcedPolicyOptions.minComplexity) { + case 4: + str = this.i18nService.t('strong'); + break; + case 3: + str = this.i18nService.t('good'); + break; + default: + str = this.i18nService.t('weak'); + break; + } + return str + ' (' + this.enforcedPolicyOptions.minComplexity + ')'; + } + + async submit() { + const hasEncKey = await this.cryptoService.hasEncKey(); + if (!hasEncKey) { + this.platformUtilsService.showToast('error', null, this.i18nService.t('updateKey')); + return; + } + + if (this.newMasterPassword == null || this.newMasterPassword === '') { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassRequired')); + return; + } + if (this.newMasterPassword.length < 8) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassLength')); + return; + } + if (this.newMasterPassword !== this.confirmNewMasterPassword) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassDoesntMatch')); + return; + } + + const strengthResult = this.passwordGenerationService.passwordStrength(this.newMasterPassword, + this.getPasswordStrengthUserInput()); + + if (this.enforcedPolicyOptions != null && + !this.policyService.evaluateMasterPassword( + strengthResult.score, + this.newMasterPassword, + this.enforcedPolicyOptions)) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPasswordPolicyRequirementsNotMet')); + return; + } + + if (strengthResult != null && strengthResult.score < 3) { + const result = await this.platformUtilsService.showDialog(this.i18nService.t('weakMasterPasswordDesc'), + this.i18nService.t('weakMasterPassword'), this.i18nService.t('yes'), this.i18nService.t('no'), + 'warning'); + if (!result) { + return; + } + } + + if (!await this.setupSubmitActions()) { + return; + } + + const email = await this.userService.getEmail(); + const kdf = await this.userService.getKdf(); + const kdfIterations = await this.userService.getKdfIterations(); + const newKey = await this.cryptoService.makeKey(this.newMasterPassword, email.trim().toLowerCase(), + kdf, kdfIterations); + const newMasterPasswordHash = await this.cryptoService.hashPassword(this.newMasterPassword, newKey); + const newEncKey = await this.cryptoService.remakeEncKey(newKey); + + await this.performSubmitActions(newMasterPasswordHash, newKey, newEncKey); + } + + async setupSubmitActions(): Promise { + // Override in sub-class + // Can be used for additional validation and/or other processes the should occur before changing passwords + return true; + } + + async performSubmitActions(newMasterPasswordHash: string, newKey: SymmetricCryptoKey, + newEncKey: [SymmetricCryptoKey, CipherString]) { + // Override in sub-class + } + + updatePasswordStrength() { + if (this.masterPasswordStrengthTimeout != null) { + clearTimeout(this.masterPasswordStrengthTimeout); + } + this.masterPasswordStrengthTimeout = setTimeout(() => { + const strengthResult = this.passwordGenerationService.passwordStrength(this.newMasterPassword, + this.getPasswordStrengthUserInput()); + this.masterPasswordScore = strengthResult == null ? null : strengthResult.score; + }, 300); + } + + private getPasswordStrengthUserInput() { + let userInput: string[] = []; + const atPosition = this.email.indexOf('@'); + if (atPosition > -1) { + userInput = userInput.concat(this.email.substr(0, atPosition).trim().toLowerCase().split(/[^A-Za-z0-9]/)); + } + return userInput; + } +} diff --git a/src/angular/components/sso.component.ts b/src/angular/components/sso.component.ts new file mode 100644 index 0000000000..777c17b6bb --- /dev/null +++ b/src/angular/components/sso.component.ts @@ -0,0 +1,125 @@ +import { + ActivatedRoute, + Router, +} from '@angular/router'; + +import { ApiService } from '../../abstractions/api.service'; +import { AuthService } from '../../abstractions/auth.service'; +import { CryptoFunctionService } from '../../abstractions/cryptoFunction.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { PasswordGenerationService } from '../../abstractions/passwordGeneration.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { StateService } from '../../abstractions/state.service'; +import { StorageService } from '../../abstractions/storage.service'; + +import { ConstantsService } from '../../services/constants.service'; + +import { Utils } from '../../misc/utils'; + +import { AuthResult } from '../../models/domain/authResult'; + +export class SsoComponent { + identifier: string; + loggingIn = false; + + formPromise: Promise; + onSuccessfulLogin: () => Promise; + onSuccessfulLoginNavigate: () => Promise; + onSuccessfulLoginTwoFactorNavigate: () => Promise; + onSuccessfulLoginChangePasswordNavigate: () => Promise; + + protected twoFactorRoute = '2fa'; + protected successRoute = 'lock'; + protected changePasswordRoute = 'change-password'; + protected redirectUri: string; + + constructor(protected authService: AuthService, protected router: Router, + protected i18nService: I18nService, protected route: ActivatedRoute, + protected storageService: StorageService, protected stateService: StateService, + protected platformUtilsService: PlatformUtilsService, protected apiService: ApiService, + protected cryptoFunctionService: CryptoFunctionService, + protected passwordGenerationService: PasswordGenerationService) { } + + async ngOnInit() { + const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => { + if (qParams.code != null && qParams.state != null) { + const codeVerifier = await this.storageService.get(ConstantsService.ssoCodeVerifierKey); + const state = await this.storageService.get(ConstantsService.ssoStateKey); + await this.storageService.remove(ConstantsService.ssoCodeVerifierKey); + await this.storageService.remove(ConstantsService.ssoStateKey); + if (qParams.code != null && codeVerifier != null && state != null && state === qParams.state) { + await this.logIn(qParams.code, codeVerifier); + } + } + if (queryParamsSub != null) { + queryParamsSub.unsubscribe(); + } + }); + } + + async submit() { + const passwordOptions: any = { + type: 'password', + length: 64, + uppercase: true, + lowercase: true, + numbers: true, + special: false, + }; + const state = await this.passwordGenerationService.generatePassword(passwordOptions); + const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); + const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, 'sha256'); + const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); + + await this.storageService.save(ConstantsService.ssoCodeVerifierKey, codeVerifier); + await this.storageService.save(ConstantsService.ssoStateKey, state); + + const authorizeUrl = this.apiService.identityBaseUrl + '/connect/authorize?' + + 'client_id=web&redirect_uri=' + encodeURIComponent(this.redirectUri) + '&' + + 'response_type=code&scope=api offline_access&' + + 'state=' + state + '&code_challenge=' + codeChallenge + '&' + + 'code_challenge_method=S256&response_mode=query&' + + 'domain_hint=' + encodeURIComponent(this.identifier); + this.platformUtilsService.launchUri(authorizeUrl, { sameWindow: true }); + } + + private async logIn(code: string, codeVerifier: string) { + this.loggingIn = true; + try { + this.formPromise = this.authService.logInSso(code, codeVerifier, this.redirectUri); + const response = await this.formPromise; + if (response.twoFactor) { + this.platformUtilsService.eventTrack('SSO Logged In To Two-step'); + if (this.onSuccessfulLoginTwoFactorNavigate != null) { + this.onSuccessfulLoginTwoFactorNavigate(); + } else { + this.router.navigate([this.twoFactorRoute], { + queryParams: { + resetMasterPassword: response.resetMasterPassword, + }, + }); + } + } else if (response.resetMasterPassword) { + this.platformUtilsService.eventTrack('SSO - routing to complete registration'); + if (this.onSuccessfulLoginChangePasswordNavigate != null) { + this.onSuccessfulLoginChangePasswordNavigate(); + } else { + this.router.navigate([this.changePasswordRoute]); + } + } else { + const disableFavicon = await this.storageService.get(ConstantsService.disableFaviconKey); + await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon); + if (this.onSuccessfulLogin != null) { + this.onSuccessfulLogin(); + } + this.platformUtilsService.eventTrack('SSO Logged In'); + if (this.onSuccessfulLoginNavigate != null) { + this.onSuccessfulLoginNavigate(); + } else { + this.router.navigate([this.successRoute]); + } + } + } catch { } + this.loggingIn = false; + } +} diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 94a1e1553a..16a7d1032d 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -37,6 +37,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { twoFactorEmail: string = null; formPromise: Promise; emailPromise: Promise; + resetMasterPassword: boolean = false; onSuccessfulLogin: () => Promise; onSuccessfulLoginNavigate: () => Promise; @@ -59,7 +60,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { } if (this.authService.authingWithSso()) { - this.successRoute = 'lock'; + this.successRoute = this.resetMasterPassword ? 'reset-master-password' : 'lock'; } if (this.initU2f && this.win != null && this.u2fSupported) { diff --git a/src/models/request/setPasswordRequest.ts b/src/models/request/setPasswordRequest.ts new file mode 100644 index 0000000000..9593f50926 --- /dev/null +++ b/src/models/request/setPasswordRequest.ts @@ -0,0 +1,4 @@ +export class SetPasswordRequest { + newMasterPasswordHash: string; + key: string; +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 8b9249429e..4999d059d2 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -46,6 +46,7 @@ import { PreloginRequest } from '../models/request/preloginRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { SeatRequest } from '../models/request/seatRequest'; import { SelectionReadOnlyRequest } from '../models/request/selectionReadOnlyRequest'; +import { SetPasswordRequest } from '../models/request/setPasswordRequest'; import { StorageRequest } from '../models/request/storageRequest'; import { TaxInfoUpdateRequest } from '../models/request/taxInfoUpdateRequest'; import { TokenRequest } from '../models/request/tokenRequest'; @@ -254,6 +255,10 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/accounts/password', request, true, false); } + setPassword(request: SetPasswordRequest): Promise { + return this.send('POST', '/accounts/set-password', request, true, false); + } + postSecurityStamp(request: PasswordVerificationRequest): Promise { return this.send('POST', '/accounts/security-stamp', request, true, false); } From 7d49902eea45275d50c949beec32b3ab5b7db725 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 3 Aug 2020 15:24:26 -0400 Subject: [PATCH 1030/1626] SSO login for generic clients and CLI (#140) * sso * move break into try block * make client id dynamic * clientId is a string, DOH! * reject if port not available * lint fixes --- package-lock.json | 22 +++ package.json | 1 + src/angular/components/sso.component.ts | 30 +++- src/cli/commands/login.command.ts | 164 ++++++++++++++++----- src/services/passwordGeneration.service.ts | 3 +- 5 files changed, 175 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index b36e8bd6fb..e17bf0fcc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4132,6 +4132,11 @@ } } }, + "is-docker": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.0.0.tgz", + "integrity": "sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==" + }, "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -4265,6 +4270,14 @@ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "requires": { + "is-docker": "^2.0.0" + } + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -6272,6 +6285,15 @@ "mimic-fn": "^1.0.0" } }, + "open": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-7.1.0.tgz", + "integrity": "sha512-lLPI5KgOwEYCDKXf4np7y1PBEkj7HYIyP2DY8mVDRnx0VIIu6bNrRB0R66TuO7Mack6EnTNLm4uvcl1UoklTpA==", + "requires": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + } + }, "opencollective": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/opencollective/-/opencollective-1.0.3.tgz", diff --git a/package.json b/package.json index 71bf68e843..72aec50c87 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "ngx-infinite-scroll": "7.0.1", "node-fetch": "2.2.0", "node-forge": "0.7.6", + "open": "7.1.0", "papaparse": "4.6.0", "rxjs": "6.3.3", "tldjs": "2.3.1", diff --git a/src/angular/components/sso.component.ts b/src/angular/components/sso.component.ts index 777c17b6bb..f2f4db3bf8 100644 --- a/src/angular/components/sso.component.ts +++ b/src/angular/components/sso.component.ts @@ -31,7 +31,10 @@ export class SsoComponent { protected twoFactorRoute = '2fa'; protected successRoute = 'lock'; protected changePasswordRoute = 'change-password'; + protected clientId: string; protected redirectUri: string; + protected state: string; + protected codeChallenge: string; constructor(protected authService: AuthService, protected router: Router, protected i18nService: I18nService, protected route: ActivatedRoute, @@ -50,6 +53,12 @@ export class SsoComponent { if (qParams.code != null && codeVerifier != null && state != null && state === qParams.state) { await this.logIn(qParams.code, codeVerifier); } + } else if (qParams.clientId != null && qParams.redirectUri != null && qParams.state != null && + qParams.codeChallenge != null) { + this.redirectUri = qParams.redirectUri; + this.state = qParams.state; + this.codeChallenge = qParams.codeChallenge; + this.clientId = qParams.clientId; } if (queryParamsSub != null) { queryParamsSub.unsubscribe(); @@ -66,16 +75,21 @@ export class SsoComponent { numbers: true, special: false, }; - const state = await this.passwordGenerationService.generatePassword(passwordOptions); - const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); - const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, 'sha256'); - const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); - - await this.storageService.save(ConstantsService.ssoCodeVerifierKey, codeVerifier); - await this.storageService.save(ConstantsService.ssoStateKey, state); + let codeChallenge = this.codeChallenge; + let state = this.state; + if (codeChallenge == null) { + const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); + const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, 'sha256'); + codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); + await this.storageService.save(ConstantsService.ssoCodeVerifierKey, codeVerifier); + await this.storageService.save(ConstantsService.ssoStateKey, state); + } + if (state == null) { + state = await this.passwordGenerationService.generatePassword(passwordOptions); + } const authorizeUrl = this.apiService.identityBaseUrl + '/connect/authorize?' + - 'client_id=web&redirect_uri=' + encodeURIComponent(this.redirectUri) + '&' + + 'client_id=' + this.clientId + '&redirect_uri=' + encodeURIComponent(this.redirectUri) + '&' + 'response_type=code&scope=api offline_access&' + 'state=' + state + '&code_challenge=' + codeChallenge + '&' + 'code_challenge_method=S256&response_mode=query&' + diff --git a/src/cli/commands/login.command.ts b/src/cli/commands/login.command.ts index f900c0e0b1..6cde86933c 100644 --- a/src/cli/commands/login.command.ts +++ b/src/cli/commands/login.command.ts @@ -1,4 +1,5 @@ import * as program from 'commander'; +import * as http from 'http'; import * as inquirer from 'inquirer'; import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; @@ -8,55 +9,91 @@ import { TwoFactorEmailRequest } from '../../models/request/twoFactorEmailReques import { ApiService } from '../../abstractions/api.service'; import { AuthService } from '../../abstractions/auth.service'; +import { CryptoFunctionService } from '../../abstractions/cryptoFunction.service'; +import { EnvironmentService } from '../../abstractions/environment.service'; import { I18nService } from '../../abstractions/i18n.service'; +import { PasswordGenerationService } from '../../abstractions/passwordGeneration.service'; import { Response } from '../models/response'; import { MessageResponse } from '../models/response/messageResponse'; import { NodeUtils } from '../../misc/nodeUtils'; +import { Utils } from '../../misc/utils'; + +// tslint:disable-next-line +const open = require('open'); export class LoginCommand { protected validatedParams: () => Promise; protected success: () => Promise; + protected canInteract: boolean; + protected clientId: string; + + private ssoRedirectUri: string = null; constructor(protected authService: AuthService, protected apiService: ApiService, - protected i18nService: I18nService) { } + protected i18nService: I18nService, protected environmentService: EnvironmentService, + protected passwordGenerationService: PasswordGenerationService, + protected cryptoFunctionService: CryptoFunctionService) { } async run(email: string, password: string, cmd: program.Command) { - const canInteract = process.env.BW_NOINTERACTION !== 'true'; - if ((email == null || email === '') && canInteract) { - const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ - type: 'input', - name: 'email', - message: 'Email address:', - }); - email = answer.email; - } - if (email == null || email.trim() === '') { - return Response.badRequest('Email address is required.'); - } - if (email.indexOf('@') === -1) { - return Response.badRequest('Email address is invalid.'); - } + this.canInteract = process.env.BW_NOINTERACTION !== 'true'; - if (password == null || password === '') { - if (cmd.passwordfile) { - password = await NodeUtils.readFirstLine(cmd.passwordfile); - } else if (cmd.passwordenv && process.env[cmd.passwordenv]) { - password = process.env[cmd.passwordenv]; - } else if (canInteract) { - const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ - type: 'password', - name: 'password', - message: 'Master password:', - }); - password = answer.password; + let ssoCodeVerifier: string = null; + let ssoCode: string = null; + if (cmd.sso != null && this.canInteract) { + const passwordOptions: any = { + type: 'password', + length: 64, + uppercase: true, + lowercase: true, + numbers: true, + special: false, + }; + const state = await this.passwordGenerationService.generatePassword(passwordOptions); + ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); + const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, 'sha256'); + const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); + try { + ssoCode = await this.getSsoCode(codeChallenge, state); + } catch { + return Response.badRequest('Something went wrong. Try again.'); + } + } else { + if ((email == null || email === '') && this.canInteract) { + const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ + type: 'input', + name: 'email', + message: 'Email address:', + }); + email = answer.email; + } + if (email == null || email.trim() === '') { + return Response.badRequest('Email address is required.'); + } + if (email.indexOf('@') === -1) { + return Response.badRequest('Email address is invalid.'); } - } - if (password == null || password === '') { - return Response.badRequest('Master password is required.'); + if (password == null || password === '') { + if (cmd.passwordfile) { + password = await NodeUtils.readFirstLine(cmd.passwordfile); + } else if (cmd.passwordenv && process.env[cmd.passwordenv]) { + password = process.env[cmd.passwordenv]; + } else if (this.canInteract) { + const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ + type: 'password', + name: 'password', + message: 'Master password:', + }); + password = answer.password; + } + } + + if (password == null || password === '') { + return Response.badRequest('Master password is required.'); + } } let twoFactorToken: string = cmd.code; @@ -76,10 +113,20 @@ export class LoginCommand { let response: AuthResult = null; if (twoFactorToken != null && twoFactorMethod != null) { - response = await this.authService.logInComplete(email, password, twoFactorMethod, - twoFactorToken, false); + if (ssoCode != null && ssoCodeVerifier != null) { + response = await this.authService.logInSsoComplete(ssoCode, ssoCodeVerifier, this.ssoRedirectUri, + twoFactorMethod, twoFactorToken, false); + } else { + response = await this.authService.logInComplete(email, password, twoFactorMethod, + twoFactorToken, false); + } } else { - response = await this.authService.logIn(email, password); + if (ssoCode != null && ssoCodeVerifier != null) { + response = await this.authService.logInSso(ssoCode, ssoCodeVerifier, this.ssoRedirectUri); + + } else { + response = await this.authService.logIn(email, password); + } if (response.twoFactor) { let selectedProvider: any = null; const twoFactorProviders = this.authService.getSupportedTwoFactorProviders(null); @@ -98,7 +145,7 @@ export class LoginCommand { if (selectedProvider == null) { if (twoFactorProviders.length === 1) { selectedProvider = twoFactorProviders[0]; - } else if (canInteract) { + } else if (this.canInteract) { const options = twoFactorProviders.map((p) => p.name); options.push(new inquirer.Separator()); options.push('Cancel'); @@ -128,7 +175,7 @@ export class LoginCommand { } if (twoFactorToken == null) { - if (canInteract) { + if (this.canInteract) { const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ type: 'input', @@ -162,4 +209,49 @@ export class LoginCommand { return Response.error(e); } } + + private async getSsoCode(codeChallenge: string, state: string): Promise { + return new Promise((resolve, reject) => { + const callbackServer = http.createServer((req, res) => { + const urlString = 'http://localhost' + req.url; + const url = new URL(urlString); + const code = url.searchParams.get('code'); + const receivedState = url.searchParams.get('state'); + res.setHeader('Content-Type', 'text/html'); + if (code != null && receivedState != null && receivedState === state) { + res.writeHead(200); + res.end('Success | Bitwarden CLI' + + '

Successfully authenticated with the Bitwarden CLI

' + + '

You may now close this tab and return to the terminal.

' + + ''); + callbackServer.close(() => resolve(code)); + } else { + res.writeHead(400); + res.end('Failed | Bitwarden CLI' + + '

Something went wrong logging into the Bitwarden CLI

' + + '

You may now close this tab and return to the terminal.

' + + ''); + callbackServer.close(() => reject()); + } + }); + let foundPort = false; + const webUrl = this.environmentService.webVaultUrl == null ? 'https://vault.bitwarden.com' : + this.environmentService.webVaultUrl; + for (let port = 8065; port <= 8070; port++) { + try { + this.ssoRedirectUri = 'http://localhost:' + port; + callbackServer.listen(port, async () => { + await open(webUrl + '/#/sso?clientId=' + this.clientId + + '&redirectUri=' + encodeURIComponent(this.ssoRedirectUri) + + '&state=' + state + '&codeChallenge=' + codeChallenge); + }); + foundPort = true; + break; + } catch { } + } + if (!foundPort) { + reject(); + } + }); + } } diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index 4aff5a4223..fc83dcb0d5 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -261,7 +261,8 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr } async getPasswordGeneratorPolicyOptions(): Promise { - const policies: Policy[] = await this.policyService.getAll(PolicyType.PasswordGenerator); + const policies: Policy[] = this.policyService == null ? null : + await this.policyService.getAll(PolicyType.PasswordGenerator); let enforcedOptions: PasswordGeneratorPolicyOptions = null; if (policies == null || policies.length === 0) { From ff98bdcce4c64f2dfc73e59fe6595fef864d6f47 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Mon, 3 Aug 2020 22:12:31 -0400 Subject: [PATCH 1031/1626] moved some logic back to web project for cipher selection --- src/angular/components/ciphers.component.ts | 29 --------------------- 1 file changed, 29 deletions(-) diff --git a/src/angular/components/ciphers.component.ts b/src/angular/components/ciphers.component.ts index 674f9b3efe..50beab2f54 100644 --- a/src/angular/components/ciphers.component.ts +++ b/src/angular/components/ciphers.component.ts @@ -31,8 +31,6 @@ export class CiphersComponent { private pagedCiphersCount = 0; private refreshing = false; - private maxCheckedCount = 500; - constructor(protected searchService: SearchService) { } async load(filter: (cipher: CipherView) => boolean = null, deleted: boolean = false) { @@ -128,31 +126,4 @@ export class CiphersComponent { this.pagedCiphers = []; this.loadMore(); } - - selectAll(select: boolean) { - if (select) { - this.selectAll(false); - } - const selectCount = select && this.ciphers.length > this.maxCheckedCount - ? this.maxCheckedCount - : this.ciphers.length; - for (let i = 0; i < selectCount; i++) { - this.checkCipher(this.ciphers[i], select); - } - } - - checkCipher(c: CipherView, select?: boolean) { - (c as any).checked = select == null ? !(c as any).checked : select; - } - - getSelected(): CipherView[] { - if (this.ciphers == null) { - return []; - } - return this.ciphers.filter((c) => !!(c as any).checked); - } - - getSelectedIds(): string[] { - return this.getSelected().map((c) => c.id); - } } From 272ce2330c80dcc3505772b07155c4ff80d9aab1 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 10 Jul 2020 15:18:00 -0500 Subject: [PATCH 1032/1626] Added custom field consideration to basic search function --- src/services/search.service.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index a7149cafba..b6041c72fa 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -164,6 +164,20 @@ export class SearchService implements SearchServiceAbstraction { if (c.login && c.login.uri != null && c.login.uri.toLowerCase().indexOf(query) > -1) { return true; } + + + if(c.hasFields){ + for (let field of c.fields) { + if (field.value.toLowerCase().indexOf(query) > -1) { + return true; + } + + if(field.name.toLowerCase().indexOf(query) > -1) { + return true; + } + } + } + return false; }); } From 573f8283df16583b70b9f607cc0bb995cbdcf879 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 10 Jul 2020 15:19:14 -0500 Subject: [PATCH 1033/1626] Removed an extra line --- src/services/search.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index b6041c72fa..e1bdfc925a 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -165,7 +165,6 @@ export class SearchService implements SearchServiceAbstraction { return true; } - if(c.hasFields){ for (let field of c.fields) { if (field.value.toLowerCase().indexOf(query) > -1) { From f7058303f39e54694ad3cd0843ad58d52d324a61 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 10 Jul 2020 15:23:05 -0500 Subject: [PATCH 1034/1626] fix a formatting issue --- src/services/search.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index e1bdfc925a..e5bebf4368 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -165,7 +165,7 @@ export class SearchService implements SearchServiceAbstraction { return true; } - if(c.hasFields){ + if(c.hasFields) { for (let field of c.fields) { if (field.value.toLowerCase().indexOf(query) > -1) { return true; From f86c7e9c3f9035a42979ce5548fbbd6e67f66051 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 10 Jul 2020 15:40:32 -0500 Subject: [PATCH 1035/1626] fixed a formatting issue --- src/services/search.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index e5bebf4368..a96724f6e6 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -165,7 +165,7 @@ export class SearchService implements SearchServiceAbstraction { return true; } - if(c.hasFields) { + if (c.hasFields) { for (let field of c.fields) { if (field.value.toLowerCase().indexOf(query) > -1) { return true; From 88765ad09338c797478bd77e12f4a2bf70810733 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 10 Jul 2020 15:44:42 -0500 Subject: [PATCH 1036/1626] yet another formatting issue --- src/services/search.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index a96724f6e6..fa7510d359 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -171,7 +171,7 @@ export class SearchService implements SearchServiceAbstraction { return true; } - if(field.name.toLowerCase().indexOf(query) > -1) { + if (field.name.toLowerCase().indexOf(query) > -1) { return true; } } From 36012e7d6f90a2da57ad6ded4acd776306c0574d Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 10 Jul 2020 15:51:01 -0500 Subject: [PATCH 1037/1626] changed let to const --- src/services/search.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index fa7510d359..9787b19aa9 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -166,7 +166,7 @@ export class SearchService implements SearchServiceAbstraction { } if (c.hasFields) { - for (let field of c.fields) { + for (const field of c.fields) { if (field.value.toLowerCase().indexOf(query) > -1) { return true; } From 44903ef8077fa8a789922784dffefe1674744b28 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Thu, 16 Jul 2020 15:39:19 -0500 Subject: [PATCH 1038/1626] moved some cipher selection logic to base component --- src/angular/components/ciphers.component.ts | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/angular/components/ciphers.component.ts b/src/angular/components/ciphers.component.ts index 50beab2f54..0b0b8b1978 100644 --- a/src/angular/components/ciphers.component.ts +++ b/src/angular/components/ciphers.component.ts @@ -31,6 +31,8 @@ export class CiphersComponent { private pagedCiphersCount = 0; private refreshing = false; + private maxCheckedCount = 500; + constructor(protected searchService: SearchService) { } async load(filter: (cipher: CipherView) => boolean = null, deleted: boolean = false) { @@ -126,4 +128,29 @@ export class CiphersComponent { this.pagedCiphers = []; this.loadMore(); } + + selectAll(select: boolean) { + if (select) { + this.selectAll(false); + } + const selectCount = select && this.ciphers.length > this.maxCheckedCount ? this.maxCheckedCount : this.ciphers.length; + for (let i = 0; i < selectCount; i++) { + this.checkCipher(this.ciphers[i], select); + } + } + + checkCipher(c: CipherView, select?: boolean) { + (c as any).checked = select == null ? !(c as any).checked : select; + } + + getSelected(): CipherView[] { + if (this.ciphers == null) { + return []; + } + return this.ciphers.filter((c) => !!(c as any).checked); + } + + getSelectedIds(): string[] { + return this.getSelected().map((c) => c.id); + } } From 5d93c84ae6af20124ea9020fb4740e8d43bb7ec2 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Thu, 16 Jul 2020 15:44:39 -0500 Subject: [PATCH 1039/1626] removed leaky code --- src/services/search.service.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 9787b19aa9..a7149cafba 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -164,19 +164,6 @@ export class SearchService implements SearchServiceAbstraction { if (c.login && c.login.uri != null && c.login.uri.toLowerCase().indexOf(query) > -1) { return true; } - - if (c.hasFields) { - for (const field of c.fields) { - if (field.value.toLowerCase().indexOf(query) > -1) { - return true; - } - - if (field.name.toLowerCase().indexOf(query) > -1) { - return true; - } - } - } - return false; }); } From f0411d02405bd829d3881c9e10940301c91b0c76 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Thu, 16 Jul 2020 16:10:00 -0500 Subject: [PATCH 1040/1626] broke up a long line --- src/angular/components/ciphers.component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/angular/components/ciphers.component.ts b/src/angular/components/ciphers.component.ts index 0b0b8b1978..674f9b3efe 100644 --- a/src/angular/components/ciphers.component.ts +++ b/src/angular/components/ciphers.component.ts @@ -133,7 +133,9 @@ export class CiphersComponent { if (select) { this.selectAll(false); } - const selectCount = select && this.ciphers.length > this.maxCheckedCount ? this.maxCheckedCount : this.ciphers.length; + const selectCount = select && this.ciphers.length > this.maxCheckedCount + ? this.maxCheckedCount + : this.ciphers.length; for (let i = 0; i < selectCount; i++) { this.checkCipher(this.ciphers[i], select); } From 1cb59b5cc7391a0a085408a861e1efe1e6ac75e9 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Sun, 19 Jul 2020 22:26:45 -0500 Subject: [PATCH 1041/1626] added api service methods for delete many w/admin calls --- src/abstractions/api.service.ts | 2 ++ src/services/api.service.ts | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 81a687b6b1..838d12e434 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -165,6 +165,7 @@ export abstract class ApiService { deleteCipher: (id: string) => Promise; deleteCipherAdmin: (id: string) => Promise; deleteManyCiphers: (request: CipherBulkDeleteRequest) => Promise; + deleteManyCiphersAdmin: (request: CipherBulkDeleteRequest) => Promise; putMoveCiphers: (request: CipherBulkMoveRequest) => Promise; putShareCipher: (id: string, request: CipherShareRequest) => Promise; putShareCiphers: (request: CipherBulkShareRequest) => Promise; @@ -176,6 +177,7 @@ export abstract class ApiService { putDeleteCipher: (id: string) => Promise; putDeleteCipherAdmin: (id: string) => Promise; putDeleteManyCiphers: (request: CipherBulkDeleteRequest) => Promise; + putDeleteManyCiphersAdmin: (request: CipherBulkDeleteRequest) => Promise; putRestoreCipher: (id: string) => Promise; putRestoreCipherAdmin: (id: string) => Promise; putRestoreManyCiphers: (request: CipherBulkRestoreRequest) => Promise; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 4999d059d2..9f7397f355 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -423,6 +423,10 @@ export class ApiService implements ApiServiceAbstraction { return this.send('DELETE', '/ciphers', request, true, false); } + deleteManyCiphersAdmin(request: CipherBulkDeleteRequest): Promise { + return this.send('DELETE', '/ciphers/admin', request, true, false); + } + putMoveCiphers(request: CipherBulkMoveRequest): Promise { return this.send('PUT', '/ciphers/move', request, true, false); } @@ -472,6 +476,10 @@ export class ApiService implements ApiServiceAbstraction { return this.send('PUT', '/ciphers/delete', request, true, false); } + putDeleteManyCiphersAdmin(request: CipherBulkDeleteRequest): Promise { + return this.send('PUT', '/ciphers/delete-admin', request, true, false); + } + putRestoreCipher(id: string): Promise { return this.send('PUT', '/ciphers/' + id + '/restore', null, true, false); } From 144f06a115513766b1dac89cff77a4dffb30f52f Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Mon, 20 Jul 2020 17:48:27 -0500 Subject: [PATCH 1042/1626] added org id to the bulk delete request model --- src/models/request/cipherBulkDeleteRequest.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/models/request/cipherBulkDeleteRequest.ts b/src/models/request/cipherBulkDeleteRequest.ts index fb19523a20..97fab41b46 100644 --- a/src/models/request/cipherBulkDeleteRequest.ts +++ b/src/models/request/cipherBulkDeleteRequest.ts @@ -1,7 +1,9 @@ export class CipherBulkDeleteRequest { ids: string[]; + organizationId: string; - constructor(ids: string[]) { + constructor(ids: string[], organizationId?: string) { this.ids = ids == null ? [] : ids; + this.organizationId = organizationId; } } From e778735ac47001b4eacb72209e1df57d135c747f Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Mon, 3 Aug 2020 22:12:31 -0400 Subject: [PATCH 1043/1626] moved some logic back to web project for cipher selection --- src/angular/components/ciphers.component.ts | 29 --------------------- 1 file changed, 29 deletions(-) diff --git a/src/angular/components/ciphers.component.ts b/src/angular/components/ciphers.component.ts index 674f9b3efe..50beab2f54 100644 --- a/src/angular/components/ciphers.component.ts +++ b/src/angular/components/ciphers.component.ts @@ -31,8 +31,6 @@ export class CiphersComponent { private pagedCiphersCount = 0; private refreshing = false; - private maxCheckedCount = 500; - constructor(protected searchService: SearchService) { } async load(filter: (cipher: CipherView) => boolean = null, deleted: boolean = false) { @@ -128,31 +126,4 @@ export class CiphersComponent { this.pagedCiphers = []; this.loadMore(); } - - selectAll(select: boolean) { - if (select) { - this.selectAll(false); - } - const selectCount = select && this.ciphers.length > this.maxCheckedCount - ? this.maxCheckedCount - : this.ciphers.length; - for (let i = 0; i < selectCount; i++) { - this.checkCipher(this.ciphers[i], select); - } - } - - checkCipher(c: CipherView, select?: boolean) { - (c as any).checked = select == null ? !(c as any).checked : select; - } - - getSelected(): CipherView[] { - if (this.ciphers == null) { - return []; - } - return this.ciphers.filter((c) => !!(c as any).checked); - } - - getSelectedIds(): string[] { - return this.getSelected().map((c) => c.id); - } } From bc31867e1a8ff4a85f59163889da5801db0ce25a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 4 Aug 2020 08:50:13 -0400 Subject: [PATCH 1044/1626] allow login command clientid from ctor --- src/cli/commands/login.command.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cli/commands/login.command.ts b/src/cli/commands/login.command.ts index 6cde86933c..9d6e6bf243 100644 --- a/src/cli/commands/login.command.ts +++ b/src/cli/commands/login.command.ts @@ -35,7 +35,9 @@ export class LoginCommand { constructor(protected authService: AuthService, protected apiService: ApiService, protected i18nService: I18nService, protected environmentService: EnvironmentService, protected passwordGenerationService: PasswordGenerationService, - protected cryptoFunctionService: CryptoFunctionService) { } + protected cryptoFunctionService: CryptoFunctionService, clientId: string) { + this.clientId = clientId; + } async run(email: string, password: string, cmd: program.Command) { this.canInteract = process.env.BW_NOINTERACTION !== 'true'; From 14b01f2e5da839ffc312deb68e5b298d8fadb363 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 4 Aug 2020 09:17:25 -0400 Subject: [PATCH 1045/1626] null check vaulttimeoutservice --- src/services/auth.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 3ceb1b84c3..074a32ae77 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -325,7 +325,9 @@ export class AuthService implements AuthServiceAbstraction { await this.cryptoService.setEncPrivateKey(tokenResponse.privateKey); } - this.vaultTimeoutService.biometricLocked = false; + if (this.vaultTimeoutService != null) { + this.vaultTimeoutService.biometricLocked = false; + } this.messagingService.send('loggedIn'); return result; } From 1513b25a350c9dfd855410d3f8f2cfbf70744d01 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 5 Aug 2020 10:53:26 -0400 Subject: [PATCH 1046/1626] callbacks for argv from window main (#141) --- src/angular/components/sso.component.ts | 2 +- src/electron/window.main.ts | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/angular/components/sso.component.ts b/src/angular/components/sso.component.ts index f2f4db3bf8..9c537e8814 100644 --- a/src/angular/components/sso.component.ts +++ b/src/angular/components/sso.component.ts @@ -82,10 +82,10 @@ export class SsoComponent { const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, 'sha256'); codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); await this.storageService.save(ConstantsService.ssoCodeVerifierKey, codeVerifier); - await this.storageService.save(ConstantsService.ssoStateKey, state); } if (state == null) { state = await this.passwordGenerationService.generatePassword(passwordOptions); + await this.storageService.save(ConstantsService.ssoStateKey, state); } const authorizeUrl = this.apiService.identityBaseUrl + '/connect/authorize?' + diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index c5d428d77f..bf4d28dbbb 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -22,7 +22,8 @@ export class WindowMain { private enableAlwaysOnTop: boolean = false; constructor(private storageService: StorageService, private hideTitleBar = false, - private defaultWidth = 950, private defaultHeight = 600) { } + private defaultWidth = 950, private defaultHeight = 600, + private argvCallback: (argv: string[]) => void = null) { } init(): Promise { return new Promise((resolve, reject) => { @@ -33,7 +34,7 @@ export class WindowMain { app.quit(); return; } else { - app.on('second-instance', (event, commandLine, workingDirectory) => { + app.on('second-instance', (event, argv, workingDirectory) => { // Someone tried to run a second instance, we should focus our window. if (this.win != null) { if (this.win.isMinimized() || !this.win.isVisible()) { @@ -41,6 +42,11 @@ export class WindowMain { } this.win.focus(); } + if (process.platform === 'win32' || process.platform === 'linux') { + if (this.argvCallback != null) { + this.argvCallback(argv); + } + } }); } } @@ -57,6 +63,9 @@ export class WindowMain { app.on('ready', async () => { await this.createWindow(); resolve(); + if (this.argvCallback != null) { + this.argvCallback(process.argv); + } }); // Quit when all windows are closed. From 7c0c06705ea7e8a0266b0946fb66061e9b33302e Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Thu, 6 Aug 2020 12:27:49 -0400 Subject: [PATCH 1047/1626] added try-catch around ext module calls (#143) --- src/electron/biometric.windows.main.ts | 50 ++++++++++++++++---------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/electron/biometric.windows.main.ts b/src/electron/biometric.windows.main.ts index 84ae41ae83..1d9f3df008 100644 --- a/src/electron/biometric.windows.main.ts +++ b/src/electron/biometric.windows.main.ts @@ -62,12 +62,17 @@ export default class BiometricWindowsMain implements BiometricMain { const module = this.getWindowsSecurityCredentialsUiModule(); if (module != null) { return new Promise((resolve, reject) => { - module.UserConsentVerifier.checkAvailabilityAsync((error: Error, result: any) => { - if (error) { - return resolve(null); - } - return resolve(result); - }); + try { + module.UserConsentVerifier.checkAvailabilityAsync((error: Error, result: any) => { + if (error) { + return resolve(null); + } + return resolve(result); + }); + } catch { + this.isError = true; + return resolve(null); + } }); } return Promise.resolve(null); @@ -77,25 +82,32 @@ export default class BiometricWindowsMain implements BiometricMain { const module = this.getWindowsSecurityCredentialsUiModule(); if (module != null) { return new Promise((resolve, reject) => { - module.UserConsentVerifier.requestVerificationAsync(message, (error: Error, result: any) => { - if (error) { - return resolve(null); - } - return resolve(result); - }); + try { + module.UserConsentVerifier.requestVerificationAsync(message, (error: Error, result: any) => { + if (error) { + return resolve(null); + } + return resolve(result); + }); + } catch (error) { + this.isError = true; + return reject(error); + } }); } return Promise.resolve(null); } getAllowedAvailabilities(): any[] { - const module = this.getWindowsSecurityCredentialsUiModule(); - if (module != null) { - return [ - module.UserConsentVerifierAvailability.available, - module.UserConsentVerifierAvailability.deviceBusy, - ]; - } + try { + const module = this.getWindowsSecurityCredentialsUiModule(); + if (module != null) { + return [ + module.UserConsentVerifierAvailability.available, + module.UserConsentVerifierAvailability.deviceBusy, + ]; + } + } catch { /*Ignore error*/ } return []; } } From 7c3a9d61e6e736c8f439a5094585efac3a4cfa12 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Mon, 10 Aug 2020 08:38:31 -0500 Subject: [PATCH 1048/1626] [SSO] Login - added launchSsoBrowser method (#144) * Added launchSsoBrowser method * Updated let -> const * Saved state/verifier to storage --- src/angular/components/login.component.ts | 38 +++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/angular/components/login.component.ts b/src/angular/components/login.component.ts index 75fea94b45..81a32a8f05 100644 --- a/src/angular/components/login.component.ts +++ b/src/angular/components/login.component.ts @@ -2,14 +2,17 @@ import { Input, OnInit, } from '@angular/core'; + import { Router } from '@angular/router'; import { AuthResult } from '../../models/domain/authResult'; import { AuthService } from '../../abstractions/auth.service'; +import { CryptoFunctionService } from '../../abstractions/cryptoFunction.service'; +import { EnvironmentService } from '../../abstractions/environment.service'; import { I18nService } from '../../abstractions/i18n.service'; +import { PasswordGenerationService } from '../../abstractions/passwordGeneration.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; -import { StateService } from '../../abstractions/state.service'; import { StorageService } from '../../abstractions/storage.service'; import { ConstantsService } from '../../services/constants.service'; @@ -37,7 +40,9 @@ export class LoginComponent implements OnInit { constructor(protected authService: AuthService, protected router: Router, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, - private storageService: StorageService, protected stateService: StorageService) { } + protected stateService: StorageService, protected environmentService: EnvironmentService, + protected passwordGenerationService: PasswordGenerationService, + protected cryptoFunctionService: CryptoFunctionService, private storageService: StorageService) { } async ngOnInit() { if (this.email == null || this.email === '') { @@ -109,4 +114,33 @@ export class LoginComponent implements OnInit { this.showPassword = !this.showPassword; document.getElementById('masterPassword').focus(); } + + async launchSsoBrowser(clientId: string, ssoRedirectUri: string) { + // Generate necessary sso params + const passwordOptions: any = { + type: 'password', + length: 64, + uppercase: true, + lowercase: true, + numbers: true, + special: false, + }; + const state = await this.passwordGenerationService.generatePassword(passwordOptions); + const ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); + const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, 'sha256'); + const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); + + // Save sso params + await this.storageService.save(ConstantsService.ssoStateKey, state); + await this.storageService.save(ConstantsService.ssoCodeVerifierKey, ssoCodeVerifier); + + // Build URI + const webUrl = this.environmentService.webVaultUrl == null ? 'https://vault.bitwarden.com' : + this.environmentService.webVaultUrl; + + // Launch browser + this.platformUtilsService.launchUri(webUrl + '/#/sso?clientId=' + clientId + + '&redirectUri=' + encodeURIComponent(ssoRedirectUri) + + '&state=' + state + '&codeChallenge=' + codeChallenge); + } } From 420393700b38ed6e8e812366faf9231858bdaa92 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Tue, 11 Aug 2020 10:47:30 -0400 Subject: [PATCH 1049/1626] Misc jslib cleanups (#146) Fixed on import parameter type on login.component and added a deprecation warning to the index.ts in abstractions --- src/abstractions/index.ts | 1 + src/angular/components/login.component.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/abstractions/index.ts b/src/abstractions/index.ts index cec535ffab..6d8e7427ed 100644 --- a/src/abstractions/index.ts +++ b/src/abstractions/index.ts @@ -1,3 +1,4 @@ +// Using index.ts is deprecated, please do not extend export { ApiService } from './api.service'; export { AppIdService } from './appId.service'; export { AuditService } from './audit.service'; diff --git a/src/angular/components/login.component.ts b/src/angular/components/login.component.ts index 81a32a8f05..1e533dbad3 100644 --- a/src/angular/components/login.component.ts +++ b/src/angular/components/login.component.ts @@ -13,6 +13,7 @@ import { EnvironmentService } from '../../abstractions/environment.service'; import { I18nService } from '../../abstractions/i18n.service'; import { PasswordGenerationService } from '../../abstractions/passwordGeneration.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { StateService } from '../../abstractions/state.service'; import { StorageService } from '../../abstractions/storage.service'; import { ConstantsService } from '../../services/constants.service'; @@ -40,7 +41,7 @@ export class LoginComponent implements OnInit { constructor(protected authService: AuthService, protected router: Router, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, - protected stateService: StorageService, protected environmentService: EnvironmentService, + protected stateService: StateService, protected environmentService: EnvironmentService, protected passwordGenerationService: PasswordGenerationService, protected cryptoFunctionService: CryptoFunctionService, private storageService: StorageService) { } From b32b016f823300699971287e59fc85c57d2c1842 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Tue, 11 Aug 2020 14:20:39 -0400 Subject: [PATCH 1050/1626] Merge plan and price updates (#145) * Created a PlanResponse model & relevant API request for getting plan data from the server --- src/abstractions/api.service.ts | 2 + src/enums/planType.ts | 15 ++-- src/enums/productType.ts | 6 ++ src/models/response/organizationResponse.ts | 3 +- src/models/response/planResponse.ts | 93 +++++++++++++++++++++ src/services/api.service.ts | 8 ++ 6 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 src/enums/productType.ts create mode 100644 src/models/response/planResponse.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 838d12e434..266c1c68ae 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -87,6 +87,7 @@ import { OrganizationUserUserDetailsResponse, } from '../models/response/organizationUserResponse'; import { PaymentResponse } from '../models/response/paymentResponse'; +import { PlanResponse } from '../models/response/planResponse'; import { PolicyResponse } from '../models/response/policyResponse'; import { PreloginResponse } from '../models/response/preloginResponse'; import { ProfileResponse } from '../models/response/profileResponse'; @@ -282,6 +283,7 @@ export abstract class ApiService { postOrganizationCancel: (id: string) => Promise; postOrganizationReinstate: (id: string) => Promise; deleteOrganization: (id: string, request: PasswordVerificationRequest) => Promise; + getPlans: () => Promise>; getEvents: (start: string, end: string, token: string) => Promise>; getEventsCipher: (id: string, start: string, end: string, token: string) => Promise>; diff --git a/src/enums/planType.ts b/src/enums/planType.ts index eadd188415..1b07c723aa 100644 --- a/src/enums/planType.ts +++ b/src/enums/planType.ts @@ -1,9 +1,14 @@ export enum PlanType { Free = 0, - FamiliesAnnually = 1, - TeamsMonthly = 2, - TeamsAnnually = 3, - EnterpriseMonthly = 4, - EnterpriseAnnually = 5, + FamiliesAnnually2019 = 1, + TeamsMonthly2019 = 2, + TeamsAnnually2019 = 3, + EnterpriseMonthly2019 = 4, + EnterpriseAnnually2019 = 5, Custom = 6, + FamiliesAnnually = 7, + TeamsMonthly = 8, + TeamsAnnually = 9, + EnterpriseMonthly = 10, + EnterpriseAnnually = 11, } diff --git a/src/enums/productType.ts b/src/enums/productType.ts new file mode 100644 index 0000000000..d8e2e0ae2e --- /dev/null +++ b/src/enums/productType.ts @@ -0,0 +1,6 @@ +export enum ProductType { + Free = 0, + Families = 1, + Teams = 2, + Enterprise = 3, +} diff --git a/src/models/response/organizationResponse.ts b/src/models/response/organizationResponse.ts index 436e09e3fa..0859dca74c 100644 --- a/src/models/response/organizationResponse.ts +++ b/src/models/response/organizationResponse.ts @@ -1,4 +1,5 @@ import { BaseResponse } from './baseResponse'; +import { PlanResponse } from './planResponse'; import { PlanType } from '../../enums/planType'; @@ -12,7 +13,7 @@ export class OrganizationResponse extends BaseResponse { businessCountry: string; businessTaxNumber: string; billingEmail: string; - plan: string; + plan: PlanResponse; planType: PlanType; seats: number; maxCollections: number; diff --git a/src/models/response/planResponse.ts b/src/models/response/planResponse.ts new file mode 100644 index 0000000000..96b385da07 --- /dev/null +++ b/src/models/response/planResponse.ts @@ -0,0 +1,93 @@ +import { PlanType } from '../../enums/planType'; +import { ProductType } from '../../enums/productType'; + +import { BaseResponse } from './baseResponse'; + +export class PlanResponse extends BaseResponse { + type: PlanType; + product: ProductType; + name: string; + isAnnual: boolean; + nameLocalizationKey: string; + descriptionLocalizationKey: string; + canBeUsedByBusiness: boolean; + baseSeats: number; + baseStorageGb: number; + maxCollections: number; + maxUsers: number; + + hasAdditionalSeatsOption: boolean; + maxAdditionalSeats: number; + hasAdditionalStorageOption: boolean; + maxAdditionalStorage: number; + hasPremiumAccessOption: boolean; + trialPeriodDays: number; + + hasSelfHost: boolean; + hasPolicies: boolean; + hasGroups: boolean; + hasDirectory: boolean; + hasEvents: boolean; + hasTotp: boolean; + has2fa: boolean; + hasApi: boolean; + hasSso: boolean; + usersGetPremium: boolean; + + upgradeSortOrder: number; + displaySortOrder: number; + legacyYear: number; + disabled: boolean; + + stripePlanId: string; + stripeSeatPlanId: string; + stripeStoragePlanId: string; + stripePremiumAccessPlanId: string; + basePrice: number; + seatPrice: number; + additionalStoragePricePerGb: number; + premiumAccessOptionPrice: number; + + constructor(response: any) { + super(response); + this.type = this.getResponseProperty('type'); + this.product = this.getResponseProperty('product'); + this.name = this.getResponseProperty('name'); + this.isAnnual = this.getResponseProperty('isAnnual'); + this.nameLocalizationKey = this.getResponseProperty('nameLocalizationKey'); + this.descriptionLocalizationKey = this.getResponseProperty('descriptionLocalizationKey'); + this.canBeUsedByBusiness = this.getResponseProperty('canBeUsedByBusiness'); + this.baseSeats = this.getResponseProperty('baseSeats'); + this.baseStorageGb = this.getResponseProperty('baseStorageGb'); + this.maxCollections = this.getResponseProperty('maxCollections'); + this.maxUsers = this.getResponseProperty('maxUsers'); + this.hasAdditionalSeatsOption = this.getResponseProperty('hasAdditionalSeatsOption'); + this.maxAdditionalSeats = this.getResponseProperty('maxAdditionalSeats'); + this.hasAdditionalStorageOption = this.getResponseProperty('hasAdditionalStorageOption'); + this.maxAdditionalStorage = this.getResponseProperty('maxAdditionalStorage'); + this.hasPremiumAccessOption = this.getResponseProperty('hasPremiumAccessOption'); + this.trialPeriodDays = this.getResponseProperty('trialPeriodDays'); + this.hasSelfHost = this.getResponseProperty('hasSelfHost'); + this.hasPolicies = this.getResponseProperty('hasPolicies'); + this.hasGroups = this.getResponseProperty('hasGroups'); + this.hasDirectory = this.getResponseProperty('hasDirectory'); + this.hasEvents = this.getResponseProperty('hasEvents'); + this.hasTotp = this.getResponseProperty('hasTotp'); + this.has2fa = this.getResponseProperty('has2fa'); + this.hasApi = this.getResponseProperty('hasApi'); + this.hasSso = this.getResponseProperty('hasSso'); + this.usersGetPremium = this.getResponseProperty('usersGetPremium'); + this.upgradeSortOrder = this.getResponseProperty('upgradeSortOrder'); + this.displaySortOrder = this.getResponseProperty('sortOrder'); + this.legacyYear = this.getResponseProperty('legacyYear'); + this.disabled = this.getResponseProperty('disabled'); + this.stripePlanId = this.getResponseProperty('stripePlanId'); + this.stripeSeatPlanId = this.getResponseProperty('stripeSeatPlanId'); + this.stripeStoragePlanId = this.getResponseProperty('stripeStoragePlanId'); + this.stripePremiumAccessPlanId = this.getResponseProperty('stripePremiumAccessPlanId'); + this.basePrice = this.getResponseProperty('basePrice'); + this.seatPrice = this.getResponseProperty('seatPrice'); + this.additionalStoragePricePerGb = this.getResponseProperty('additionalStoragePricePerGb'); + this.premiumAccessOptionPrice = this.getResponseProperty('premiumAccessOptionPrice'); + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 9f7397f355..f79178e2a7 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -92,6 +92,7 @@ import { OrganizationUserUserDetailsResponse, } from '../models/response/organizationUserResponse'; import { PaymentResponse } from '../models/response/paymentResponse'; +import { PlanResponse } from '../models/response/planResponse'; import { PolicyResponse } from '../models/response/policyResponse'; import { PreloginResponse } from '../models/response/preloginResponse'; import { ProfileResponse } from '../models/response/profileResponse'; @@ -685,6 +686,13 @@ export class ApiService implements ApiServiceAbstraction { return this.send('DELETE', '/organizations/' + organizationId + '/users/' + id, null, true, false); } + // Plan APIs + + async getPlans(): Promise> { + const r = await this.send('GET', '/plans/', null, true, true); + return new ListResponse(r, PlanResponse); + } + // Sync APIs async getSync(): Promise { From e516692559d79f4afd6bae1331e7b3060c9c762c Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Wed, 12 Aug 2020 21:42:42 +0200 Subject: [PATCH 1051/1626] Upgrade TypeScript (#148) * Update typescript to 3.6.5 along with tslint to latest. * Upgrade @types/node to 12.12.54 to get rid of compile errors. * Update tslint. * Use @types/node 10.17.28 instead --- package-lock.json | 138 +++++------------- package.json | 6 +- src/abstractions/collection.service.ts | 2 +- src/abstractions/folder.service.ts | 2 +- src/abstractions/search.service.ts | 2 +- src/angular/components/groupings.component.ts | 4 +- src/importers/kasperskyTxtImporter.ts | 4 +- src/misc/serviceUtils.ts | 4 +- src/misc/throttle.ts | 4 +- src/models/domain/importResult.ts | 4 +- src/models/domain/treeNode.ts | 2 +- src/models/request/importCiphersRequest.ts | 2 +- .../importOrganizationCiphersRequest.ts | 2 +- src/services/cipher.service.ts | 6 +- src/services/collection.service.ts | 6 +- src/services/folder.service.ts | 6 +- src/services/search.service.ts | 2 +- 17 files changed, 62 insertions(+), 134 deletions(-) diff --git a/package-lock.json b/package-lock.json index e17bf0fcc9..131cc03887 100644 --- a/package-lock.json +++ b/package-lock.json @@ -325,9 +325,9 @@ "dev": true }, "@types/node": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.9.4.tgz", - "integrity": "sha512-fCHV45gS+m3hH17zgkgADUSi2RR1Vht6wOZ0jyHP8rjiQra9f+mIcgwPQHllmDocYOstIEbKlxbFDYlgrTPYqw==", + "version": "10.17.28", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.28.tgz", + "integrity": "sha512-dzjES1Egb4c1a89C7lKwQh8pwjYmlOAG9dW1pBgxEk57tMrLnssOfEthz8kdkNaBd7lIqQx7APm5+mZ619IiCQ==", "dev": true }, "@types/node-fetch": { @@ -846,53 +846,6 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, "babel-polyfill": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.23.0.tgz", @@ -2778,9 +2731,9 @@ "dev": true }, "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, "diffie-hellman": { @@ -4533,12 +4486,6 @@ } } }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - }, "js-yaml": { "version": "3.13.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", @@ -8182,65 +8129,46 @@ "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" }, "tslint": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.12.1.tgz", - "integrity": "sha512-sfodBHOucFg6egff8d1BvuofoOQ/nOeYNfbp7LDlKBcLNrL3lmS5zoiDGyOMdT7YsEXAwWpTdAHwOGOc8eRZAw==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", + "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", "dev": true, "requires": { - "babel-code-frame": "^6.22.0", + "@babel/code-frame": "^7.0.0", "builtin-modules": "^1.1.1", "chalk": "^2.3.0", "commander": "^2.12.1", - "diff": "^3.2.0", + "diff": "^4.0.1", "glob": "^7.1.1", - "js-yaml": "^3.7.0", + "js-yaml": "^3.13.1", "minimatch": "^3.0.4", + "mkdirp": "^0.5.3", "resolve": "^1.3.2", "semver": "^5.3.0", - "tslib": "^1.8.0", - "tsutils": "^2.27.2" + "tslib": "^1.13.0", + "tsutils": "^2.29.0" }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "minimist": "^1.2.5" } + }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", + "dev": true } } }, @@ -8308,9 +8236,9 @@ } }, "typescript": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.4.tgz", - "integrity": "sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg==", + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.5.tgz", + "integrity": "sha512-BEjlc0Z06ORZKbtcxGrIvvwYs5hAnuo6TKdNFL55frVDlB+na3z5bsLhFaIxmT+dPWgBIjMo6aNnTOgHHmHgiQ==", "dev": true }, "uglify-js": { diff --git a/package.json b/package.json index 72aec50c87..db760393ee 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@types/jasmine": "^3.3.12", "@types/lowdb": "^1.0.5", "@types/lunr": "^2.3.3", - "@types/node": "^10.9.4", + "@types/node": "^10.17.28", "@types/node-fetch": "^2.1.2", "@types/node-forge": "^0.7.5", "@types/papaparse": "^4.5.3", @@ -57,9 +57,9 @@ "karma-typescript": "^4.0.0", "nodemon": "^1.17.3", "rimraf": "^2.6.2", - "tslint": "^5.12.1", + "tslint": "^6.1.3", "typemoq": "^2.1.0", - "typescript": "3.2.4" + "typescript": "3.6.5" }, "dependencies": { "@angular/animations": "7.2.1", diff --git a/src/abstractions/collection.service.ts b/src/abstractions/collection.service.ts index a20c313b4c..0904553793 100644 --- a/src/abstractions/collection.service.ts +++ b/src/abstractions/collection.service.ts @@ -14,7 +14,7 @@ export abstract class CollectionService { get: (id: string) => Promise; getAll: () => Promise; getAllDecrypted: () => Promise; - getAllNested: (collections?: CollectionView[]) => Promise>>; + getAllNested: (collections?: CollectionView[]) => Promise[]>; getNested: (id: string) => Promise>; upsert: (collection: CollectionData | CollectionData[]) => Promise; replace: (collections: { [id: string]: CollectionData; }) => Promise; diff --git a/src/abstractions/folder.service.ts b/src/abstractions/folder.service.ts index 9f7099988d..f1997ffef5 100644 --- a/src/abstractions/folder.service.ts +++ b/src/abstractions/folder.service.ts @@ -14,7 +14,7 @@ export abstract class FolderService { get: (id: string) => Promise; getAll: () => Promise; getAllDecrypted: () => Promise; - getAllNested: () => Promise>>; + getAllNested: () => Promise[]>; getNested: (id: string) => Promise>; saveWithServer: (folder: Folder) => Promise; upsert: (folder: FolderData | FolderData[]) => Promise; diff --git a/src/abstractions/search.service.ts b/src/abstractions/search.service.ts index 1cfc32b19c..d6d0364faf 100644 --- a/src/abstractions/search.service.ts +++ b/src/abstractions/search.service.ts @@ -5,7 +5,7 @@ export abstract class SearchService { isSearchable: (query: string) => boolean; indexCiphers: () => Promise; searchCiphers: (query: string, - filter?: ((cipher: CipherView) => boolean) | (Array<(cipher: CipherView) => boolean>), + filter?: ((cipher: CipherView) => boolean) | (((cipher: CipherView) => boolean)[]), ciphers?: CipherView[]) => Promise; searchCiphersBasic: (ciphers: CipherView[], query: string, deleted?: boolean) => CipherView[]; } diff --git a/src/angular/components/groupings.component.ts b/src/angular/components/groupings.component.ts index 022da692d5..3d93e7f890 100644 --- a/src/angular/components/groupings.component.ts +++ b/src/angular/components/groupings.component.ts @@ -34,9 +34,9 @@ export class GroupingsComponent { @Output() onCollectionClicked = new EventEmitter(); folders: FolderView[]; - nestedFolders: Array>; + nestedFolders: TreeNode[]; collections: CollectionView[]; - nestedCollections: Array>; + nestedCollections: TreeNode[]; loaded: boolean = false; cipherType = CipherType; selectedAll: boolean = false; diff --git a/src/importers/kasperskyTxtImporter.ts b/src/importers/kasperskyTxtImporter.ts index 895c693950..9c318df611 100644 --- a/src/importers/kasperskyTxtImporter.ts +++ b/src/importers/kasperskyTxtImporter.ts @@ -75,11 +75,11 @@ export class KasperskyTxtImporter extends BaseImporter implements Importer { return result; } - private parseDataCategory(data: string): Array> { + private parseDataCategory(data: string): Map[] { if (this.isNullOrWhitespace(data) || data.indexOf(Delimiter) === -1) { return []; } - const items: Array> = []; + const items: Map[] = []; data.split(Delimiter).forEach((p) => { if (p.indexOf('\n') === -1) { return; diff --git a/src/misc/serviceUtils.ts b/src/misc/serviceUtils.ts index f70e119ae6..50ee43eb7d 100644 --- a/src/misc/serviceUtils.ts +++ b/src/misc/serviceUtils.ts @@ -4,7 +4,7 @@ import { } from '../models/domain/treeNode'; export class ServiceUtils { - static nestedTraverse(nodeTree: Array>, partIndex: number, parts: string[], + static nestedTraverse(nodeTree: TreeNode[], partIndex: number, parts: string[], obj: ITreeNodeObject, parent: ITreeNodeObject, delimiter: string) { if (parts.length <= partIndex) { return; @@ -38,7 +38,7 @@ export class ServiceUtils { } } - static getTreeNodeObject(nodeTree: Array>, id: string): TreeNode { + static getTreeNodeObject(nodeTree: TreeNode[], id: string): TreeNode { for (let i = 0; i < nodeTree.length; i++) { if (nodeTree[i].node.id === id) { return nodeTree[i]; diff --git a/src/misc/throttle.ts b/src/misc/throttle.ts index cc60c6aaf6..5455db72f8 100644 --- a/src/misc/throttle.ts +++ b/src/misc/throttle.ts @@ -8,14 +8,14 @@ export function throttle(limit: number, throttleKey: (args: any[]) => string) { return (target: any, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<(...args: any[]) => Promise>) => { const originalMethod: () => Promise = descriptor.value; - const allThrottles = new Map void>>>(); + const allThrottles = new Map void)[]>>(); const getThrottles = (obj: any) => { let throttles = allThrottles.get(obj); if (throttles != null) { return throttles; } - throttles = new Map void>>(); + throttles = new Map void)[]>(); allThrottles.set(obj, throttles); return throttles; }; diff --git a/src/models/domain/importResult.ts b/src/models/domain/importResult.ts index ab53fb1412..00534f4ae0 100644 --- a/src/models/domain/importResult.ts +++ b/src/models/domain/importResult.ts @@ -7,7 +7,7 @@ export class ImportResult { errorMessage: string; ciphers: CipherView[] = []; folders: FolderView[] = []; - folderRelationships: Array<[number, number]> = []; + folderRelationships: [number, number][] = []; collections: CollectionView[] = []; - collectionRelationships: Array<[number, number]> = []; + collectionRelationships: [number, number][] = []; } diff --git a/src/models/domain/treeNode.ts b/src/models/domain/treeNode.ts index beda19f158..9ea553bc00 100644 --- a/src/models/domain/treeNode.ts +++ b/src/models/domain/treeNode.ts @@ -1,7 +1,7 @@ export class TreeNode { parent: T; node: T; - children: Array> = []; + children: TreeNode[] = []; constructor(node: T, name: string, parent: T) { this.parent = parent; diff --git a/src/models/request/importCiphersRequest.ts b/src/models/request/importCiphersRequest.ts index cfa5df17ed..eec6260268 100644 --- a/src/models/request/importCiphersRequest.ts +++ b/src/models/request/importCiphersRequest.ts @@ -5,5 +5,5 @@ import { KvpRequest } from './kvpRequest'; export class ImportCiphersRequest { ciphers: CipherRequest[] = []; folders: FolderRequest[] = []; - folderRelationships: Array> = []; + folderRelationships: KvpRequest[] = []; } diff --git a/src/models/request/importOrganizationCiphersRequest.ts b/src/models/request/importOrganizationCiphersRequest.ts index c39e66e797..a19293c20a 100644 --- a/src/models/request/importOrganizationCiphersRequest.ts +++ b/src/models/request/importOrganizationCiphersRequest.ts @@ -5,5 +5,5 @@ import { KvpRequest } from './kvpRequest'; export class ImportOrganizationCiphersRequest { ciphers: CipherRequest[] = []; collections: CollectionRequest[] = []; - collectionRelationships: Array> = []; + collectionRelationships: KvpRequest[] = []; } diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 7b519a1a4f..5145f519b5 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -170,7 +170,7 @@ export class CipherService implements CipherServiceAbstraction { return null; } - const promises: Array> = []; + const promises: Promise[] = []; const encAttachments: Attachment[] = []; attachmentsModel.forEach(async (model) => { const attachment = new Attachment(); @@ -510,7 +510,7 @@ export class CipherService implements CipherServiceAbstraction { } async shareWithServer(cipher: CipherView, organizationId: string, collectionIds: string[]): Promise { - const attachmentPromises: Array> = []; + const attachmentPromises: Promise[] = []; if (cipher.attachments != null) { cipher.attachments.forEach((attachment) => { if (attachment.key == null) { @@ -531,7 +531,7 @@ export class CipherService implements CipherServiceAbstraction { } async shareManyWithServer(ciphers: CipherView[], organizationId: string, collectionIds: string[]): Promise { - const promises: Array> = []; + const promises: Promise[] = []; const encCiphers: Cipher[] = []; for (const cipher of ciphers) { cipher.organizationId = organizationId; diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index 39115e20bb..61e233ff65 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -51,7 +51,7 @@ export class CollectionService implements CollectionServiceAbstraction { return []; } const decCollections: CollectionView[] = []; - const promises: Array> = []; + const promises: Promise[] = []; collections.forEach((collection) => { promises.push(collection.decrypt().then((c) => decCollections.push(c))); }); @@ -98,11 +98,11 @@ export class CollectionService implements CollectionServiceAbstraction { return this.decryptedCollectionCache; } - async getAllNested(collections: CollectionView[] = null): Promise>> { + async getAllNested(collections: CollectionView[] = null): Promise[]> { if (collections == null) { collections = await this.getAllDecrypted(); } - const nodes: Array> = []; + const nodes: TreeNode[] = []; collections.forEach((c) => { const collectionCopy = new CollectionView(); collectionCopy.id = c.id; diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index 754fa30b42..dad533973a 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -81,7 +81,7 @@ export class FolderService implements FolderServiceAbstraction { } const decFolders: FolderView[] = []; - const promises: Array> = []; + const promises: Promise[] = []; const folders = await this.getAll(); folders.forEach((folder) => { promises.push(folder.decrypt().then((f) => decFolders.push(f))); @@ -98,9 +98,9 @@ export class FolderService implements FolderServiceAbstraction { return this.decryptedFolderCache; } - async getAllNested(): Promise>> { + async getAllNested(): Promise[]> { const folders = await this.getAllDecrypted(); - const nodes: Array> = []; + const nodes: TreeNode[] = []; folders.forEach((f) => { const folderCopy = new FolderView(); folderCopy.id = f.id; diff --git a/src/services/search.service.ts b/src/services/search.service.ts index a7149cafba..28bcbf048f 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -72,7 +72,7 @@ export class SearchService implements SearchServiceAbstraction { } async searchCiphers(query: string, - filter: (((cipher: CipherView) => boolean) | (Array<(cipher: CipherView) => boolean>)) = null, + filter: (((cipher: CipherView) => boolean) | (((cipher: CipherView) => boolean)[])) = null, ciphers: CipherView[] = null): Promise { const results: CipherView[] = []; From 5c62938dbbf3fe4146bf18a10827cd51a4157049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Mar=C3=AD?= Date: Wed, 12 Aug 2020 21:59:59 +0200 Subject: [PATCH 1052/1626] Add new method for cycling through every login (#142) * Add new method for cycling through every login To be used from browser extension when autofilling. Related PR: https://github.com/bitwarden/browser/pull/956 * Cache sorted ciphers by URL and invalidate them after a period of 5 seconds * Move file to models --- src/abstractions/cipher.service.ts | 1 + src/models/domain/sortedCiphersCache.ts | 60 +++++++++++++++++++++++++ src/services/cipher.service.ts | 27 ++++++++--- 3 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 src/models/domain/sortedCiphersCache.ts diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index 3c55b2cdbf..8ec886e240 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -24,6 +24,7 @@ export abstract class CipherService { getAllDecryptedForUrl: (url: string, includeOtherTypes?: CipherType[]) => Promise; getAllFromApiForOrganization: (organizationId: string) => Promise; getLastUsedForUrl: (url: string) => Promise; + getNextCipherForUrl: (url: string) => Promise; updateLastUsedDate: (id: string) => Promise; saveNeverDomain: (domain: string) => Promise; saveWithServer: (cipher: Cipher) => Promise; diff --git a/src/models/domain/sortedCiphersCache.ts b/src/models/domain/sortedCiphersCache.ts new file mode 100644 index 0000000000..6976904d9d --- /dev/null +++ b/src/models/domain/sortedCiphersCache.ts @@ -0,0 +1,60 @@ +import { CipherView } from '../view'; + +const CacheTTL = 5000; + +export class SortedCiphersCache { + private readonly sortedCiphersByUrl: Map = new Map(); + private readonly timeouts: Map = new Map(); + + constructor(private readonly comparator: (a: CipherView, b: CipherView) => number) { } + + isCached(url: string) { + return this.sortedCiphersByUrl.has(url); + } + + addCiphers(url: string, ciphers: CipherView[]) { + ciphers.sort(this.comparator); + this.sortedCiphersByUrl.set(url, new Ciphers(ciphers)); + this.resetTimer(url); + } + + getLastUsed(url: string) { + this.resetTimer(url); + return this.isCached(url) ? this.sortedCiphersByUrl.get(url).getLastUsed() : null; + } + + getNext(url: string) { + this.resetTimer(url); + return this.isCached(url) ? this.sortedCiphersByUrl.get(url).getNext() : null; + } + + clear() { + this.sortedCiphersByUrl.clear(); + this.timeouts.clear(); + } + + private resetTimer(url: string) { + clearTimeout(this.timeouts.get(url)); + this.timeouts.set(url, setTimeout(() => { + this.sortedCiphersByUrl.delete(url); + this.timeouts.delete(url); + }, CacheTTL)); + } +} + +class Ciphers { + lastUsedIndex = -1; + + constructor(private readonly ciphers: CipherView[]) { } + + getLastUsed() { + this.lastUsedIndex = Math.max(this.lastUsedIndex, 0); + return this.ciphers[this.lastUsedIndex]; + } + + getNext() { + const nextIndex = (this.lastUsedIndex + 1) % this.ciphers.length; + this.lastUsedIndex = nextIndex; + return this.ciphers[nextIndex]; + } +} diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 5145f519b5..fe661bdeea 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -35,6 +35,8 @@ import { FieldView } from '../models/view/fieldView'; import { PasswordHistoryView } from '../models/view/passwordHistoryView'; import { View } from '../models/view/view'; +import { SortedCiphersCache } from '../models/domain/sortedCiphersCache'; + import { ApiService } from '../abstractions/api.service'; import { CipherService as CipherServiceAbstraction } from '../abstractions/cipher.service'; import { CryptoService } from '../abstractions/crypto.service'; @@ -63,6 +65,8 @@ export class CipherService implements CipherServiceAbstraction { // tslint:disable-next-line _decryptedCipherCache: CipherView[]; + private sortedCiphersCache: SortedCiphersCache = new SortedCiphersCache(this.sortCiphersByLastUsed); + constructor(private cryptoService: CryptoService, private userService: UserService, private settingsService: SettingsService, private apiService: ApiService, private storageService: StorageService, private i18nService: I18nService, @@ -85,6 +89,7 @@ export class CipherService implements CipherServiceAbstraction { clearCache(): void { this.decryptedCipherCache = null; + this.sortedCiphersCache.clear(); } async encrypt(model: CipherView, key?: SymmetricCryptoKey, originalCipher: Cipher = null): Promise { @@ -437,13 +442,11 @@ export class CipherService implements CipherServiceAbstraction { } async getLastUsedForUrl(url: string): Promise { - const ciphers = await this.getAllDecryptedForUrl(url); - if (ciphers.length === 0) { - return null; - } + return this.getCipherForUrl(url, true); + } - const sortedCiphers = ciphers.sort(this.sortCiphersByLastUsed); - return sortedCiphers[0]; + async getNextCipherForUrl(url: string): Promise { + return this.getCipherForUrl(url, false); } async updateLastUsedDate(id: string): Promise { @@ -1002,4 +1005,16 @@ export class CipherService implements CipherServiceAbstraction { throw new Error('Unknown cipher type.'); } } + + private async getCipherForUrl(url: string, lastUsed: boolean): Promise { + if (!this.sortedCiphersCache.isCached(url)) { + const ciphers = await this.getAllDecryptedForUrl(url); + if (!ciphers) { + return null; + } + this.sortedCiphersCache.addCiphers(url, ciphers); + } + + return lastUsed ? this.sortedCiphersCache.getLastUsed(url) : this.sortedCiphersCache.getNext(url); + } } From ed6978baff5b129341bd46cc90a6155c1bcc5124 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 12 Aug 2020 16:38:32 -0400 Subject: [PATCH 1053/1626] add support for org identifier to api models (#149) --- src/models/request/organizationUpdateRequest.ts | 1 + src/models/response/organizationResponse.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/models/request/organizationUpdateRequest.ts b/src/models/request/organizationUpdateRequest.ts index ffb9149b34..306c20b305 100644 --- a/src/models/request/organizationUpdateRequest.ts +++ b/src/models/request/organizationUpdateRequest.ts @@ -1,5 +1,6 @@ export class OrganizationUpdateRequest { name: string; + identifier: string; businessName: string; billingEmail: string; } diff --git a/src/models/response/organizationResponse.ts b/src/models/response/organizationResponse.ts index 0859dca74c..c54aee077c 100644 --- a/src/models/response/organizationResponse.ts +++ b/src/models/response/organizationResponse.ts @@ -5,6 +5,7 @@ import { PlanType } from '../../enums/planType'; export class OrganizationResponse extends BaseResponse { id: string; + identifier: string; name: string; businessName: string; businessAddress1: string; @@ -28,6 +29,7 @@ export class OrganizationResponse extends BaseResponse { constructor(response: any) { super(response); this.id = this.getResponseProperty('Id'); + this.identifier = this.getResponseProperty('Identifier'); this.name = this.getResponseProperty('Name'); this.businessName = this.getResponseProperty('BusinessName'); this.businessAddress1 = this.getResponseProperty('BusinessAddress1'); From 7bf00b4fb37d85d8ffe7aa3248bd72f304297331 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 17 Aug 2020 10:34:52 -0400 Subject: [PATCH 1054/1626] extend functionality for set password flow (#150) --- .../components/change-password.component.ts | 72 +++++++++++-------- src/models/request/setPasswordRequest.ts | 10 ++- 2 files changed, 52 insertions(+), 30 deletions(-) diff --git a/src/angular/components/change-password.component.ts b/src/angular/components/change-password.component.ts index 830c0406d2..797170cb51 100644 --- a/src/angular/components/change-password.component.ts +++ b/src/angular/components/change-password.component.ts @@ -1,10 +1,6 @@ -import { - OnInit, -} from '@angular/core'; +import { OnInit } from '@angular/core'; -import { - Router, -} from '@angular/router'; +import { Router } from '@angular/router'; import { ApiService } from '../../abstractions/api.service'; import { CipherService } from '../../abstractions/cipher.service'; @@ -22,13 +18,18 @@ import { CipherString } from '../../models/domain/cipherString'; import { MasterPasswordPolicyOptions } from '../../models/domain/masterPasswordPolicyOptions'; import { SymmetricCryptoKey } from '../../models/domain/symmetricCryptoKey'; +import { KdfType } from '../../enums/kdfType'; + export class ChangePasswordComponent implements OnInit { - newMasterPassword: string; - confirmNewMasterPassword: string; + masterPassword: string; + masterPasswordRetype: string; formPromise: Promise; masterPasswordScore: number; enforcedPolicyOptions: MasterPasswordPolicyOptions; + protected kdf: KdfType; + protected kdfIterations: number; + private masterPasswordStrengthTimeout: any; private email: string; @@ -65,35 +66,29 @@ export class ChangePasswordComponent implements OnInit { } async submit() { - const hasEncKey = await this.cryptoService.hasEncKey(); - if (!hasEncKey) { - this.platformUtilsService.showToast('error', null, this.i18nService.t('updateKey')); - return; - } - - if (this.newMasterPassword == null || this.newMasterPassword === '') { + if (this.masterPassword == null || this.masterPassword === '') { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('masterPassRequired')); return; } - if (this.newMasterPassword.length < 8) { + if (this.masterPassword.length < 8) { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('masterPassLength')); return; } - if (this.newMasterPassword !== this.confirmNewMasterPassword) { + if (this.masterPassword !== this.masterPasswordRetype) { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('masterPassDoesntMatch')); return; } - const strengthResult = this.passwordGenerationService.passwordStrength(this.newMasterPassword, + const strengthResult = this.passwordGenerationService.passwordStrength(this.masterPassword, this.getPasswordStrengthUserInput()); if (this.enforcedPolicyOptions != null && !this.policyService.evaluateMasterPassword( strengthResult.score, - this.newMasterPassword, + this.masterPassword, this.enforcedPolicyOptions)) { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('masterPasswordPolicyRequirementsNotMet')); @@ -114,14 +109,25 @@ export class ChangePasswordComponent implements OnInit { } const email = await this.userService.getEmail(); - const kdf = await this.userService.getKdf(); - const kdfIterations = await this.userService.getKdfIterations(); - const newKey = await this.cryptoService.makeKey(this.newMasterPassword, email.trim().toLowerCase(), - kdf, kdfIterations); - const newMasterPasswordHash = await this.cryptoService.hashPassword(this.newMasterPassword, newKey); - const newEncKey = await this.cryptoService.remakeEncKey(newKey); + if (this.kdf == null) { + this.kdf = await this.userService.getKdf(); + } + if (this.kdfIterations == null) { + this.kdfIterations = await this.userService.getKdfIterations(); + } + const key = await this.cryptoService.makeKey(this.masterPassword, email.trim().toLowerCase(), + this.kdf, this.kdfIterations); + const masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key); - await this.performSubmitActions(newMasterPasswordHash, newKey, newEncKey); + let encKey: [SymmetricCryptoKey, CipherString] = null; + const existingEncKey = await this.cryptoService.getEncKey(); + if (existingEncKey == null) { + encKey = await this.cryptoService.makeEncKey(key); + } else { + encKey = await this.cryptoService.remakeEncKey(key); + } + + await this.performSubmitActions(masterPasswordHash, key, encKey); } async setupSubmitActions(): Promise { @@ -130,8 +136,8 @@ export class ChangePasswordComponent implements OnInit { return true; } - async performSubmitActions(newMasterPasswordHash: string, newKey: SymmetricCryptoKey, - newEncKey: [SymmetricCryptoKey, CipherString]) { + async performSubmitActions(masterPasswordHash: string, key: SymmetricCryptoKey, + encKey: [SymmetricCryptoKey, CipherString]) { // Override in sub-class } @@ -140,12 +146,20 @@ export class ChangePasswordComponent implements OnInit { clearTimeout(this.masterPasswordStrengthTimeout); } this.masterPasswordStrengthTimeout = setTimeout(() => { - const strengthResult = this.passwordGenerationService.passwordStrength(this.newMasterPassword, + const strengthResult = this.passwordGenerationService.passwordStrength(this.masterPassword, this.getPasswordStrengthUserInput()); this.masterPasswordScore = strengthResult == null ? null : strengthResult.score; }, 300); } + async logOut() { + const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('logOutConfirmation'), + this.i18nService.t('logOut'), this.i18nService.t('logOut'), this.i18nService.t('cancel')); + if (confirmed) { + this.messagingService.send('logout'); + } + } + private getPasswordStrengthUserInput() { let userInput: string[] = []; const atPosition = this.email.indexOf('@'); diff --git a/src/models/request/setPasswordRequest.ts b/src/models/request/setPasswordRequest.ts index 9593f50926..4907375121 100644 --- a/src/models/request/setPasswordRequest.ts +++ b/src/models/request/setPasswordRequest.ts @@ -1,4 +1,12 @@ +import { KeysRequest } from './keysRequest'; + +import { KdfType } from '../../enums/kdfType'; + export class SetPasswordRequest { - newMasterPasswordHash: string; + masterPasswordHash: string; key: string; + masterPasswordHint: string; + keys: KeysRequest; + kdf: KdfType; + kdfIterations: number; } From 8fe78916e25778962fa0f9d4542995451ab72a46 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Mon, 17 Aug 2020 18:14:40 +0200 Subject: [PATCH 1055/1626] Upgrade Angular to 9 (#151) * Upgrade Angular to 8 * Upgrade Angular to 9 * Fix format * Add ordered-imports tslint rule * Upgrade Angular CDK to 9.2.4 --- package-lock.json | 107 ++++++------------ package.json | 25 ++-- src/angular/components/add-edit.component.ts | 2 + .../components/attachments.component.ts | 2 + src/angular/components/ciphers.component.ts | 2 + .../components/collections.component.ts | 2 + .../components/environment.component.ts | 2 + src/angular/components/export.component.ts | 2 + .../components/folder-add-edit.component.ts | 2 + src/angular/components/groupings.component.ts | 2 + src/angular/components/login.component.ts | 2 + src/angular/components/modal.component.ts | 2 +- .../password-generator.component.ts | 2 + src/angular/components/share.component.ts | 2 + .../two-factor-options.component.ts | 3 +- src/angular/components/view.component.ts | 2 + .../directives/true-false-value.directive.ts | 2 +- tslint.json | 1 + 18 files changed, 78 insertions(+), 86 deletions(-) diff --git a/package-lock.json b/package-lock.json index 131cc03887..674cae9e9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,85 +5,52 @@ "requires": true, "dependencies": { "@angular/animations": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-7.2.1.tgz", - "integrity": "sha512-2AHc4HYz2cUVW3E0oYOTyUzBTnPJdtmVOx/Uo6+jnRqikvOGFOc5VXzFIYODe1Iiy+EYcSZ1lvQqwUbpZd6gwA==", - "requires": { - "tslib": "^1.9.0" - } + "version": "9.1.12", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-9.1.12.tgz", + "integrity": "sha512-tphpf9QHnOPoL2Jl7KpR+R5aHNW3oifLEmRUTajJYJGvo1uzdUDE82+V9OGOinxJsYseCth9gYJhN24aYTB9NA==" }, "@angular/cdk": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-7.2.1.tgz", - "integrity": "sha512-oU1Pjq3JkDtkXquLxWK84A2jOCeYRf352dVGbQCxWoSOQ5KBtMAd42huGidPiOSHN6/f7xZwL3n4fq3fVIut8A==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-9.2.4.tgz", + "integrity": "sha512-iw2+qHMXHYVC6K/fttHeNHIieSKiTEodVutZoOEcBu9rmRTGbLB26V/CRsfIRmA1RBk+uFYWc6UQZnMC3RdnJQ==", "requires": { - "parse5": "^5.0.0", - "tslib": "^1.7.1" + "parse5": "^5.0.0" } }, "@angular/common": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-7.2.1.tgz", - "integrity": "sha512-lYf3MeVMz69EriS5ANFY5PerJK0i4xHp/Jy67reb8ydZ+sfW320PUMuFtx3bZvk9PD7NdL3QZvXmla/ogrltTQ==", - "requires": { - "tslib": "^1.9.0" - } + "version": "9.1.12", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-9.1.12.tgz", + "integrity": "sha512-XSIqkbM6VV1yixF9zuzeE5eqN1VsiXS517K2VU0XgCRSAzhVhLOeKsdYjeLf7PdSu/HgW/Tr81H+isi9A9I0YA==" }, "@angular/compiler": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-7.2.1.tgz", - "integrity": "sha512-wf9w882hNoRaTDRqkEvQxV7nGB3liTX/LWEMunmm/Yz0nWkvgErR9pIHv3Sm4Ox0hyG3GdMpcVBzQ8qPomGOag==", - "requires": { - "tslib": "^1.9.0" - } + "version": "9.1.12", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-9.1.12.tgz", + "integrity": "sha512-suefk0OFkaJpUUKnV+phbL4T8fmVGHvzkereY5eqybQlumOez8NPL1PJcygAylh/E6OIAYm8SWookYwM6ZY9dg==" }, "@angular/core": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-7.2.1.tgz", - "integrity": "sha512-FYNAf4chxBoIVGCW2+fwR2MB2Ur5v1aG9L6zCcMXlZLbR64bu5j2m4e70RhXk/VptKvYWJ45od3xE5KfcaeEig==", - "requires": { - "tslib": "^1.9.0" - } + "version": "9.1.12", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-9.1.12.tgz", + "integrity": "sha512-WVA/eh3fzjx0apOzkKot4YRRUsGkHj50zFQWrAOMgivGaj1YVrvhf+m3hpglj5fn/BkLiFDl8RT0wAE8z9X+gQ==" }, "@angular/forms": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-7.2.1.tgz", - "integrity": "sha512-MxinNUvl02UfpY9gJtbTU6Mdt9fjIJYOGskcpwm+5u9jiMeRvOnG94ySoNrygg3EWwScX+P0mM4KN6fJWau7gQ==", - "requires": { - "tslib": "^1.9.0" - } + "version": "9.1.12", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-9.1.12.tgz", + "integrity": "sha512-LhjnZlC4WEsEsAJfOZLte+Lks3WBAFVeRv2lzoQNFVr/IMzBNDVfjEaaSqKF1cei3cjY39Df2nYDMJM7HfqbJA==" }, "@angular/platform-browser": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-7.2.1.tgz", - "integrity": "sha512-/6uHdFLmRkrkeOo+TzScrLG2YydG8kBNyT6ZpSOBf+bmB5DHyIGd55gh/tQJteKrnyadxRhqWCLBTYAbVX9Pnw==", - "requires": { - "tslib": "^1.9.0" - } + "version": "9.1.12", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-9.1.12.tgz", + "integrity": "sha512-rPa/hJcLfdId6bYB0b6pFUo3QIgjZlvUlmtKMGdrLNLYR8XQxPa2Y/UdN/5YeZ12htGw6GXrX9U8U7nTbUSpkw==" }, "@angular/platform-browser-dynamic": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.2.1.tgz", - "integrity": "sha512-hrSkI7aESEkqYnu628Z/LvYNlUNMqIqkXYAkT3urxFdCw7UwNeZKrDmd9sRwK3gK3sC1VeD9pXtqaKmGsnBjOA==", - "requires": { - "tslib": "^1.9.0" - } + "version": "9.1.12", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-9.1.12.tgz", + "integrity": "sha512-NmwUZaQeMnA6f+vP9Fp9P+qjL72H8dKlxLS76ujlKHVf75pP5oahWS8wfl7KXel1tKW3FQWMMffmKf5/NHRiSw==" }, "@angular/router": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-7.2.1.tgz", - "integrity": "sha512-3qMZnhFr6xx3dMy14rKwIw9ISTOZlsp9jAkthXVsfA2/thffScXHPBrH4SipkySLmOAtPmF5m5jscy8mx/1mJQ==", - "requires": { - "tslib": "^1.9.0" - } - }, - "@angular/upgrade": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/@angular/upgrade/-/upgrade-7.2.1.tgz", - "integrity": "sha512-2y41RbGt1MCEEGjeEpS32hL2sCAI39/BtqPq9L2yGfKdehVTavsh86Fpy8KHhLqgM9WxcisLqpwqj3IjXXkGBg==", - "requires": { - "tslib": "^1.9.0" - } + "version": "9.1.12", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-9.1.12.tgz", + "integrity": "sha512-+qCaXa9y0nsRhzjAYBqmGoQ2YkrdXgftZwuFDf6t4qEi30EXa0oS97KrlFq0M5GKdLIDGrbUm9PcdHSTOI+ZhA==" }, "@babel/code-frame": { "version": "7.0.0", @@ -7209,9 +7176,9 @@ "dev": true }, "rxjs": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", - "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", "requires": { "tslib": "^1.9.0" } @@ -8236,9 +8203,9 @@ } }, "typescript": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.5.tgz", - "integrity": "sha512-BEjlc0Z06ORZKbtcxGrIvvwYs5hAnuo6TKdNFL55frVDlB+na3z5bsLhFaIxmT+dPWgBIjMo6aNnTOgHHmHgiQ==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", "dev": true }, "uglify-js": { @@ -8766,9 +8733,9 @@ "dev": true }, "zone.js": { - "version": "0.8.28", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.28.tgz", - "integrity": "sha512-MjwlvV0wr65IQiT0WSHedo/zUhAqtypMdTUjqroV81RohGj1XANwHuC37dwYxphTRbZBYidk0gNS0dQrU2Q3Pw==" + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.9.1.tgz", + "integrity": "sha512-GkPiJL8jifSrKReKaTZ5jkhrMEgXbXYC+IPo1iquBjayRa0q86w3Dipjn8b415jpitMExe9lV8iTsv8tk3DGag==" }, "zxcvbn": { "version": "4.4.2", diff --git a/package.json b/package.json index db760393ee..2640a91aa1 100644 --- a/package.json +++ b/package.json @@ -59,19 +59,18 @@ "rimraf": "^2.6.2", "tslint": "^6.1.3", "typemoq": "^2.1.0", - "typescript": "3.6.5" + "typescript": "3.8.3" }, "dependencies": { - "@angular/animations": "7.2.1", - "@angular/cdk": "7.2.1", - "@angular/common": "7.2.1", - "@angular/compiler": "7.2.1", - "@angular/core": "7.2.1", - "@angular/forms": "7.2.1", - "@angular/platform-browser": "7.2.1", - "@angular/platform-browser-dynamic": "7.2.1", - "@angular/router": "7.2.1", - "@angular/upgrade": "7.2.1", + "@angular/animations": "9.1.12", + "@angular/cdk": "9.2.4", + "@angular/common": "9.1.12", + "@angular/compiler": "9.1.12", + "@angular/core": "9.1.12", + "@angular/forms": "9.1.12", + "@angular/platform-browser": "9.1.12", + "@angular/platform-browser-dynamic": "9.1.12", + "@angular/router": "9.1.12", "@microsoft/signalr": "3.1.0", "@microsoft/signalr-protocol-msgpack": "3.1.0", "@nodert-win10-rs4/windows.security.credentials.ui": "^0.4.4", @@ -95,9 +94,9 @@ "node-forge": "0.7.6", "open": "7.1.0", "papaparse": "4.6.0", - "rxjs": "6.3.3", + "rxjs": "6.6.2", "tldjs": "2.3.1", - "zone.js": "0.8.28", + "zone.js": "0.9.1", "zxcvbn": "4.4.2" } } diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index a0909cb2b8..16c0f87540 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -3,6 +3,7 @@ import { moveItemInArray, } from '@angular/cdk/drag-drop'; import { + Directive, EventEmitter, Input, OnInit, @@ -41,6 +42,7 @@ import { SecureNoteView } from '../../models/view/secureNoteView'; import { Utils } from '../../misc/utils'; +@Directive() export class AddEditComponent implements OnInit { @Input() cloneMode: boolean = false; @Input() folderId: string = null; diff --git a/src/angular/components/attachments.component.ts b/src/angular/components/attachments.component.ts index b3f8e7d0c8..5c8e00c137 100644 --- a/src/angular/components/attachments.component.ts +++ b/src/angular/components/attachments.component.ts @@ -1,4 +1,5 @@ import { + Directive, EventEmitter, Input, OnInit, @@ -16,6 +17,7 @@ import { Cipher } from '../../models/domain/cipher'; import { AttachmentView } from '../../models/view/attachmentView'; import { CipherView } from '../../models/view/cipherView'; +@Directive() export class AttachmentsComponent implements OnInit { @Input() cipherId: string; @Output() onUploadedAttachment = new EventEmitter(); diff --git a/src/angular/components/ciphers.component.ts b/src/angular/components/ciphers.component.ts index 50beab2f54..0cca07d3d6 100644 --- a/src/angular/components/ciphers.component.ts +++ b/src/angular/components/ciphers.component.ts @@ -1,4 +1,5 @@ import { + Directive, EventEmitter, Input, Output, @@ -8,6 +9,7 @@ import { SearchService } from '../../abstractions/search.service'; import { CipherView } from '../../models/view/cipherView'; +@Directive() export class CiphersComponent { @Input() activeCipherId: string = null; @Output() onCipherClicked = new EventEmitter(); diff --git a/src/angular/components/collections.component.ts b/src/angular/components/collections.component.ts index fcac85c010..c6be2fc985 100644 --- a/src/angular/components/collections.component.ts +++ b/src/angular/components/collections.component.ts @@ -1,4 +1,5 @@ import { + Directive, EventEmitter, Input, OnInit, @@ -15,6 +16,7 @@ import { CollectionView } from '../../models/view/collectionView'; import { Cipher } from '../../models/domain/cipher'; +@Directive() export class CollectionsComponent implements OnInit { @Input() cipherId: string; @Input() allowSelectNone = false; diff --git a/src/angular/components/environment.component.ts b/src/angular/components/environment.component.ts index 3a65af58a0..5a1b5c4562 100644 --- a/src/angular/components/environment.component.ts +++ b/src/angular/components/environment.component.ts @@ -1,4 +1,5 @@ import { + Directive, EventEmitter, Output, } from '@angular/core'; @@ -7,6 +8,7 @@ import { EnvironmentService } from '../../abstractions/environment.service'; import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +@Directive() export class EnvironmentComponent { @Output() onSaved = new EventEmitter(); diff --git a/src/angular/components/export.component.ts b/src/angular/components/export.component.ts index f7d3a0d31d..319134d519 100644 --- a/src/angular/components/export.component.ts +++ b/src/angular/components/export.component.ts @@ -1,4 +1,5 @@ import { + Directive, EventEmitter, Output, } from '@angular/core'; @@ -10,6 +11,7 @@ import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { EventType } from '../../enums/eventType'; +@Directive() export class ExportComponent { @Output() onSaved = new EventEmitter(); diff --git a/src/angular/components/folder-add-edit.component.ts b/src/angular/components/folder-add-edit.component.ts index 2515459c47..e1008de9c4 100644 --- a/src/angular/components/folder-add-edit.component.ts +++ b/src/angular/components/folder-add-edit.component.ts @@ -1,4 +1,5 @@ import { + Directive, EventEmitter, Input, OnInit, @@ -11,6 +12,7 @@ import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { FolderView } from '../../models/view/folderView'; +@Directive() export class FolderAddEditComponent implements OnInit { @Input() folderId: string; @Output() onSavedFolder = new EventEmitter(); diff --git a/src/angular/components/groupings.component.ts b/src/angular/components/groupings.component.ts index 3d93e7f890..f8f1c8cf2d 100644 --- a/src/angular/components/groupings.component.ts +++ b/src/angular/components/groupings.component.ts @@ -1,4 +1,5 @@ import { + Directive, EventEmitter, Input, Output, @@ -18,6 +19,7 @@ import { UserService } from '../../abstractions/user.service'; import { ConstantsService } from '../../services/constants.service'; +@Directive() export class GroupingsComponent { @Input() showFolders = true; @Input() showCollections = true; diff --git a/src/angular/components/login.component.ts b/src/angular/components/login.component.ts index 1e533dbad3..b55d1f270b 100644 --- a/src/angular/components/login.component.ts +++ b/src/angular/components/login.component.ts @@ -1,4 +1,5 @@ import { + Directive, Input, OnInit, } from '@angular/core'; @@ -25,6 +26,7 @@ const Keys = { rememberEmail: 'rememberEmail', }; +@Directive() export class LoginComponent implements OnInit { @Input() email: string = ''; @Input() rememberEmail = true; diff --git a/src/angular/components/modal.component.ts b/src/angular/components/modal.component.ts index d851710f24..b58c739a7f 100644 --- a/src/angular/components/modal.component.ts +++ b/src/angular/components/modal.component.ts @@ -20,7 +20,7 @@ export class ModalComponent implements OnDestroy { @Output() onClosed = new EventEmitter(); @Output() onShow = new EventEmitter(); @Output() onShown = new EventEmitter(); - @ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef; + @ViewChild('container', { read: ViewContainerRef, static: true }) container: ViewContainerRef; parentContainer: ViewContainerRef = null; fade: boolean = true; diff --git a/src/angular/components/password-generator.component.ts b/src/angular/components/password-generator.component.ts index 7b4fb07305..982db25ada 100644 --- a/src/angular/components/password-generator.component.ts +++ b/src/angular/components/password-generator.component.ts @@ -1,4 +1,5 @@ import { + Directive, EventEmitter, Input, OnInit, @@ -11,6 +12,7 @@ import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { PasswordGeneratorPolicyOptions } from '../../models/domain/passwordGeneratorPolicyOptions'; +@Directive() export class PasswordGeneratorComponent implements OnInit { @Input() showSelect: boolean = false; @Output() onSelected = new EventEmitter(); diff --git a/src/angular/components/share.component.ts b/src/angular/components/share.component.ts index 956178fb71..3cfc5d8eb5 100644 --- a/src/angular/components/share.component.ts +++ b/src/angular/components/share.component.ts @@ -1,4 +1,5 @@ import { + Directive, EventEmitter, Input, OnInit, @@ -19,6 +20,7 @@ import { CollectionView } from '../../models/view/collectionView'; import { Utils } from '../../misc/utils'; +@Directive() export class ShareComponent implements OnInit { @Input() cipherId: string; @Input() organizationId: string; diff --git a/src/angular/components/two-factor-options.component.ts b/src/angular/components/two-factor-options.component.ts index 875dbb8510..88e3ad42c4 100644 --- a/src/angular/components/two-factor-options.component.ts +++ b/src/angular/components/two-factor-options.component.ts @@ -1,6 +1,6 @@ import { + Directive, EventEmitter, - Input, OnInit, Output, } from '@angular/core'; @@ -12,6 +12,7 @@ import { AuthService } from '../../abstractions/auth.service'; import { I18nService } from '../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +@Directive() export class TwoFactorOptionsComponent implements OnInit { @Output() onProviderSelected = new EventEmitter(); @Output() onRecoverSelected = new EventEmitter(); diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index 4737b21306..067e6c6953 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectorRef, + Directive, EventEmitter, Input, NgZone, @@ -30,6 +31,7 @@ import { BroadcasterService } from '../services/broadcaster.service'; const BroadcasterSubscriptionId = 'ViewComponent'; +@Directive() export class ViewComponent implements OnDestroy, OnInit { @Input() cipherId: string; @Output() onEditCipher = new EventEmitter(); diff --git a/src/angular/directives/true-false-value.directive.ts b/src/angular/directives/true-false-value.directive.ts index 12729b90f1..6dcf628e2d 100644 --- a/src/angular/directives/true-false-value.directive.ts +++ b/src/angular/directives/true-false-value.directive.ts @@ -8,8 +8,8 @@ import { } from '@angular/core'; import { ControlValueAccessor, - NG_VALUE_ACCESSOR, NgControl, + NG_VALUE_ACCESSOR, } from '@angular/forms'; // ref: https://juristr.com/blog/2018/02/ng-true-value-directive/ diff --git a/tslint.json b/tslint.json index bb9ef3827d..41090a98e1 100644 --- a/tslint.json +++ b/tslint.json @@ -38,6 +38,7 @@ "no-empty": [ true, "allow-empty-catch" ], "object-literal-sort-keys": false, "object-literal-shorthand": [ true, "never" ], + "ordered-imports": true, "prefer-for-of": false, "quotemark": [ true, "single" ], "whitespace": [ From 9957125d3a9d416d7a60b9904f0b7882f3fb58d0 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 17 Aug 2020 14:27:44 -0400 Subject: [PATCH 1056/1626] dont set keys if not there on sso (#152) * dont set keys if not there on sso * a comment --- src/services/auth.service.ts | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 074a32ae77..5633d66dda 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -308,21 +308,25 @@ export class AuthService implements AuthServiceAbstraction { if (hashedPassword != null) { await this.cryptoService.setKeyHash(hashedPassword); } - await this.cryptoService.setEncKey(tokenResponse.key); - // User doesn't have a key pair yet (old account), let's generate one for them - if (tokenResponse.privateKey == null) { - try { - const keyPair = await this.cryptoService.makeKeyPair(); - await this.apiService.postAccountKeys(new KeysRequest(keyPair[0], keyPair[1].encryptedString)); - tokenResponse.privateKey = keyPair[1].encryptedString; - } catch (e) { - // tslint:disable-next-line - console.error(e); + // Skip this step during SSO new user flow. No key is returned from server. + if (code == null || tokenResponse.key != null) { + await this.cryptoService.setEncKey(tokenResponse.key); + + // User doesn't have a key pair yet (old account), let's generate one for them + if (tokenResponse.privateKey == null) { + try { + const keyPair = await this.cryptoService.makeKeyPair(); + await this.apiService.postAccountKeys(new KeysRequest(keyPair[0], keyPair[1].encryptedString)); + tokenResponse.privateKey = keyPair[1].encryptedString; + } catch (e) { + // tslint:disable-next-line + console.error(e); + } } - } - await this.cryptoService.setEncPrivateKey(tokenResponse.privateKey); + await this.cryptoService.setEncPrivateKey(tokenResponse.privateKey); + } } if (this.vaultTimeoutService != null) { From 5d874d07b35a23dc6d54f1f435d88d2ddd815e33 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 19 Aug 2020 10:57:35 -0400 Subject: [PATCH 1057/1626] abstract set password component to jslib (#153) --- .../components/set-password.component.ts | 90 +++++++++++++++++++ src/angular/components/sso.component.ts | 2 +- 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/angular/components/set-password.component.ts diff --git a/src/angular/components/set-password.component.ts b/src/angular/components/set-password.component.ts new file mode 100644 index 0000000000..d2d33817ef --- /dev/null +++ b/src/angular/components/set-password.component.ts @@ -0,0 +1,90 @@ +import { + ActivatedRoute, + Router, +} from '@angular/router'; + +import { ApiService } from '../../abstractions/api.service'; +import { CipherService } from '../../abstractions/cipher.service'; +import { CryptoService } from '../../abstractions/crypto.service'; +import { FolderService } from '../../abstractions/folder.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { MessagingService } from '../../abstractions/messaging.service'; +import { PasswordGenerationService } from '../../abstractions/passwordGeneration.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { PolicyService } from '../../abstractions/policy.service'; +import { SyncService } from '../../abstractions/sync.service'; +import { UserService } from '../../abstractions/user.service'; + +import { CipherString } from '../../models/domain/cipherString'; +import { SymmetricCryptoKey } from '../../models/domain/symmetricCryptoKey'; + +import { KeysRequest } from '../../models/request/keysRequest'; +import { SetPasswordRequest } from '../../models/request/setPasswordRequest'; + +import { ChangePasswordComponent as BaseChangePasswordComponent } from './change-password.component'; + +import { KdfType } from '../../enums/kdfType'; + +export class SetPasswordComponent extends BaseChangePasswordComponent { + showPassword: boolean = false; + hint: string = ''; + + onSuccessfulChangePassword: () => Promise; + successRoute = 'vault'; + + constructor(apiService: ApiService, i18nService: I18nService, + cryptoService: CryptoService, messagingService: MessagingService, + userService: UserService, passwordGenerationService: PasswordGenerationService, + platformUtilsService: PlatformUtilsService, folderService: FolderService, + cipherService: CipherService, syncService: SyncService, + policyService: PolicyService, router: Router, private route: ActivatedRoute) { + super(apiService, i18nService, cryptoService, messagingService, userService, passwordGenerationService, + platformUtilsService, folderService, cipherService, syncService, policyService, router); + } + + async setupSubmitActions() { + this.kdf = KdfType.PBKDF2_SHA256; + const useLowerKdf = this.platformUtilsService.isEdge() || this.platformUtilsService.isIE(); + this.kdfIterations = useLowerKdf ? 10000 : 100000; + return true; + } + + async performSubmitActions(masterPasswordHash: string, key: SymmetricCryptoKey, + encKey: [SymmetricCryptoKey, CipherString]) { + const request = new SetPasswordRequest(); + request.masterPasswordHash = masterPasswordHash; + request.key = encKey[1].encryptedString; + request.masterPasswordHint = this.hint; + request.kdf = this.kdf; + request.kdfIterations = this.kdfIterations; + + const keys = await this.cryptoService.makeKeyPair(encKey[0]); + request.keys = new KeysRequest(keys[0], keys[1].encryptedString); + + try { + this.formPromise = this.apiService.setPassword(request); + await this.formPromise; + + await this.userService.setInformation(await this.userService.getUserId(), await this.userService.getEmail(), + this.kdf, this.kdfIterations); + await this.cryptoService.setKey(key); + await this.cryptoService.setKeyHash(masterPasswordHash); + await this.cryptoService.setEncKey(encKey[1].encryptedString); + await this.cryptoService.setEncPrivateKey(keys[1].encryptedString); + + if (this.onSuccessfulChangePassword != null) { + this.onSuccessfulChangePassword(); + } else { + this.router.navigate([this.successRoute]); + } + } catch { + this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred')); + } + } + + togglePassword(confirmField: boolean) { + this.platformUtilsService.eventTrack('Toggled Master Password on Set Password'); + this.showPassword = !this.showPassword; + document.getElementById(confirmField ? 'masterPasswordRetype' : 'masterPassword').focus(); + } +} diff --git a/src/angular/components/sso.component.ts b/src/angular/components/sso.component.ts index 9c537e8814..5b873bf4fb 100644 --- a/src/angular/components/sso.component.ts +++ b/src/angular/components/sso.component.ts @@ -30,7 +30,7 @@ export class SsoComponent { protected twoFactorRoute = '2fa'; protected successRoute = 'lock'; - protected changePasswordRoute = 'change-password'; + protected changePasswordRoute = 'set-password'; protected clientId: string; protected redirectUri: string; protected state: string; From 6ab444a9868c32ac53761f9d233d816744da9a1d Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Fri, 21 Aug 2020 12:25:33 -0500 Subject: [PATCH 1058/1626] Updated depdency chain -> end result to allieviate issues in directory connector (#155) --- .../components/change-password.component.ts | 16 ++++----------- .../components/set-password.component.ts | 20 ++++++------------- 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/src/angular/components/change-password.component.ts b/src/angular/components/change-password.component.ts index 797170cb51..f4ec66e31e 100644 --- a/src/angular/components/change-password.component.ts +++ b/src/angular/components/change-password.component.ts @@ -1,17 +1,11 @@ import { OnInit } from '@angular/core'; -import { Router } from '@angular/router'; - -import { ApiService } from '../../abstractions/api.service'; -import { CipherService } from '../../abstractions/cipher.service'; import { CryptoService } from '../../abstractions/crypto.service'; -import { FolderService } from '../../abstractions/folder.service'; import { I18nService } from '../../abstractions/i18n.service'; import { MessagingService } from '../../abstractions/messaging.service'; import { PasswordGenerationService } from '../../abstractions/passwordGeneration.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { PolicyService } from '../../abstractions/policy.service'; -import { SyncService } from '../../abstractions/sync.service'; import { UserService } from '../../abstractions/user.service'; import { CipherString } from '../../models/domain/cipherString'; @@ -33,12 +27,10 @@ export class ChangePasswordComponent implements OnInit { private masterPasswordStrengthTimeout: any; private email: string; - constructor(protected apiService: ApiService, protected i18nService: I18nService, - protected cryptoService: CryptoService, protected messagingService: MessagingService, - protected userService: UserService, protected passwordGenerationService: PasswordGenerationService, - protected platformUtilsService: PlatformUtilsService, protected folderService: FolderService, - protected cipherService: CipherService, protected syncService: SyncService, - protected policyService: PolicyService, protected router: Router) { } + constructor(protected i18nService: I18nService, protected cryptoService: CryptoService, + protected messagingService: MessagingService, protected userService: UserService, + protected passwordGenerationService: PasswordGenerationService, + protected platformUtilsService: PlatformUtilsService, protected policyService: PolicyService) { } async ngOnInit() { this.email = await this.userService.getEmail(); diff --git a/src/angular/components/set-password.component.ts b/src/angular/components/set-password.component.ts index d2d33817ef..4bb1932967 100644 --- a/src/angular/components/set-password.component.ts +++ b/src/angular/components/set-password.component.ts @@ -1,18 +1,12 @@ -import { - ActivatedRoute, - Router, -} from '@angular/router'; +import { Router } from '@angular/router'; import { ApiService } from '../../abstractions/api.service'; -import { CipherService } from '../../abstractions/cipher.service'; import { CryptoService } from '../../abstractions/crypto.service'; -import { FolderService } from '../../abstractions/folder.service'; import { I18nService } from '../../abstractions/i18n.service'; import { MessagingService } from '../../abstractions/messaging.service'; import { PasswordGenerationService } from '../../abstractions/passwordGeneration.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { PolicyService } from '../../abstractions/policy.service'; -import { SyncService } from '../../abstractions/sync.service'; import { UserService } from '../../abstractions/user.service'; import { CipherString } from '../../models/domain/cipherString'; @@ -32,14 +26,12 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { onSuccessfulChangePassword: () => Promise; successRoute = 'vault'; - constructor(apiService: ApiService, i18nService: I18nService, - cryptoService: CryptoService, messagingService: MessagingService, + constructor(i18nService: I18nService, cryptoService: CryptoService, messagingService: MessagingService, userService: UserService, passwordGenerationService: PasswordGenerationService, - platformUtilsService: PlatformUtilsService, folderService: FolderService, - cipherService: CipherService, syncService: SyncService, - policyService: PolicyService, router: Router, private route: ActivatedRoute) { - super(apiService, i18nService, cryptoService, messagingService, userService, passwordGenerationService, - platformUtilsService, folderService, cipherService, syncService, policyService, router); + platformUtilsService: PlatformUtilsService, policyService: PolicyService, private router: Router, + private apiService: ApiService) { + super(i18nService, cryptoService, messagingService, userService, passwordGenerationService, + platformUtilsService, policyService); } async setupSubmitActions() { From e55528e61737635e7f8970b913bcc3f10bede85d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 24 Aug 2020 12:21:17 -0400 Subject: [PATCH 1059/1626] adds support for yoti csv importer (#157) --- src/importers/yotiCsvImporter.ts | 28 ++++++++++++++++++++++++++++ src/services/import.service.ts | 4 ++++ 2 files changed, 32 insertions(+) create mode 100644 src/importers/yotiCsvImporter.ts diff --git a/src/importers/yotiCsvImporter.ts b/src/importers/yotiCsvImporter.ts new file mode 100644 index 0000000000..2651e6fa70 --- /dev/null +++ b/src/importers/yotiCsvImporter.ts @@ -0,0 +1,28 @@ +import { BaseImporter } from './baseImporter'; +import { Importer } from './importer'; + +import { ImportResult } from '../models/domain/importResult'; + +export class YotiCsvImporter extends BaseImporter implements Importer { + parse(data: string): ImportResult { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return result; + } + + results.forEach((value) => { + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(value.Name, '--'); + cipher.login.username = this.getValueOrDefault(value['User name']); + cipher.login.password = this.getValueOrDefault(value.Password); + cipher.login.uris = this.makeUriArray(value.URL); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return result; + } +} diff --git a/src/services/import.service.ts b/src/services/import.service.ts index e7b58550ca..a74fc2c62d 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -72,6 +72,7 @@ import { SplashIdCsvImporter } from '../importers/splashIdCsvImporter'; import { StickyPasswordXmlImporter } from '../importers/stickyPasswordXmlImporter'; import { TrueKeyCsvImporter } from '../importers/truekeyCsvImporter'; import { UpmCsvImporter } from '../importers/upmCsvImporter'; +import { YotiCsvImporter } from '../importers/yotiCsvImporter'; import { ZohoVaultCsvImporter } from '../importers/zohoVaultCsvImporter'; export class ImportService implements ImportServiceAbstraction { @@ -131,6 +132,7 @@ export class ImportService implements ImportServiceAbstraction { { id: 'buttercupcsv', name: 'Buttercup (csv)' }, { id: 'codebookcsv', name: 'Codebook (csv)' }, { id: 'encryptrcsv', name: 'Encryptr (csv)' }, + { id: 'yoticsv', name: 'Yoti (csv)' }, ]; constructor(private cipherService: CipherService, private folderService: FolderService, @@ -279,6 +281,8 @@ export class ImportService implements ImportServiceAbstraction { return new CodebookCsvImporter(); case 'encryptrcsv': return new EncryptrCsvImporter(); + case 'yoticsv': + return new YotiCsvImporter(); default: return null; } From 8f27110754d7a492ebbd5e3bb9b9b99c9ab9869b Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Wed, 26 Aug 2020 10:54:16 -0500 Subject: [PATCH 1060/1626] [SSO] Bug - Fixed set password route (#156) * Fixed 2fa + set password bug// moved query params parsing in shared lib * Removed unnecessary params parse // added auth result conditional for success route --- src/angular/components/two-factor.component.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 16a7d1032d..72c92bdf89 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -2,6 +2,7 @@ import { OnDestroy, OnInit, } from '@angular/core'; + import { Router } from '@angular/router'; import { DeviceType } from '../../enums/deviceType'; @@ -9,6 +10,8 @@ import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; import { TwoFactorEmailRequest } from '../../models/request/twoFactorEmailRequest'; +import { AuthResult } from '../../models/domain'; + import { ApiService } from '../../abstractions/api.service'; import { AuthService } from '../../abstractions/auth.service'; import { EnvironmentService } from '../../abstractions/environment.service'; @@ -37,7 +40,6 @@ export class TwoFactorComponent implements OnInit, OnDestroy { twoFactorEmail: string = null; formPromise: Promise; emailPromise: Promise; - resetMasterPassword: boolean = false; onSuccessfulLogin: () => Promise; onSuccessfulLoginNavigate: () => Promise; @@ -60,7 +62,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { } if (this.authService.authingWithSso()) { - this.successRoute = this.resetMasterPassword ? 'reset-master-password' : 'lock'; + this.successRoute = 'lock'; } if (this.initU2f && this.win != null && this.u2fSupported) { @@ -176,7 +178,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { try { this.formPromise = this.authService.logInTwoFactor(this.selectedProviderType, this.token, this.remember); - await this.formPromise; + const response: AuthResult = await this.formPromise; const disableFavicon = await this.storageService.get(ConstantsService.disableFaviconKey); await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon); if (this.onSuccessfulLogin != null) { @@ -186,6 +188,9 @@ export class TwoFactorComponent implements OnInit, OnDestroy { if (this.onSuccessfulLoginNavigate != null) { this.onSuccessfulLoginNavigate(); } else { + if (response.resetMasterPassword) { + this.successRoute = 'set-password'; + } this.router.navigate([this.successRoute]); } } catch { From e07526a1b6cae520561e452a6695a1508d785585 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Thu, 27 Aug 2020 11:00:05 -0400 Subject: [PATCH 1061/1626] Link existing user to sso (#158) * facilite linking an existing user to an org sso * fixed a broken import * added ssoBound and identifier to an org model * added user identifier to sso callout url * changed url for delete sso user api method * facilite linking an existing user to an org sso * fixed a broken import * added ssoBound and identifier to an org model * added user identifier to sso callout url * changed url for delete sso user api method * added a token to the existing user sso link flow * facilite linking an existing user to an org sso * fixed a broken import * facilite linking an existing user to an org sso * fixed a broken import * added ssoBound and identifier to an org model * added user identifier to sso callout url * changed url for delete sso user api method * added a token to the existing user sso link flow * facilite linking an existing user to an org sso * fixed a broken import * removed an extra line * encoded the user identifier on sso link * code review cleanup for link sso * removed a blank line --- src/abstractions/api.service.ts | 3 ++ src/angular/components/sso.component.ts | 28 +++++++++++++++---- src/models/data/organizationData.ts | 6 ++++ src/models/domain/organization.ts | 6 ++++ .../response/profileOrganizationResponse.ts | 6 ++++ src/services/api.service.ts | 23 ++++++++++----- 6 files changed, 60 insertions(+), 12 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 266c1c68ae..f670c9ca00 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -293,6 +293,9 @@ export abstract class ApiService { start: string, end: string, token: string) => Promise>; postEventsCollect: (request: EventRequest[]) => Promise; + deleteSsoUser: (organizationId: string) => Promise; + getSsoUserIdentifier: () => Promise; + getUserPublicKey: (id: string) => Promise; getHibpBreach: (username: string) => Promise; diff --git a/src/angular/components/sso.component.ts b/src/angular/components/sso.component.ts index 5b873bf4fb..563307ff75 100644 --- a/src/angular/components/sso.component.ts +++ b/src/angular/components/sso.component.ts @@ -66,7 +66,15 @@ export class SsoComponent { }); } - async submit() { + async submit(returnUri?: string, includeUserIdentifier?: boolean) { + const authorizeUrl = await this.buildAuthorizeUrl(returnUri, includeUserIdentifier); + this.platformUtilsService.launchUri(authorizeUrl, { sameWindow: true }); + } + + protected async buildAuthorizeUrl(returnUri?: string, includeUserIdentifier?: boolean): Promise { + let codeChallenge = this.codeChallenge; + let state = this.state; + const passwordOptions: any = { type: 'password', length: 64, @@ -75,26 +83,36 @@ export class SsoComponent { numbers: true, special: false, }; - let codeChallenge = this.codeChallenge; - let state = this.state; + if (codeChallenge == null) { const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, 'sha256'); codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); await this.storageService.save(ConstantsService.ssoCodeVerifierKey, codeVerifier); } + if (state == null) { state = await this.passwordGenerationService.generatePassword(passwordOptions); + if (returnUri) { + state += `_returnUri='${returnUri}'`; + } + await this.storageService.save(ConstantsService.ssoStateKey, state); } - const authorizeUrl = this.apiService.identityBaseUrl + '/connect/authorize?' + + let authorizeUrl = this.apiService.identityBaseUrl + '/connect/authorize?' + 'client_id=' + this.clientId + '&redirect_uri=' + encodeURIComponent(this.redirectUri) + '&' + 'response_type=code&scope=api offline_access&' + 'state=' + state + '&code_challenge=' + codeChallenge + '&' + 'code_challenge_method=S256&response_mode=query&' + 'domain_hint=' + encodeURIComponent(this.identifier); - this.platformUtilsService.launchUri(authorizeUrl, { sameWindow: true }); + + if (includeUserIdentifier) { + const userIdentifier = await this.apiService.getSsoUserIdentifier(); + authorizeUrl += `&user_identifier=${encodeURIComponent(userIdentifier)}`; + } + + return authorizeUrl; } private async logIn(code: string, codeVerifier: string) { diff --git a/src/models/data/organizationData.ts b/src/models/data/organizationData.ts index 4b0fc7694c..d6d10dbbd4 100644 --- a/src/models/data/organizationData.ts +++ b/src/models/data/organizationData.ts @@ -17,11 +17,14 @@ export class OrganizationData { use2fa: boolean; useApi: boolean; useBusinessPortal: boolean; + useSso: boolean; selfHost: boolean; usersGetPremium: boolean; seats: number; maxCollections: number; maxStorageGb?: number; + ssoBound: boolean; + identifier: string; constructor(response: ProfileOrganizationResponse) { this.id = response.id; @@ -37,10 +40,13 @@ export class OrganizationData { this.use2fa = response.use2fa; this.useApi = response.useApi; this.useBusinessPortal = response.useBusinessPortal; + this.useSso = response.useSso; this.selfHost = response.selfHost; this.usersGetPremium = response.usersGetPremium; this.seats = response.seats; this.maxCollections = response.maxCollections; this.maxStorageGb = response.maxStorageGb; + this.ssoBound = response.ssoBound; + this.identifier = response.identifier; } } diff --git a/src/models/domain/organization.ts b/src/models/domain/organization.ts index 21011fb095..f616f2e1c3 100644 --- a/src/models/domain/organization.ts +++ b/src/models/domain/organization.ts @@ -17,11 +17,14 @@ export class Organization { use2fa: boolean; useApi: boolean; useBusinessPortal: boolean; + useSso: boolean; selfHost: boolean; usersGetPremium: boolean; seats: number; maxCollections: number; maxStorageGb?: number; + ssoBound: boolean; + identifier: string; constructor(obj?: OrganizationData) { if (obj == null) { @@ -41,11 +44,14 @@ export class Organization { this.use2fa = obj.use2fa; this.useApi = obj.useApi; this.useBusinessPortal = obj.useBusinessPortal; + this.useSso = obj.useSso; this.selfHost = obj.selfHost; this.usersGetPremium = obj.usersGetPremium; this.seats = obj.seats; this.maxCollections = obj.maxCollections; this.maxStorageGb = obj.maxStorageGb; + this.ssoBound = obj.ssoBound; + this.identifier = obj.identifier; } get canAccess() { diff --git a/src/models/response/profileOrganizationResponse.ts b/src/models/response/profileOrganizationResponse.ts index f14ba9714b..91a4c350d6 100644 --- a/src/models/response/profileOrganizationResponse.ts +++ b/src/models/response/profileOrganizationResponse.ts @@ -14,6 +14,7 @@ export class ProfileOrganizationResponse extends BaseResponse { use2fa: boolean; useApi: boolean; useBusinessPortal: boolean; + useSso: boolean; selfHost: boolean; usersGetPremium: boolean; seats: number; @@ -23,6 +24,8 @@ export class ProfileOrganizationResponse extends BaseResponse { status: OrganizationUserStatusType; type: OrganizationUserType; enabled: boolean; + ssoBound: boolean; + identifier: string; constructor(response: any) { super(response); @@ -36,6 +39,7 @@ export class ProfileOrganizationResponse extends BaseResponse { this.use2fa = this.getResponseProperty('Use2fa'); this.useApi = this.getResponseProperty('UseApi'); this.useBusinessPortal = this.getResponseProperty('UseBusinessPortal'); + this.useSso = this.getResponseProperty('UseSso'); this.selfHost = this.getResponseProperty('SelfHost'); this.usersGetPremium = this.getResponseProperty('UsersGetPremium'); this.seats = this.getResponseProperty('Seats'); @@ -45,5 +49,7 @@ export class ProfileOrganizationResponse extends BaseResponse { this.status = this.getResponseProperty('Status'); this.type = this.getResponseProperty('Type'); this.enabled = this.getResponseProperty('Enabled'); + this.ssoBound = this.getResponseProperty('SsoBound'); + this.identifier = this.getResponseProperty('Identifier'); } } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index f79178e2a7..22cb00f508 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -348,6 +348,14 @@ export class ApiService implements ApiServiceAbstraction { return r as string; } + async deleteSsoUser(organizationId: string): Promise { + return this.send('DELETE', '/accounts/sso/' + organizationId, null, true, false); + } + + async getSsoUserIdentifier(): Promise { + return this.send('GET', '/accounts/sso/user-identifier', null, true, true) + } + // Folder APIs async getFolder(id: string): Promise { @@ -693,13 +701,6 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, PlanResponse); } - // Sync APIs - - async getSync(): Promise { - const path = this.isDesktopClient || this.isWebClient ? '/sync?excludeDomains=true' : '/sync'; - const r = await this.send('GET', path, null, true, true); - return new SyncResponse(r); - } async postImportDirectory(organizationId: string, request: ImportDirectoryRequest): Promise { return this.send('POST', '/organizations/' + organizationId + '/import', request, true, false); @@ -717,6 +718,14 @@ export class ApiService implements ApiServiceAbstraction { return new DomainsResponse(r); } + // Sync APIs + + async getSync(): Promise { + const path = this.isDesktopClient || this.isWebClient ? '/sync?excludeDomains=true' : '/sync'; + const r = await this.send('GET', path, null, true, true); + return new SyncResponse(r); + } + // Two-factor APIs async getTwoFactorProviders(): Promise> { From 42d5784ef354a4f3a875570a28111bbbaf438770 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Thu, 27 Aug 2020 13:24:38 -0500 Subject: [PATCH 1062/1626] [SSO] Set Password flow sync (#159) * Added sync service to set-password // Added error for CLI in set-password flow * Updated error string // added sync tracking boolean * VS code failed to saved latest change - resaving false loading --- src/angular/components/set-password.component.ts | 10 +++++++++- src/cli/commands/login.command.ts | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/angular/components/set-password.component.ts b/src/angular/components/set-password.component.ts index 4bb1932967..94fe16326d 100644 --- a/src/angular/components/set-password.component.ts +++ b/src/angular/components/set-password.component.ts @@ -7,6 +7,7 @@ import { MessagingService } from '../../abstractions/messaging.service'; import { PasswordGenerationService } from '../../abstractions/passwordGeneration.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { PolicyService } from '../../abstractions/policy.service'; +import { SyncService } from '../../abstractions/sync.service'; import { UserService } from '../../abstractions/user.service'; import { CipherString } from '../../models/domain/cipherString'; @@ -20,6 +21,7 @@ import { ChangePasswordComponent as BaseChangePasswordComponent } from './change import { KdfType } from '../../enums/kdfType'; export class SetPasswordComponent extends BaseChangePasswordComponent { + syncLoading: boolean = true; showPassword: boolean = false; hint: string = ''; @@ -29,11 +31,17 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { constructor(i18nService: I18nService, cryptoService: CryptoService, messagingService: MessagingService, userService: UserService, passwordGenerationService: PasswordGenerationService, platformUtilsService: PlatformUtilsService, policyService: PolicyService, private router: Router, - private apiService: ApiService) { + private apiService: ApiService, private syncService: SyncService) { super(i18nService, cryptoService, messagingService, userService, passwordGenerationService, platformUtilsService, policyService); } + async ngOnInit() { + await this.syncService.fullSync(true); + this.syncLoading = false; + super.ngOnInit(); + } + async setupSubmitActions() { this.kdf = KdfType.PBKDF2_SHA256; const useLowerKdf = this.platformUtilsService.isEdge() || this.platformUtilsService.isIE(); diff --git a/src/cli/commands/login.command.ts b/src/cli/commands/login.command.ts index 9d6e6bf243..acccd4f4d2 100644 --- a/src/cli/commands/login.command.ts +++ b/src/cli/commands/login.command.ts @@ -200,6 +200,11 @@ export class LoginCommand { return Response.error('Login failed.'); } + if (response.resetMasterPassword) { + return Response.error('In order to log in with SSO from the CLI, you must first log in' + + ' through the web vault to set your master password.'); + } + if (this.success != null) { const res = await this.success(); return Response.success(res); From 700e945008edb38543d619e9dd4543908c057f5c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 27 Aug 2020 14:58:34 -0400 Subject: [PATCH 1063/1626] reduce cache ttl to 3 seconds --- src/models/domain/sortedCiphersCache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/domain/sortedCiphersCache.ts b/src/models/domain/sortedCiphersCache.ts index 6976904d9d..9f11f73db7 100644 --- a/src/models/domain/sortedCiphersCache.ts +++ b/src/models/domain/sortedCiphersCache.ts @@ -1,6 +1,6 @@ import { CipherView } from '../view'; -const CacheTTL = 5000; +const CacheTTL = 3000; export class SortedCiphersCache { private readonly sortedCiphersByUrl: Map = new Map(); From bffec57e02a5a95dab556d77dadf03dec9ce90cd Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 3 Sep 2020 10:35:24 -0400 Subject: [PATCH 1064/1626] change to environmentService.getWebVaultUrl (#162) --- src/cli/commands/login.command.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cli/commands/login.command.ts b/src/cli/commands/login.command.ts index acccd4f4d2..51aff1f1a6 100644 --- a/src/cli/commands/login.command.ts +++ b/src/cli/commands/login.command.ts @@ -242,8 +242,10 @@ export class LoginCommand { } }); let foundPort = false; - const webUrl = this.environmentService.webVaultUrl == null ? 'https://vault.bitwarden.com' : - this.environmentService.webVaultUrl; + let webUrl = this.environmentService.getWebVaultUrl(); + if (webUrl == null) { + webUrl = 'https://vault.bitwarden.com'; + } for (let port = 8065; port <= 8070; port++) { try { this.ssoRedirectUri = 'http://localhost:' + port; From cc12e58ac5abb6dfc58e98dd6675ac5c91457137 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 4 Sep 2020 14:08:18 -0400 Subject: [PATCH 1065/1626] updated sso password string to be 32 characters (#164) --- src/angular/components/sso.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/sso.component.ts b/src/angular/components/sso.component.ts index 563307ff75..7dc9fa207b 100644 --- a/src/angular/components/sso.component.ts +++ b/src/angular/components/sso.component.ts @@ -77,7 +77,7 @@ export class SsoComponent { const passwordOptions: any = { type: 'password', - length: 64, + length: 32, uppercase: true, lowercase: true, numbers: true, From 4745c24695aea2392a334c58fd73c7f2612b1ed9 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 4 Sep 2020 16:01:54 -0400 Subject: [PATCH 1066/1626] reverted codeVerifier length back to 64 (#166) --- src/angular/components/sso.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/sso.component.ts b/src/angular/components/sso.component.ts index 7dc9fa207b..563307ff75 100644 --- a/src/angular/components/sso.component.ts +++ b/src/angular/components/sso.component.ts @@ -77,7 +77,7 @@ export class SsoComponent { const passwordOptions: any = { type: 'password', - length: 32, + length: 64, uppercase: true, lowercase: true, numbers: true, From 0bff8bcd56a131122da4b8c0effcd60d467b985a Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Tue, 8 Sep 2020 09:17:04 -0400 Subject: [PATCH 1067/1626] changed the way we load planResponse objects in organizationResponse objects (#167) --- src/models/response/organizationResponse.ts | 3 +- src/models/response/planResponse.ts | 78 ++++++++++----------- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/src/models/response/organizationResponse.ts b/src/models/response/organizationResponse.ts index c54aee077c..466a2b3334 100644 --- a/src/models/response/organizationResponse.ts +++ b/src/models/response/organizationResponse.ts @@ -38,7 +38,8 @@ export class OrganizationResponse extends BaseResponse { this.businessCountry = this.getResponseProperty('BusinessCountry'); this.businessTaxNumber = this.getResponseProperty('BusinessTaxNumber'); this.billingEmail = this.getResponseProperty('BillingEmail'); - this.plan = this.getResponseProperty('Plan'); + const plan = this.getResponseProperty('Plan'); + this.plan = plan == null ? null : new PlanResponse(plan); this.planType = this.getResponseProperty('PlanType'); this.seats = this.getResponseProperty('Seats'); this.maxCollections = this.getResponseProperty('MaxCollections'); diff --git a/src/models/response/planResponse.ts b/src/models/response/planResponse.ts index 96b385da07..2cdd18acf0 100644 --- a/src/models/response/planResponse.ts +++ b/src/models/response/planResponse.ts @@ -50,44 +50,44 @@ export class PlanResponse extends BaseResponse { constructor(response: any) { super(response); - this.type = this.getResponseProperty('type'); - this.product = this.getResponseProperty('product'); - this.name = this.getResponseProperty('name'); - this.isAnnual = this.getResponseProperty('isAnnual'); - this.nameLocalizationKey = this.getResponseProperty('nameLocalizationKey'); - this.descriptionLocalizationKey = this.getResponseProperty('descriptionLocalizationKey'); - this.canBeUsedByBusiness = this.getResponseProperty('canBeUsedByBusiness'); - this.baseSeats = this.getResponseProperty('baseSeats'); - this.baseStorageGb = this.getResponseProperty('baseStorageGb'); - this.maxCollections = this.getResponseProperty('maxCollections'); - this.maxUsers = this.getResponseProperty('maxUsers'); - this.hasAdditionalSeatsOption = this.getResponseProperty('hasAdditionalSeatsOption'); - this.maxAdditionalSeats = this.getResponseProperty('maxAdditionalSeats'); - this.hasAdditionalStorageOption = this.getResponseProperty('hasAdditionalStorageOption'); - this.maxAdditionalStorage = this.getResponseProperty('maxAdditionalStorage'); - this.hasPremiumAccessOption = this.getResponseProperty('hasPremiumAccessOption'); - this.trialPeriodDays = this.getResponseProperty('trialPeriodDays'); - this.hasSelfHost = this.getResponseProperty('hasSelfHost'); - this.hasPolicies = this.getResponseProperty('hasPolicies'); - this.hasGroups = this.getResponseProperty('hasGroups'); - this.hasDirectory = this.getResponseProperty('hasDirectory'); - this.hasEvents = this.getResponseProperty('hasEvents'); - this.hasTotp = this.getResponseProperty('hasTotp'); - this.has2fa = this.getResponseProperty('has2fa'); - this.hasApi = this.getResponseProperty('hasApi'); - this.hasSso = this.getResponseProperty('hasSso'); - this.usersGetPremium = this.getResponseProperty('usersGetPremium'); - this.upgradeSortOrder = this.getResponseProperty('upgradeSortOrder'); - this.displaySortOrder = this.getResponseProperty('sortOrder'); - this.legacyYear = this.getResponseProperty('legacyYear'); - this.disabled = this.getResponseProperty('disabled'); - this.stripePlanId = this.getResponseProperty('stripePlanId'); - this.stripeSeatPlanId = this.getResponseProperty('stripeSeatPlanId'); - this.stripeStoragePlanId = this.getResponseProperty('stripeStoragePlanId'); - this.stripePremiumAccessPlanId = this.getResponseProperty('stripePremiumAccessPlanId'); - this.basePrice = this.getResponseProperty('basePrice'); - this.seatPrice = this.getResponseProperty('seatPrice'); - this.additionalStoragePricePerGb = this.getResponseProperty('additionalStoragePricePerGb'); - this.premiumAccessOptionPrice = this.getResponseProperty('premiumAccessOptionPrice'); + this.type = this.getResponseProperty('Type'); + this.product = this.getResponseProperty('Product'); + this.name = this.getResponseProperty('Name'); + this.isAnnual = this.getResponseProperty('IsAnnual'); + this.nameLocalizationKey = this.getResponseProperty('NameLocalizationKey'); + this.descriptionLocalizationKey = this.getResponseProperty('DescriptionLocalizationKey'); + this.canBeUsedByBusiness = this.getResponseProperty('CanBeUsedByBusiness'); + this.baseSeats = this.getResponseProperty('BaseSeats'); + this.baseStorageGb = this.getResponseProperty('BaseStorageGb'); + this.maxCollections = this.getResponseProperty('MaxCollections'); + this.maxUsers = this.getResponseProperty('MaxUsers'); + this.hasAdditionalSeatsOption = this.getResponseProperty('HasAdditionalSeatsOption'); + this.maxAdditionalSeats = this.getResponseProperty('MaxAdditionalSeats'); + this.hasAdditionalStorageOption = this.getResponseProperty('HasAdditionalStorageOption'); + this.maxAdditionalStorage = this.getResponseProperty('MaxAdditionalStorage'); + this.hasPremiumAccessOption = this.getResponseProperty('HasPremiumAccessOption'); + this.trialPeriodDays = this.getResponseProperty('TrialPeriodDays'); + this.hasSelfHost = this.getResponseProperty('HasSelfHost'); + this.hasPolicies = this.getResponseProperty('HasPolicies'); + this.hasGroups = this.getResponseProperty('HasGroups'); + this.hasDirectory = this.getResponseProperty('HasDirectory'); + this.hasEvents = this.getResponseProperty('HasEvents'); + this.hasTotp = this.getResponseProperty('HasTotp'); + this.has2fa = this.getResponseProperty('Has2fa'); + this.hasApi = this.getResponseProperty('HasApi'); + this.hasSso = this.getResponseProperty('HasSso'); + this.usersGetPremium = this.getResponseProperty('UsersGetPremium'); + this.upgradeSortOrder = this.getResponseProperty('UpgradeSortOrder'); + this.displaySortOrder = this.getResponseProperty('SortOrder'); + this.legacyYear = this.getResponseProperty('LegacyYear'); + this.disabled = this.getResponseProperty('Disabled'); + this.stripePlanId = this.getResponseProperty('StripePlanId'); + this.stripeSeatPlanId = this.getResponseProperty('StripeSeatPlanId'); + this.stripeStoragePlanId = this.getResponseProperty('StripeStoragePlanId'); + this.stripePremiumAccessPlanId = this.getResponseProperty('StripePremiumAccessPlanId'); + this.basePrice = this.getResponseProperty('BasePrice'); + this.seatPrice = this.getResponseProperty('SeatPrice'); + this.additionalStoragePricePerGb = this.getResponseProperty('AdditionalStoragePricePerGb'); + this.premiumAccessOptionPrice = this.getResponseProperty('PremiumAccessOptionPrice'); } } From fa2b8e834bf0576ef3c33eb64bb133b54028943f Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Tue, 8 Sep 2020 10:36:22 -0400 Subject: [PATCH 1068/1626] Wrap sso login with pre-validation check (#160) * Wrap sso login with pre-validation check * Add form promise for SSO preValidate * Removed boolean variable, .catch() --- src/abstractions/api.service.ts | 2 ++ src/angular/components/sso.component.ts | 17 +++++++++++++-- src/services/api.service.ts | 29 +++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index f670c9ca00..a8faa65bb7 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -306,4 +306,6 @@ export abstract class ApiService { getActiveBearerToken: () => Promise; fetch: (request: Request) => Promise; nativeFetch: (request: Request) => Promise; + + preValidateSso: (identifier: string) => Promise; } diff --git a/src/angular/components/sso.component.ts b/src/angular/components/sso.component.ts index 563307ff75..1b6bdb22ae 100644 --- a/src/angular/components/sso.component.ts +++ b/src/angular/components/sso.component.ts @@ -23,6 +23,7 @@ export class SsoComponent { loggingIn = false; formPromise: Promise; + initiateSsoFormPromise: Promise; onSuccessfulLogin: () => Promise; onSuccessfulLoginNavigate: () => Promise; onSuccessfulLoginTwoFactorNavigate: () => Promise; @@ -67,8 +68,20 @@ export class SsoComponent { } async submit(returnUri?: string, includeUserIdentifier?: boolean) { - const authorizeUrl = await this.buildAuthorizeUrl(returnUri, includeUserIdentifier); - this.platformUtilsService.launchUri(authorizeUrl, { sameWindow: true }); + this.initiateSsoFormPromise = this.preValidate(); + if (await this.initiateSsoFormPromise) { + const authorizeUrl = await this.buildAuthorizeUrl(returnUri, includeUserIdentifier); + this.platformUtilsService.launchUri(authorizeUrl, { sameWindow: true }); + } + } + + async preValidate(): Promise { + if (this.identifier == null || this.identifier === '') { + this.platformUtilsService.showToast('error', this.i18nService.t('ssoValidationFailed'), + this.i18nService.t('ssoIdentifierRequired')); + return false; + } + return await this.apiService.preValidateSso(this.identifier); } protected async buildAuthorizeUrl(returnUri?: string, includeUserIdentifier?: boolean): Promise { diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 22cb00f508..6be644dc03 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -1040,6 +1040,35 @@ export class ApiService implements ApiServiceAbstraction { return fetch(request); } + async preValidateSso(identifier: string): Promise { + + if (identifier == null || identifier === '') { + throw new Error('Organization Identifier was not provided.'); + } + const headers = new Headers({ + 'Accept': 'application/json', + 'Device-Type': this.deviceType, + }); + if (this.customUserAgent != null) { + headers.set('User-Agent', this.customUserAgent); + } + + const path = `/account/prevalidate?domainHint=${encodeURIComponent(identifier)}`; + const response = await this.fetch(new Request(this.identityBaseUrl + path, { + cache: 'no-store', + credentials: this.getCredentials(), + headers: headers, + method: 'GET', + })); + + if (response.status === 200) { + return true; + } else { + const error = await this.handleError(response, false); + return Promise.reject(error); + } + } + private async send(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body: any, authed: boolean, hasResponse: boolean): Promise { const headers = new Headers({ From 5e0a2d1d998b5d36b093f3eff032453421680a41 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Tue, 15 Sep 2020 10:23:21 -0400 Subject: [PATCH 1069/1626] remove old Edge browser hacks (#168) * remove old Edge browser hacks * Remove final edge hacks * Update constructor parameters * Update search-ciphers.pipe.ts Co-authored-by: Kyle Spearrin --- spec/common/misc/utils.spec.ts | 7 ------- spec/web/services/webCryptoFunction.service.spec.ts | 4 ++-- src/angular/components/register.component.ts | 2 +- src/angular/components/set-password.component.ts | 2 +- src/angular/pipes/search-ciphers.pipe.ts | 13 ------------- src/services/search.service.ts | 10 +--------- src/services/webCryptoFunction.service.ts | 6 ++---- 7 files changed, 7 insertions(+), 37 deletions(-) diff --git a/spec/common/misc/utils.spec.ts b/spec/common/misc/utils.spec.ts index 5876415c0f..a2e628454a 100644 --- a/spec/common/misc/utils.spec.ts +++ b/spec/common/misc/utils.spec.ts @@ -49,13 +49,6 @@ describe('Utils Service', () => { expect(Utils.getHostname('https://bitwarden.com')).toBe('bitwarden.com'); expect(Utils.getHostname('http://bitwarden.com')).toBe('bitwarden.com'); expect(Utils.getHostname('http://vault.bitwarden.com')).toBe('vault.bitwarden.com'); - - if (Utils.isNode || window.navigator.userAgent.indexOf(' Edge/') === -1) { - // Note: Broken in Edge browser. See - // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8004284/ - expect(Utils.getHostname('https://user:password@bitwarden.com:8080/password/sites?and&query#hash')) - .toBe('bitwarden.com'); - } }); it('should support localhost and IP', () => { diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index 2e31fa1525..d1ead1592e 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -380,8 +380,8 @@ function testRsaGenerateKeyPair(length: 1024 | 2048 | 4096) { function getWebCryptoFunctionService() { const platformUtilsMock = TypeMoq.Mock.ofType(PlatformUtilsServiceMock); - platformUtilsMock.setup((x) => x.isEdge()).returns(() => navigator.userAgent.indexOf(' Edge/') !== -1); - platformUtilsMock.setup((x) => x.isIE()).returns(() => navigator.userAgent.indexOf(' Edge/') === -1 && + platformUtilsMock.setup((x) => x.isEdge()).returns(() => navigator.userAgent.indexOf(' Edg/') !== -1); + platformUtilsMock.setup((x) => x.isIE()).returns(() => navigator.userAgent.indexOf(' Edg/') === -1 && navigator.userAgent.indexOf(' Trident/') !== -1); return new WebCryptoFunctionService(window, platformUtilsMock.object); } diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index b3f689fcd0..062b1fa0f2 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -105,7 +105,7 @@ export class RegisterComponent { this.name = this.name === '' ? null : this.name; this.email = this.email.trim().toLowerCase(); const kdf = KdfType.PBKDF2_SHA256; - const useLowerKdf = this.platformUtilsService.isEdge() || this.platformUtilsService.isIE(); + const useLowerKdf = this.platformUtilsService.isIE(); const kdfIterations = useLowerKdf ? 10000 : 100000; const key = await this.cryptoService.makeKey(this.masterPassword, this.email, kdf, kdfIterations); const encKey = await this.cryptoService.makeEncKey(key); diff --git a/src/angular/components/set-password.component.ts b/src/angular/components/set-password.component.ts index 94fe16326d..9db3941f08 100644 --- a/src/angular/components/set-password.component.ts +++ b/src/angular/components/set-password.component.ts @@ -44,7 +44,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { async setupSubmitActions() { this.kdf = KdfType.PBKDF2_SHA256; - const useLowerKdf = this.platformUtilsService.isEdge() || this.platformUtilsService.isIE(); + const useLowerKdf = this.platformUtilsService.isIE(); this.kdfIterations = useLowerKdf ? 10000 : 100000; return true; } diff --git a/src/angular/pipes/search-ciphers.pipe.ts b/src/angular/pipes/search-ciphers.pipe.ts index 1de1fbf267..407011e911 100644 --- a/src/angular/pipes/search-ciphers.pipe.ts +++ b/src/angular/pipes/search-ciphers.pipe.ts @@ -5,20 +5,10 @@ import { import { CipherView } from '../../models/view/cipherView'; -import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; - -import { DeviceType } from '../../enums'; - @Pipe({ name: 'searchCiphers', }) export class SearchCiphersPipe implements PipeTransform { - private onlySearchName = false; - - constructor(platformUtilsService: PlatformUtilsService) { - this.onlySearchName = platformUtilsService.getDevice() === DeviceType.EdgeExtension; - } - transform(ciphers: CipherView[], searchText: string, deleted: boolean = false): CipherView[] { if (ciphers == null || ciphers.length === 0) { return []; @@ -38,9 +28,6 @@ export class SearchCiphersPipe implements PipeTransform { if (c.name != null && c.name.toLowerCase().indexOf(searchText) > -1) { return true; } - if (this.onlySearchName) { - return false; - } if (searchText.length >= 8 && c.id.startsWith(searchText)) { return true; } diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 28bcbf048f..4b992d54f3 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -3,22 +3,17 @@ import * as lunr from 'lunr'; import { CipherView } from '../models/view/cipherView'; import { CipherService } from '../abstractions/cipher.service'; -import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { SearchService as SearchServiceAbstraction } from '../abstractions/search.service'; import { CipherType } from '../enums/cipherType'; -import { DeviceType } from '../enums/deviceType'; import { FieldType } from '../enums/fieldType'; import { UriMatchType } from '../enums/uriMatchType'; export class SearchService implements SearchServiceAbstraction { private indexing = false; private index: lunr.Index = null; - private onlySearchName = false; - constructor(private cipherService: CipherService, platformUtilsService: PlatformUtilsService) { - this.onlySearchName = platformUtilsService == null || - platformUtilsService.getDevice() === DeviceType.EdgeExtension; + constructor(private cipherService: CipherService) { } clearIndex(): void { @@ -152,9 +147,6 @@ export class SearchService implements SearchServiceAbstraction { if (c.name != null && c.name.toLowerCase().indexOf(query) > -1) { return true; } - if (this.onlySearchName) { - return false; - } if (query.length >= 8 && c.id.startsWith(query)) { return true; } diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index fc37e3ae3e..a651d778d3 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -11,14 +11,12 @@ import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; export class WebCryptoFunctionService implements CryptoFunctionService { private crypto: Crypto; private subtle: SubtleCrypto; - private isEdge: boolean; private isIE: boolean; private isOldSafari: boolean; constructor(private win: Window, private platformUtilsService: PlatformUtilsService) { this.crypto = typeof win.crypto !== 'undefined' ? win.crypto : null; this.subtle = (!!this.crypto && typeof win.crypto.subtle !== 'undefined') ? win.crypto.subtle : null; - this.isEdge = platformUtilsService.isEdge(); this.isIE = platformUtilsService.isIE(); const ua = win.navigator.userAgent; this.isOldSafari = platformUtilsService.isSafari() && @@ -27,7 +25,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { async pbkdf2(password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', iterations: number): Promise { - if (this.isEdge || this.isIE || this.isOldSafari) { + if (this.isIE || this.isOldSafari) { const forgeLen = algorithm === 'sha256' ? 32 : 64; const passwordBytes = this.toByteString(password); const saltBytes = this.toByteString(salt); @@ -52,7 +50,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { } async hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5'): Promise { - if (((this.isEdge || this.isIE) && algorithm === 'sha1') || algorithm === 'md5') { + if ((this.isIE && algorithm === 'sha1') || algorithm === 'md5') { const md = algorithm === 'md5' ? forge.md.md5.create() : forge.md.sha1.create(); const valueBytes = this.toByteString(value); md.update(valueBytes, 'raw'); From 27bcbf4b41373f8fbd3941c8ea6645414dac4fdd Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Thu, 17 Sep 2020 15:36:11 -0400 Subject: [PATCH 1070/1626] Conditional load of biometrics API on Win10+ (#169) * Conditional load of biometrics API on Win10+ * consolidate if block * Return -1 instead of null --- src/electron/biometric.windows.main.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/electron/biometric.windows.main.ts b/src/electron/biometric.windows.main.ts index 1d9f3df008..316110b5ec 100644 --- a/src/electron/biometric.windows.main.ts +++ b/src/electron/biometric.windows.main.ts @@ -48,7 +48,7 @@ export default class BiometricWindowsMain implements BiometricMain { getWindowsSecurityCredentialsUiModule(): any { try { - if (this.windowsSecurityCredentialsUiModule == null) { + if (this.windowsSecurityCredentialsUiModule == null && this.getWindowsMajorVersion() >= 10) { this.windowsSecurityCredentialsUiModule = require('@nodert-win10-rs4/windows.security.credentials.ui'); } return this.windowsSecurityCredentialsUiModule; @@ -110,4 +110,16 @@ export default class BiometricWindowsMain implements BiometricMain { } catch { /*Ignore error*/ } return []; } + + getWindowsMajorVersion(): number { + if (process.platform !== 'win32') { + return -1; + } + try { + const version = require('os').release(); + return Number.parseInt(version.split('.')[0], 10); + } + catch { } + return -1; + } } From 0a20face13a0cf57c25fbd44840378cc0d0afc02 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 18 Sep 2020 15:02:49 -0400 Subject: [PATCH 1071/1626] check authed before checking if token has premium (#170) --- src/services/user.service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 62ddd0a04a..df8755d7f1 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -107,6 +107,11 @@ export class UserService implements UserServiceAbstraction { } async canAccessPremium(): Promise { + const authed = await this.isAuthenticated(); + if (!authed) { + return false; + } + const tokenPremium = this.tokenService.getPremium(); if (tokenPremium) { return true; From 3bf322a904cd7ccb8c7e77edbecf8e152feb7364 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sun, 20 Sep 2020 09:47:35 -0400 Subject: [PATCH 1072/1626] support defaultMatch for getAllDecryptedForUrl --- src/abstractions/cipher.service.ts | 4 +++- src/services/cipher.service.ts | 9 ++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index 8ec886e240..339ca0a461 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -1,4 +1,5 @@ import { CipherType } from '../enums/cipherType'; +import { UriMatchType } from '../enums/uriMatchType'; import { CipherData } from '../models/data/cipherData'; @@ -21,7 +22,8 @@ export abstract class CipherService { getAll: () => Promise; getAllDecrypted: () => Promise; getAllDecryptedForGrouping: (groupingId: string, folder?: boolean) => Promise; - getAllDecryptedForUrl: (url: string, includeOtherTypes?: CipherType[]) => Promise; + getAllDecryptedForUrl: (url: string, includeOtherTypes?: CipherType[], + defaultMatch?: UriMatchType) => Promise; getAllFromApiForOrganization: (organizationId: string) => Promise; getLastUsedForUrl: (url: string) => Promise; getNextCipherForUrl: (url: string) => Promise; diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index fe661bdeea..1942628898 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -328,7 +328,8 @@ export class CipherService implements CipherServiceAbstraction { }); } - async getAllDecryptedForUrl(url: string, includeOtherTypes?: CipherType[]): Promise { + async getAllDecryptedForUrl(url: string, includeOtherTypes?: CipherType[], + defaultMatch: UriMatchType = null): Promise { if (url == null && includeOtherTypes == null) { return Promise.resolve([]); } @@ -354,9 +355,11 @@ export class CipherService implements CipherServiceAbstraction { const matchingDomains = result[0]; const ciphers = result[1]; - let defaultMatch = await this.storageService.get(ConstantsService.defaultUriMatch); if (defaultMatch == null) { - defaultMatch = UriMatchType.Domain; + defaultMatch = await this.storageService.get(ConstantsService.defaultUriMatch); + if (defaultMatch == null) { + defaultMatch = UriMatchType.Domain; + } } return ciphers.filter((cipher) => { From 5c3e337fb1bcccda2c8898563ccefe32970cb0d5 Mon Sep 17 00:00:00 2001 From: vachan-maker <65799568+vachan-maker@users.noreply.github.com> Date: Mon, 21 Sep 2020 17:39:10 +0530 Subject: [PATCH 1073/1626] Added locale name(ml) (#171) I have no clue if what I did was correct. If it is not, please try to display the locale name. Thanks --- src/services/i18n.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/i18n.service.ts b/src/services/i18n.service.ts index efb845ee11..d2246b7dbe 100644 --- a/src/services/i18n.service.ts +++ b/src/services/i18n.service.ts @@ -30,6 +30,7 @@ export class I18nService implements I18nServiceAbstraction { ['it', 'italiano'], ['ja', '日本語'], ['ko', '한국어'], + ['ml', 'മലയാളം'], ['nb', 'norsk (bokmål)'], ['nl', 'Nederlands'], ['pl', 'polski'], From f0dc38b1c49a8444cbcca14a84d8aedb122ee11d Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 21 Sep 2020 11:17:48 -0400 Subject: [PATCH 1074/1626] update languages (#172) * update languages * fix lint error --- src/services/i18n.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/i18n.service.ts b/src/services/i18n.service.ts index d2246b7dbe..457d0489cd 100644 --- a/src/services/i18n.service.ts +++ b/src/services/i18n.service.ts @@ -8,6 +8,7 @@ export class I18nService implements I18nServiceAbstraction { collator: Intl.Collator; localeNames = new Map([ ['af', 'Afrikaans'], + ['be', 'Беларуская'], ['bg', 'български'], ['ca', 'català'], ['cs', 'čeština'], @@ -30,7 +31,8 @@ export class I18nService implements I18nServiceAbstraction { ['it', 'italiano'], ['ja', '日本語'], ['ko', '한국어'], - ['ml', 'മലയാളം'], + ['lv', 'Latvietis'], + ['ml', 'മലയാളം'], ['nb', 'norsk (bokmål)'], ['nl', 'Nederlands'], ['pl', 'polski'], From 5c6c3a8b0a2fa22da7f5db91da5215d9e728e66f Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Wed, 23 Sep 2020 10:18:14 -0400 Subject: [PATCH 1075/1626] Fixed web vault URL reference for SSO launch (#175) --- src/angular/components/login.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/angular/components/login.component.ts b/src/angular/components/login.component.ts index b55d1f270b..2556ed4f2b 100644 --- a/src/angular/components/login.component.ts +++ b/src/angular/components/login.component.ts @@ -138,8 +138,8 @@ export class LoginComponent implements OnInit { await this.storageService.save(ConstantsService.ssoCodeVerifierKey, ssoCodeVerifier); // Build URI - const webUrl = this.environmentService.webVaultUrl == null ? 'https://vault.bitwarden.com' : - this.environmentService.webVaultUrl; + const webUrl = this.environmentService.getWebVaultUrl() == null ? 'https://vault.bitwarden.com' : + this.environmentService.getWebVaultUrl(); // Launch browser this.platformUtilsService.launchUri(webUrl + '/#/sso?clientId=' + clientId + From 2ea1f8484f473afa881bb55a506cb387f353aa39 Mon Sep 17 00:00:00 2001 From: Matt Portune <59324545+mportune-bw@users.noreply.github.com> Date: Wed, 23 Sep 2020 10:35:26 -0400 Subject: [PATCH 1076/1626] hide bio unlock option when logging in with sso (#176) --- src/angular/components/lock.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 0f4b901a0b..423476d891 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -50,7 +50,7 @@ export class LockComponent implements OnInit { this.pinSet = await this.vaultTimeoutService.isPinLockSet(); this.pinLock = (this.pinSet[0] && this.vaultTimeoutService.pinProtectedKey != null) || this.pinSet[1]; this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); - this.biometricLock = await this.vaultTimeoutService.isBiometricLockSet(); + this.biometricLock = await this.vaultTimeoutService.isBiometricLockSet() && await this.cryptoService.hasKey(); this.biometricText = await this.storageService.get(ConstantsService.biometricText); this.email = await this.userService.getEmail(); let vaultUrl = this.environmentService.getWebVaultUrl(); From 5cb3e9c965269a7e442536fa1b6ba00add2c7153 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 23 Sep 2020 11:49:16 -0400 Subject: [PATCH 1077/1626] implement launchUri function (#177) --- src/cli/commands/login.command.ts | 8 +++++--- src/cli/services/cliPlatformUtils.service.ts | 10 +++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/cli/commands/login.command.ts b/src/cli/commands/login.command.ts index 51aff1f1a6..26d401df8e 100644 --- a/src/cli/commands/login.command.ts +++ b/src/cli/commands/login.command.ts @@ -13,6 +13,7 @@ import { CryptoFunctionService } from '../../abstractions/cryptoFunction.service import { EnvironmentService } from '../../abstractions/environment.service'; import { I18nService } from '../../abstractions/i18n.service'; import { PasswordGenerationService } from '../../abstractions/passwordGeneration.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; import { Response } from '../models/response'; @@ -35,7 +36,8 @@ export class LoginCommand { constructor(protected authService: AuthService, protected apiService: ApiService, protected i18nService: I18nService, protected environmentService: EnvironmentService, protected passwordGenerationService: PasswordGenerationService, - protected cryptoFunctionService: CryptoFunctionService, clientId: string) { + protected cryptoFunctionService: CryptoFunctionService, protected platformUtilsService: PlatformUtilsService, + clientId: string) { this.clientId = clientId; } @@ -249,8 +251,8 @@ export class LoginCommand { for (let port = 8065; port <= 8070; port++) { try { this.ssoRedirectUri = 'http://localhost:' + port; - callbackServer.listen(port, async () => { - await open(webUrl + '/#/sso?clientId=' + this.clientId + + callbackServer.listen(port, () => { + this.platformUtilsService.launchUri(webUrl + '/#/sso?clientId=' + this.clientId + '&redirectUri=' + encodeURIComponent(this.ssoRedirectUri) + '&state=' + state + '&codeChallenge=' + codeChallenge); }); diff --git a/src/cli/services/cliPlatformUtils.service.ts b/src/cli/services/cliPlatformUtils.service.ts index 23a0aa01d0..f86adfeeac 100644 --- a/src/cli/services/cliPlatformUtils.service.ts +++ b/src/cli/services/cliPlatformUtils.service.ts @@ -1,8 +1,12 @@ +import * as child_process from 'child_process'; import { DeviceType } from '../../enums/deviceType'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +// tslint:disable-next-line +const open = require('open'); + export class CliPlatformUtilsService implements PlatformUtilsService { identityClientId: string; @@ -81,7 +85,11 @@ export class CliPlatformUtilsService implements PlatformUtilsService { } launchUri(uri: string, options?: any): void { - throw new Error('Not implemented.'); + if (process.platform === 'linux') { + child_process.spawnSync('xdg-open', [uri]); + } else { + open(uri); + } } saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void { From 26d40d4c431deaaf8129e647e7cc1ef3861b0968 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Wed, 23 Sep 2020 12:41:25 -0400 Subject: [PATCH 1078/1626] Add login launch data (#174) * added launch time data to CipherView for autofill * removed unused code * fixed linter errors --- src/abstractions/cipher.service.ts | 2 ++ src/models/domain/sortedCiphersCache.ts | 9 +++++ src/services/cipher.service.ts | 48 ++++++++++++++++++++++--- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index 339ca0a461..ca4d29af1e 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -26,8 +26,10 @@ export abstract class CipherService { defaultMatch?: UriMatchType) => Promise; getAllFromApiForOrganization: (organizationId: string) => Promise; getLastUsedForUrl: (url: string) => Promise; + getLastLaunchedForUrl: (url: string) => Promise; getNextCipherForUrl: (url: string) => Promise; updateLastUsedDate: (id: string) => Promise; + updateLastLaunchedDate: (id: string) => Promise; saveNeverDomain: (domain: string) => Promise; saveWithServer: (cipher: Cipher) => Promise; shareWithServer: (cipher: CipherView, organizationId: string, collectionIds: string[]) => Promise; diff --git a/src/models/domain/sortedCiphersCache.ts b/src/models/domain/sortedCiphersCache.ts index 9f11f73db7..e4e22ee9ee 100644 --- a/src/models/domain/sortedCiphersCache.ts +++ b/src/models/domain/sortedCiphersCache.ts @@ -23,6 +23,10 @@ export class SortedCiphersCache { return this.isCached(url) ? this.sortedCiphersByUrl.get(url).getLastUsed() : null; } + getLastLaunched(url: string) { + return this.isCached(url) ? this.sortedCiphersByUrl.get(url).getLastLaunched() : null; + } + getNext(url: string) { this.resetTimer(url); return this.isCached(url) ? this.sortedCiphersByUrl.get(url).getNext() : null; @@ -52,6 +56,11 @@ class Ciphers { return this.ciphers[this.lastUsedIndex]; } + getLastLaunched() { + const sortedCiphers = this.ciphers.sort((x, y) => y.localData?.lastLaunched?.valueOf() - x.localData?.lastLaunched?.valueOf()); + return sortedCiphers[0]; + } + getNext() { const nextIndex = (this.lastUsedIndex + 1) % this.ciphers.length; this.lastUsedIndex = nextIndex; diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 1942628898..0095c4773a 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -445,11 +445,15 @@ export class CipherService implements CipherServiceAbstraction { } async getLastUsedForUrl(url: string): Promise { - return this.getCipherForUrl(url, true); + return this.getCipherForUrl(url, true, false); + } + + async getLastLaunchedForUrl(url: string): Promise { + return this.getCipherForUrl(url, false, true); } async getNextCipherForUrl(url: string): Promise { - return this.getCipherForUrl(url, false); + return this.getCipherForUrl(url, false, false); } async updateLastUsedDate(id: string): Promise { @@ -481,6 +485,35 @@ export class CipherService implements CipherServiceAbstraction { } } + async updateLastLaunchedDate(id: string): Promise { + let ciphersLocalData = await this.storageService.get(Keys.localData); + if (!ciphersLocalData) { + ciphersLocalData = {}; + } + + if (ciphersLocalData[id]) { + ciphersLocalData[id].lastLaunched = new Date().getTime(); + } else { + ciphersLocalData[id] = { + lastUsedDate: new Date().getTime(), + }; + } + + await this.storageService.save(Keys.localData, ciphersLocalData); + + if (this.decryptedCipherCache == null) { + return; + } + + for (let i = 0; i < this.decryptedCipherCache.length; i++) { + const cached = this.decryptedCipherCache[i]; + if (cached.id === id) { + cached.localData = ciphersLocalData[id]; + break; + } + } + } + async saveNeverDomain(domain: string): Promise { if (domain == null) { return; @@ -1009,7 +1042,7 @@ export class CipherService implements CipherServiceAbstraction { } } - private async getCipherForUrl(url: string, lastUsed: boolean): Promise { + private async getCipherForUrl(url: string, lastUsed: boolean, lastLaunched: boolean): Promise { if (!this.sortedCiphersCache.isCached(url)) { const ciphers = await this.getAllDecryptedForUrl(url); if (!ciphers) { @@ -1018,6 +1051,13 @@ export class CipherService implements CipherServiceAbstraction { this.sortedCiphersCache.addCiphers(url, ciphers); } - return lastUsed ? this.sortedCiphersCache.getLastUsed(url) : this.sortedCiphersCache.getNext(url); + if (lastLaunched) { + return this.sortedCiphersCache.getLastLaunched(url); + } else if (lastUsed) { + return this.sortedCiphersCache.getLastUsed(url); + } + else { + return this.sortedCiphersCache.getNext(url); + } } } From b5cc5409ff03d8e5f5b0fafba6185d2e93c62b2b Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Wed, 23 Sep 2020 13:53:43 -0400 Subject: [PATCH 1079/1626] bug fix + added launch time to view component (#178) --- src/angular/components/view.component.ts | 6 +++++- src/models/domain/sortedCiphersCache.ts | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index 067e6c6953..cd8c0c4d95 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -196,11 +196,15 @@ export class ViewComponent implements OnDestroy, OnInit { } } - launch(uri: LoginUriView) { + launch(uri: LoginUriView, cipherId?: string) { if (!uri.canLaunch) { return; } + if (cipherId) { + this.cipherService.updateLastLaunchedDate(cipherId); + } + this.platformUtilsService.eventTrack('Launched Login URI'); this.platformUtilsService.launchUri(uri.launchUri); } diff --git a/src/models/domain/sortedCiphersCache.ts b/src/models/domain/sortedCiphersCache.ts index e4e22ee9ee..133dc39313 100644 --- a/src/models/domain/sortedCiphersCache.ts +++ b/src/models/domain/sortedCiphersCache.ts @@ -57,7 +57,8 @@ class Ciphers { } getLastLaunched() { - const sortedCiphers = this.ciphers.sort((x, y) => y.localData?.lastLaunched?.valueOf() - x.localData?.lastLaunched?.valueOf()); + const usedCiphers = this.ciphers.filter(cipher => cipher.localData?.lastLaunched) + const sortedCiphers = usedCiphers.sort((x, y) => y.localData.lastLaunched.valueOf() - x.localData.lastLaunched.valueOf()); return sortedCiphers[0]; } From 3628f44f98d611a7f3e7debbf15d0ba993399ae4 Mon Sep 17 00:00:00 2001 From: Matthew Rodatus Date: Thu, 24 Sep 2020 09:56:59 -0400 Subject: [PATCH 1080/1626] Add Microsoft RDP URI schemes (#180) This will allow users to launch the Remote Desktop client directly from Bitwarden if their OS supports the URI scheme. Per https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/clients/remote-desktop-uri, rdp:// is legacy and ms-rd: is the preferred one. I'm adding both so that either can be used as ms-rd: is not available on all versions of Windows and rdp:// is easier to add as a custom URI scheme in Windows 10 if it is desired. --- src/models/view/loginUriView.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/models/view/loginUriView.ts b/src/models/view/loginUriView.ts index 7ef9fa092e..eab4806b40 100644 --- a/src/models/view/loginUriView.ts +++ b/src/models/view/loginUriView.ts @@ -14,6 +14,9 @@ const CanLaunchWhitelist = [ 'sftp://', 'irc://', 'vnc://', + // https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/clients/remote-desktop-uri + 'rdp://', // Legacy RDP URI scheme + 'ms-rd:', // Preferred RDP URI scheme 'chrome://', 'iosapp://', 'androidapp://', From f30d6f8027055507abfdefd1eeb5d9aab25cc601 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 24 Sep 2020 11:41:54 -0400 Subject: [PATCH 1081/1626] update electron builder --- package-lock.json | 96 +++++++++++++++++++++++++++++++---------------- package.json | 2 +- 2 files changed, 65 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index 674cae9e9d..32d7e58c5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -451,9 +451,9 @@ } }, "@types/semver": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.2.0.tgz", - "integrity": "sha512-1OzrNb4RuAzIT7wHSsgZRlMBlNsJl+do6UblR7JMW4oB7bbR+uBEYtUh7gEc/jM84GGilh68lSOokyM/zNUlBA==" + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ==" }, "@types/through": { "version": "0.0.29", @@ -797,6 +797,11 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" + }, "atob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", @@ -1284,20 +1289,20 @@ "dev": true }, "builder-util-runtime": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.4.0.tgz", - "integrity": "sha512-CJB/eKfPf2vHrkmirF5eicVnbDCkMBbwd5tRYlTlgud16zFeqD7QmrVUAOEXdnsrcNkiLg9dbuUsQKtl/AwsYQ==", + "version": "8.7.2", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.7.2.tgz", + "integrity": "sha512-xBqv+8bg6cfnzAQK1k3OGpfaHg+QkPgIgpEkXNhouZ0WiUkyZCftuRc2LYzQrLucFywpa14Xbc6+hTbpq83yRA==", "requires": { "debug": "^4.1.1", "sax": "^1.2.4" }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { @@ -2854,39 +2859,62 @@ } }, "electron-updater": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.2.0.tgz", - "integrity": "sha512-GuS3g7HDh17x/SaFjxjswlWUaKHczksYkV2Xc5CKj/bZH0YCvTSHtOmnBAdAmCk99u/71p3zP8f0jIqDfGcjww==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.3.5.tgz", + "integrity": "sha512-5jjN7ebvfj1cLI0VZMdCnJk6aC4bP+dy7ryBf21vArR0JzpRVk0OZHA2QBD+H5rm6ZSeDYHOY6+8PrMEqJ4wlQ==", "requires": { - "@types/semver": "^6.0.2", - "builder-util-runtime": "8.4.0", - "fs-extra": "^8.1.0", - "js-yaml": "^3.13.1", + "@types/semver": "^7.3.1", + "builder-util-runtime": "8.7.2", + "fs-extra": "^9.0.1", + "js-yaml": "^3.14.0", "lazy-val": "^1.0.4", "lodash.isequal": "^4.5.0", - "pako": "^1.0.10", - "semver": "^6.3.0" + "semver": "^7.3.2" }, "dependencies": { "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", "requires": { + "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" } }, "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsonfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", + "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^1.0.0" + } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + }, + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==" } } }, @@ -4457,6 +4485,7 @@ "version": "3.13.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -4576,6 +4605,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, "requires": { "graceful-fs": "^4.1.6" } @@ -6458,7 +6488,8 @@ "pako": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", - "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==" + "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", + "dev": true }, "papaparse": { "version": "4.6.0", @@ -8278,7 +8309,8 @@ "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true }, "unpipe": { "version": "1.0.0", diff --git a/package.json b/package.json index 2640a91aa1..c7d2532704 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", "electron-log": "2.2.17", "electron-store": "1.3.0", - "electron-updater": "4.2.0", + "electron-updater": "4.3.5", "form-data": "2.3.2", "https-proxy-agent": "4.0.0", "inquirer": "6.2.0", From 9216a8ead72ac82a646f70f5b5a107cf17581c46 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 1 Oct 2020 14:13:51 -0400 Subject: [PATCH 1082/1626] pre-create and chmod 600 data file (#182) --- src/services/lowdbStorage.service.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/lowdbStorage.service.ts b/src/services/lowdbStorage.service.ts index e9621a0fcf..ae9865c1a7 100644 --- a/src/services/lowdbStorage.service.ts +++ b/src/services/lowdbStorage.service.ts @@ -22,6 +22,10 @@ export class LowdbStorageService implements StorageService { NodeUtils.mkdirpSync(dir, '700'); } this.dataFilePath = path.join(dir, 'data.json'); + if (!fs.existsSync(this.dataFilePath)) { + fs.writeFileSync(this.dataFilePath, '', { mode: 0o600 }); + fs.chmodSync(this.dataFilePath, 0o600); + } adapter = new FileSync(this.dataFilePath); } try { From 685636b129d5992251862dafa5009cf61514cb62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Mar=C3=AD?= Date: Fri, 9 Oct 2020 13:30:55 +0200 Subject: [PATCH 1083/1626] New method to update the last used index (#184) Instead of updating it every time you call getNext(), it will be updated in a separate call, to avoid updating the index when the cipher did not auto-fill correctly (e.g wrong frame) Fixes #1392 --- src/abstractions/cipher.service.ts | 2 +- src/models/domain/sortedCiphersCache.ts | 20 ++++++++++++++++---- src/services/cipher.service.ts | 7 +++++-- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index ca4d29af1e..cab7a972b3 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -7,7 +7,6 @@ import { Cipher } from '../models/domain/cipher'; import { Field } from '../models/domain/field'; import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; -import { AttachmentView } from '../models/view/attachmentView'; import { CipherView } from '../models/view/cipherView'; import { FieldView } from '../models/view/fieldView'; @@ -28,6 +27,7 @@ export abstract class CipherService { getLastUsedForUrl: (url: string) => Promise; getLastLaunchedForUrl: (url: string) => Promise; getNextCipherForUrl: (url: string) => Promise; + updateLastUsedIndexForUrl: (url: string) => void; updateLastUsedDate: (id: string) => Promise; updateLastLaunchedDate: (id: string) => Promise; saveNeverDomain: (domain: string) => Promise; diff --git a/src/models/domain/sortedCiphersCache.ts b/src/models/domain/sortedCiphersCache.ts index 133dc39313..60da2d93be 100644 --- a/src/models/domain/sortedCiphersCache.ts +++ b/src/models/domain/sortedCiphersCache.ts @@ -32,6 +32,12 @@ export class SortedCiphersCache { return this.isCached(url) ? this.sortedCiphersByUrl.get(url).getNext() : null; } + updateLastUsedIndex(url: string) { + if (this.isCached(url)) { + this.sortedCiphersByUrl.get(url).updateLastUsedIndex(); + } + } + clear() { this.sortedCiphersByUrl.clear(); this.timeouts.clear(); @@ -57,14 +63,20 @@ class Ciphers { } getLastLaunched() { - const usedCiphers = this.ciphers.filter(cipher => cipher.localData?.lastLaunched) + const usedCiphers = this.ciphers.filter(cipher => cipher.localData?.lastLaunched); const sortedCiphers = usedCiphers.sort((x, y) => y.localData.lastLaunched.valueOf() - x.localData.lastLaunched.valueOf()); return sortedCiphers[0]; } + getNextIndex() { + return (this.lastUsedIndex + 1) % this.ciphers.length; + } + getNext() { - const nextIndex = (this.lastUsedIndex + 1) % this.ciphers.length; - this.lastUsedIndex = nextIndex; - return this.ciphers[nextIndex]; + return this.ciphers[this.getNextIndex()]; + } + + updateLastUsedIndex() { + this.lastUsedIndex = this.getNextIndex(); } } diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 0095c4773a..6cd58de8d5 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -456,6 +456,10 @@ export class CipherService implements CipherServiceAbstraction { return this.getCipherForUrl(url, false, false); } + updateLastUsedIndexForUrl(url: string) { + this.sortedCiphersCache.updateLastUsedIndex(url); + } + async updateLastUsedDate(id: string): Promise { let ciphersLocalData = await this.storageService.get(Keys.localData); if (!ciphersLocalData) { @@ -1055,8 +1059,7 @@ export class CipherService implements CipherServiceAbstraction { return this.sortedCiphersCache.getLastLaunched(url); } else if (lastUsed) { return this.sortedCiphersCache.getLastUsed(url); - } - else { + } else { return this.sortedCiphersCache.getNext(url); } } From 9e9795fd855d643459a9205c5299bfbf887d4985 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Mon, 12 Oct 2020 11:54:20 -0400 Subject: [PATCH 1084/1626] added OnlyOrg to PolicyType enum (#183) --- src/enums/policyType.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/enums/policyType.ts b/src/enums/policyType.ts index 9ecbf97f67..2ae160685d 100644 --- a/src/enums/policyType.ts +++ b/src/enums/policyType.ts @@ -2,4 +2,5 @@ export enum PolicyType { TwoFactorAuthentication = 0, MasterPassword = 1, PasswordGenerator = 2, + OnlyOrg = 3, } From 595215a9dab97020e680ba8b4fe638017f2951e1 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Tue, 13 Oct 2020 12:06:51 -0500 Subject: [PATCH 1085/1626] [Require SSO] Add policy type enumeration (#186) * Added SsoAuthentication policy type * Updated policy type name // added comments for clarification of what each type controls --- src/enums/policyType.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/enums/policyType.ts b/src/enums/policyType.ts index 2ae160685d..918511556a 100644 --- a/src/enums/policyType.ts +++ b/src/enums/policyType.ts @@ -1,6 +1,7 @@ export enum PolicyType { - TwoFactorAuthentication = 0, - MasterPassword = 1, - PasswordGenerator = 2, - OnlyOrg = 3, + TwoFactorAuthentication = 0, // Requires users to have 2fa enabled + MasterPassword = 1, // Sets minimum requirements for master password complexity + PasswordGenerator = 2, // Sets minimum requirements/default type for generated passwords/passphrases + OnlyOrg = 3, // Allows users to only be apart of one organization + RequireSso = 4, // Requires users to authenticate with SSO } From d84d6da7f77ec89ba69299b1ff982f083fd77203 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Tue, 13 Oct 2020 15:21:03 -0500 Subject: [PATCH 1086/1626] [SSO] New user provision flow (#173) * Initial commit of new user sso flow * Adjusted stateSplit conditional per review --- .../components/set-password.component.ts | 20 +++++++++++-- src/angular/components/sso.component.ts | 29 +++++++++++++++---- .../components/two-factor.component.ts | 24 +++++++++++++-- src/models/request/setPasswordRequest.ts | 1 + 4 files changed, 63 insertions(+), 11 deletions(-) diff --git a/src/angular/components/set-password.component.ts b/src/angular/components/set-password.component.ts index 9db3941f08..6c29be00b7 100644 --- a/src/angular/components/set-password.component.ts +++ b/src/angular/components/set-password.component.ts @@ -1,4 +1,7 @@ -import { Router } from '@angular/router'; +import { + ActivatedRoute, + Router +} from '@angular/router'; import { ApiService } from '../../abstractions/api.service'; import { CryptoService } from '../../abstractions/crypto.service'; @@ -24,6 +27,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { syncLoading: boolean = true; showPassword: boolean = false; hint: string = ''; + identifier: string = null; onSuccessfulChangePassword: () => Promise; successRoute = 'vault'; @@ -31,7 +35,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { constructor(i18nService: I18nService, cryptoService: CryptoService, messagingService: MessagingService, userService: UserService, passwordGenerationService: PasswordGenerationService, platformUtilsService: PlatformUtilsService, policyService: PolicyService, private router: Router, - private apiService: ApiService, private syncService: SyncService) { + private apiService: ApiService, private syncService: SyncService, private route: ActivatedRoute) { super(i18nService, cryptoService, messagingService, userService, passwordGenerationService, platformUtilsService, policyService); } @@ -39,6 +43,17 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { async ngOnInit() { await this.syncService.fullSync(true); this.syncLoading = false; + + const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => { + if (qParams.identifier != null) { + this.identifier = qParams.identifier; + } + + if (queryParamsSub != null) { + queryParamsSub.unsubscribe(); + } + }); + super.ngOnInit(); } @@ -57,6 +72,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { request.masterPasswordHint = this.hint; request.kdf = this.kdf; request.kdfIterations = this.kdfIterations; + request.orgIdentifier = this.identifier; const keys = await this.cryptoService.makeKeyPair(encKey[0]); request.keys = new KeysRequest(keys[0], keys[1].encryptedString); diff --git a/src/angular/components/sso.component.ts b/src/angular/components/sso.component.ts index 1b6bdb22ae..f5dbaab7dd 100644 --- a/src/angular/components/sso.component.ts +++ b/src/angular/components/sso.component.ts @@ -52,7 +52,7 @@ export class SsoComponent { await this.storageService.remove(ConstantsService.ssoCodeVerifierKey); await this.storageService.remove(ConstantsService.ssoStateKey); if (qParams.code != null && codeVerifier != null && state != null && state === qParams.state) { - await this.logIn(qParams.code, codeVerifier); + await this.logIn(qParams.code, codeVerifier, this.getOrgIdentiferFromState(state)); } } else if (qParams.clientId != null && qParams.redirectUri != null && qParams.state != null && qParams.codeChallenge != null) { @@ -109,10 +109,14 @@ export class SsoComponent { if (returnUri) { state += `_returnUri='${returnUri}'`; } - - await this.storageService.save(ConstantsService.ssoStateKey, state); } + // Add Organization Identifier to state + state += `_identifier=${this.identifier}`; + + // Save state (regardless of new or existing) + await this.storageService.save(ConstantsService.ssoStateKey, state); + let authorizeUrl = this.apiService.identityBaseUrl + '/connect/authorize?' + 'client_id=' + this.clientId + '&redirect_uri=' + encodeURIComponent(this.redirectUri) + '&' + 'response_type=code&scope=api offline_access&' + @@ -128,7 +132,7 @@ export class SsoComponent { return authorizeUrl; } - private async logIn(code: string, codeVerifier: string) { + private async logIn(code: string, codeVerifier: string, orgIdFromState: string) { this.loggingIn = true; try { this.formPromise = this.authService.logInSso(code, codeVerifier, this.redirectUri); @@ -140,7 +144,7 @@ export class SsoComponent { } else { this.router.navigate([this.twoFactorRoute], { queryParams: { - resetMasterPassword: response.resetMasterPassword, + identifier: orgIdFromState, }, }); } @@ -149,7 +153,11 @@ export class SsoComponent { if (this.onSuccessfulLoginChangePasswordNavigate != null) { this.onSuccessfulLoginChangePasswordNavigate(); } else { - this.router.navigate([this.changePasswordRoute]); + this.router.navigate([this.changePasswordRoute], { + queryParams: { + identifier: orgIdFromState, + }, + }); } } else { const disableFavicon = await this.storageService.get(ConstantsService.disableFaviconKey); @@ -167,4 +175,13 @@ export class SsoComponent { } catch { } this.loggingIn = false; } + + private getOrgIdentiferFromState(state: string): string { + if (!state) { + return null; + } + + const stateSplit = state.split('_identifier='); + return stateSplit.length > 1 ? stateSplit[1] : null; + } } diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 72c92bdf89..4fe4e0c4b1 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -3,7 +3,10 @@ import { OnInit, } from '@angular/core'; -import { Router } from '@angular/router'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; import { DeviceType } from '../../enums/deviceType'; import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; @@ -40,6 +43,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { twoFactorEmail: string = null; formPromise: Promise; emailPromise: Promise; + identifier: string = null; onSuccessfulLogin: () => Promise; onSuccessfulLoginNavigate: () => Promise; @@ -50,7 +54,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { protected i18nService: I18nService, protected apiService: ApiService, protected platformUtilsService: PlatformUtilsService, protected win: Window, protected environmentService: EnvironmentService, protected stateService: StateService, - protected storageService: StorageService) { + protected storageService: StorageService, protected route: ActivatedRoute) { this.u2fSupported = this.platformUtilsService.supportsU2f(win); } @@ -61,6 +65,16 @@ export class TwoFactorComponent implements OnInit, OnDestroy { return; } + const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => { + if (qParams.identifier != null) { + this.identifier = qParams.identifier; + } + + if (queryParamsSub != null) { + queryParamsSub.unsubscribe(); + } + }); + if (this.authService.authingWithSso()) { this.successRoute = 'lock'; } @@ -191,7 +205,11 @@ export class TwoFactorComponent implements OnInit, OnDestroy { if (response.resetMasterPassword) { this.successRoute = 'set-password'; } - this.router.navigate([this.successRoute]); + this.router.navigate([this.successRoute], { + queryParams: { + identifier: this.identifier, + }, + }); } } catch { if (this.selectedProviderType === TwoFactorProviderType.U2f && this.u2f != null) { diff --git a/src/models/request/setPasswordRequest.ts b/src/models/request/setPasswordRequest.ts index 4907375121..fab35c4136 100644 --- a/src/models/request/setPasswordRequest.ts +++ b/src/models/request/setPasswordRequest.ts @@ -9,4 +9,5 @@ export class SetPasswordRequest { keys: KeysRequest; kdf: KdfType; kdfIterations: number; + orgIdentifier: string; } From 4cd20f0fa8c040989e8b9e8037c00bbafeb9b653 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Tue, 20 Oct 2020 09:33:30 -0400 Subject: [PATCH 1087/1626] Add logging to lowdb storage service (#188) --- src/services/lowdbStorage.service.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/services/lowdbStorage.service.ts b/src/services/lowdbStorage.service.ts index ae9865c1a7..aa69717a19 100644 --- a/src/services/lowdbStorage.service.ts +++ b/src/services/lowdbStorage.service.ts @@ -3,6 +3,7 @@ import * as lowdb from 'lowdb'; import * as FileSync from 'lowdb/adapters/FileSync'; import * as path from 'path'; +import { LogService } from '../abstractions/log.service'; import { StorageService } from '../abstractions/storage.service'; import { NodeUtils } from '../misc/nodeUtils'; @@ -13,28 +14,37 @@ export class LowdbStorageService implements StorageService { private defaults: any; private dataFilePath: string; - constructor(defaults?: any, dir?: string, private allowCache = false) { + constructor(private logService: LogService, defaults?: any, dir?: string, private allowCache = false) { this.defaults = defaults; + this.logService.info('Initializing lowdb storage service.'); let adapter: lowdb.AdapterSync; if (Utils.isNode && dir != null) { if (!fs.existsSync(dir)) { + this.logService.warning(`Could not find dir, "${dir}"; creating it instead.`); NodeUtils.mkdirpSync(dir, '700'); + this.logService.info(`Created dir "${dir}".`); } this.dataFilePath = path.join(dir, 'data.json'); if (!fs.existsSync(this.dataFilePath)) { + this.logService.warning(`Could not find data file, "${this.dataFilePath}"; creating it instead.`); fs.writeFileSync(this.dataFilePath, '', { mode: 0o600 }); fs.chmodSync(this.dataFilePath, 0o600); + this.logService.info(`Created data file "${this.dataFilePath}" with chmod 600.`); } adapter = new FileSync(this.dataFilePath); } try { + this.logService.info('Attempting to create lowdb storage adapter.'); this.db = lowdb(adapter); + this.logService.info('Successfuly created lowdb storage adapter.'); } catch (e) { if (e instanceof SyntaxError) { + this.logService.warning(`Error creating lowdb storage adapter, "${e.message}"; emptying data file.`); adapter.write({}); this.db = lowdb(adapter); } else { + this.logService.error(`Error creating lowdb storage adapter, "${e.message}".`); throw e; } } @@ -42,8 +52,10 @@ export class LowdbStorageService implements StorageService { init() { if (this.defaults != null) { + this.logService.info('Writing defaults.'); this.readForNoCache(); this.db.defaults(this.defaults).write(); + this.logService.info('Successfully wrote defaults to db.'); } } From 23ded0d115a9cc0882458b38b423f186a5152543 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Tue, 20 Oct 2020 10:20:22 -0400 Subject: [PATCH 1088/1626] Fix lint errors/warnings (#187) --- src/services/api.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 6be644dc03..a7ba94045e 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -353,7 +353,7 @@ export class ApiService implements ApiServiceAbstraction { } async getSsoUserIdentifier(): Promise { - return this.send('GET', '/accounts/sso/user-identifier', null, true, true) + return this.send('GET', '/accounts/sso/user-identifier', null, true, true); } // Folder APIs @@ -701,7 +701,6 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, PlanResponse); } - async postImportDirectory(organizationId: string, request: ImportDirectoryRequest): Promise { return this.send('POST', '/organizations/' + organizationId + '/import', request, true, false); } From 76c09641ba8ef6cdbf74977a7a719b9aa099edb7 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Mon, 26 Oct 2020 16:01:17 -0400 Subject: [PATCH 1089/1626] changed OnlyOrg enum to be SingleOrg (#189) --- src/enums/policyType.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/enums/policyType.ts b/src/enums/policyType.ts index 918511556a..31285a5da2 100644 --- a/src/enums/policyType.ts +++ b/src/enums/policyType.ts @@ -2,6 +2,6 @@ export enum PolicyType { TwoFactorAuthentication = 0, // Requires users to have 2fa enabled MasterPassword = 1, // Sets minimum requirements for master password complexity PasswordGenerator = 2, // Sets minimum requirements/default type for generated passwords/passphrases - OnlyOrg = 3, // Allows users to only be apart of one organization + SingleOrg = 3, // Allows users to only be apart of one organization RequireSso = 4, // Requires users to authenticate with SSO } From 8cb5a9f505ac64d7af716678e6e3e6068166099b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 29 Oct 2020 15:52:12 -0400 Subject: [PATCH 1090/1626] hkdf crypto functions (#190) * hkdf crypto functions * comment to spec --- .../nodeCryptoFunction.service.spec.ts | 88 +++++++++++++++++++ .../webCryptoFunction.service.spec.ts | 88 +++++++++++++++++++ src/abstractions/cryptoFunction.service.ts | 4 + src/services/crypto.service.ts | 27 ++---- src/services/nodeCryptoFunction.service.ts | 50 ++++++++++- src/services/webCryptoFunction.service.ts | 49 +++++++++++ 6 files changed, 283 insertions(+), 23 deletions(-) diff --git a/spec/node/services/nodeCryptoFunction.service.spec.ts b/spec/node/services/nodeCryptoFunction.service.spec.ts index 8b58312da6..392c0a61bc 100644 --- a/spec/node/services/nodeCryptoFunction.service.spec.ts +++ b/spec/node/services/nodeCryptoFunction.service.spec.ts @@ -49,6 +49,45 @@ describe('NodeCrypto Function Service', () => { testPbkdf2('sha512', regular512Key, utf8512Key, unicode512Key); }); + describe('hkdf', () => { + const regular256Key = 'qBUmEYtwTwwGPuw/z6bs/qYXXYNUlocFlyAuuANI8Pw='; + const utf8256Key = '6DfJwW1R3txgiZKkIFTvVAb7qVlG7lKcmJGJoxR2GBU='; + const unicode256Key = 'gejGI82xthA+nKtKmIh82kjw+ttHr+ODsUoGdu5sf0A='; + + const regular512Key = 'xe5cIG6ZfwGmb1FvsOedM0XKOm21myZkjL/eDeKIqqM='; + const utf8512Key = 'XQMVBnxVEhlvjSFDQc77j5GDE9aorvbS0vKnjhRg0LY='; + const unicode512Key = '148GImrTbrjaGAe/iWEpclINM8Ehhko+9lB14+52lqc='; + + testHkdf('sha256', regular256Key, utf8256Key, unicode256Key); + testHkdf('sha512', regular512Key, utf8512Key, unicode512Key); + }); + + describe('hkdfExpand', () => { + const prk16Byte = 'criAmKtfzxanbgea5/kelQ=='; + const prk32Byte = 'F5h4KdYQnIVH4rKH0P9CZb1GrR4n16/sJrS0PsQEn0Y='; + const prk64Byte = 'ssBK0mRG17VHdtsgt8yo4v25CRNpauH+0r2fwY/E9rLyaFBAOMbIeTry+' + + 'gUJ28p8y+hFh3EI9pcrEWaNvFYonQ==' + + testHkdfExpand('sha256', prk32Byte, 32, 'BnIqJlfnHm0e/2iB/15cbHyR19ARPIcWRp4oNS22CD8='); + testHkdfExpand('sha256', prk32Byte, 64, 'BnIqJlfnHm0e/2iB/15cbHyR19ARPIcWRp4oNS22CD9BV+' + + '/queOZenPNkDhmlVyL2WZ3OSU5+7ISNF5NhNfvZA=='); + testHkdfExpand('sha512', prk64Byte, 32, 'uLWbMWodSBms5uGJ5WTRTesyW+MD7nlpCZvagvIRXlk='); + testHkdfExpand('sha512', prk64Byte, 64, 'uLWbMWodSBms5uGJ5WTRTesyW+MD7nlpCZvagvIRXlkY5Pv0sB+' + + 'MqvaopmkC6sD/j89zDwTV9Ib2fpucUydO8w=='); + + it('should fail with prk too small', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const f = cryptoFunctionService.hkdfExpand(Utils.fromB64ToArray(prk16Byte), 'info', 32, 'sha256'); + await expectAsync(f).toBeRejectedWith(new Error('prk is too small.')); + }); + + it('should fail with outputByteSize is too large', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const f = cryptoFunctionService.hkdfExpand(Utils.fromB64ToArray(prk32Byte), 'info', 8161, 'sha256'); + await expectAsync(f).toBeRejectedWith(new Error('outputByteSize is too large.')); + }); + }); + describe('hash', () => { const regular1Hash = '2a241604fb921fad12bf877282457268e1dccb70'; const utf81Hash = '85672798dc5831e96d6c48655d3d39365a9c88b6'; @@ -234,6 +273,55 @@ function testPbkdf2(algorithm: 'sha256' | 'sha512', regularKey: string, utf8Key: }); } +function testHkdf(algorithm: 'sha256' | 'sha512', regularKey: string, utf8Key: string, unicodeKey: string) { + const ikm = Utils.fromB64ToArray('criAmKtfzxanbgea5/kelQ=='); + + const regularSalt = 'salt'; + const utf8Salt = 'üser_salt'; + const unicodeSalt = '😀salt🙏'; + + const regularInfo = 'info'; + const utf8Info = 'üser_info'; + const unicodeInfo = '😀info🙏'; + + it('should create valid ' + algorithm + ' key from regular input', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const key = await cryptoFunctionService.hkdf(ikm, regularSalt, regularInfo, 32, algorithm); + expect(Utils.fromBufferToB64(key)).toBe(regularKey); + }); + + it('should create valid ' + algorithm + ' key from utf8 input', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const key = await cryptoFunctionService.hkdf(ikm, utf8Salt, utf8Info, 32, algorithm); + expect(Utils.fromBufferToB64(key)).toBe(utf8Key); + }); + + it('should create valid ' + algorithm + ' key from unicode input', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const key = await cryptoFunctionService.hkdf(ikm, unicodeSalt, unicodeInfo, 32, algorithm); + expect(Utils.fromBufferToB64(key)).toBe(unicodeKey); + }); + + it('should create valid ' + algorithm + ' key from array buffer input', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const key = await cryptoFunctionService.hkdf(ikm, Utils.fromUtf8ToArray(regularSalt).buffer, + Utils.fromUtf8ToArray(regularInfo).buffer, 32, algorithm); + expect(Utils.fromBufferToB64(key)).toBe(regularKey); + }); +} + +function testHkdfExpand(algorithm: 'sha256' | 'sha512', b64prk: string, outputByteSize: number, + b64ExpectedOkm: string) { + const info = 'info'; + + it('should create valid ' + algorithm + ' ' + outputByteSize + ' byte okm', async () => { + const cryptoFunctionService = new NodeCryptoFunctionService(); + const okm = await cryptoFunctionService.hkdfExpand(Utils.fromB64ToArray(b64prk), info, outputByteSize, + algorithm); + expect(Utils.fromBufferToB64(okm)).toBe(b64ExpectedOkm); + }); +} + function testHash(algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5', regularHash: string, utf8Hash: string, unicodeHash: string) { const regularValue = 'HashMe!!'; diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index d1ead1592e..e71cb3a493 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -53,6 +53,45 @@ describe('WebCrypto Function Service', () => { testPbkdf2('sha512', regular512Key, utf8512Key, unicode512Key); }); + describe('hkdf', () => { + const regular256Key = 'qBUmEYtwTwwGPuw/z6bs/qYXXYNUlocFlyAuuANI8Pw='; + const utf8256Key = '6DfJwW1R3txgiZKkIFTvVAb7qVlG7lKcmJGJoxR2GBU='; + const unicode256Key = 'gejGI82xthA+nKtKmIh82kjw+ttHr+ODsUoGdu5sf0A='; + + const regular512Key = 'xe5cIG6ZfwGmb1FvsOedM0XKOm21myZkjL/eDeKIqqM='; + const utf8512Key = 'XQMVBnxVEhlvjSFDQc77j5GDE9aorvbS0vKnjhRg0LY='; + const unicode512Key = '148GImrTbrjaGAe/iWEpclINM8Ehhko+9lB14+52lqc='; + + testHkdf('sha256', regular256Key, utf8256Key, unicode256Key); + testHkdf('sha512', regular512Key, utf8512Key, unicode512Key); + }); + + describe('hkdfExpand', () => { + const prk16Byte = 'criAmKtfzxanbgea5/kelQ=='; + const prk32Byte = 'F5h4KdYQnIVH4rKH0P9CZb1GrR4n16/sJrS0PsQEn0Y='; + const prk64Byte = 'ssBK0mRG17VHdtsgt8yo4v25CRNpauH+0r2fwY/E9rLyaFBAOMbIeTry+' + + 'gUJ28p8y+hFh3EI9pcrEWaNvFYonQ==' + + testHkdfExpand('sha256', prk32Byte, 32, 'BnIqJlfnHm0e/2iB/15cbHyR19ARPIcWRp4oNS22CD8='); + testHkdfExpand('sha256', prk32Byte, 64, 'BnIqJlfnHm0e/2iB/15cbHyR19ARPIcWRp4oNS22CD9BV+' + + '/queOZenPNkDhmlVyL2WZ3OSU5+7ISNF5NhNfvZA=='); + testHkdfExpand('sha512', prk64Byte, 32, 'uLWbMWodSBms5uGJ5WTRTesyW+MD7nlpCZvagvIRXlk='); + testHkdfExpand('sha512', prk64Byte, 64, 'uLWbMWodSBms5uGJ5WTRTesyW+MD7nlpCZvagvIRXlkY5Pv0sB+' + + 'MqvaopmkC6sD/j89zDwTV9Ib2fpucUydO8w=='); + + it('should fail with prk too small', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const f = cryptoFunctionService.hkdfExpand(Utils.fromB64ToArray(prk16Byte), 'info', 32, 'sha256'); + await expectAsync(f).toBeRejectedWith(new Error('prk is too small.')); + }); + + it('should fail with outputByteSize is too large', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const f = cryptoFunctionService.hkdfExpand(Utils.fromB64ToArray(prk32Byte), 'info', 8161, 'sha256'); + await expectAsync(f).toBeRejectedWith(new Error('outputByteSize is too large.')); + }); + }); + describe('hash', () => { const regular1Hash = '2a241604fb921fad12bf877282457268e1dccb70'; const utf81Hash = '85672798dc5831e96d6c48655d3d39365a9c88b6'; @@ -318,6 +357,55 @@ function testPbkdf2(algorithm: 'sha256' | 'sha512', regularKey: string, }); } +function testHkdf(algorithm: 'sha256' | 'sha512', regularKey: string, utf8Key: string, unicodeKey: string) { + const ikm = Utils.fromB64ToArray('criAmKtfzxanbgea5/kelQ=='); + + const regularSalt = 'salt'; + const utf8Salt = 'üser_salt'; + const unicodeSalt = '😀salt🙏'; + + const regularInfo = 'info'; + const utf8Info = 'üser_info'; + const unicodeInfo = '😀info🙏'; + + it('should create valid ' + algorithm + ' key from regular input', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const key = await cryptoFunctionService.hkdf(ikm, regularSalt, regularInfo, 32, algorithm); + expect(Utils.fromBufferToB64(key)).toBe(regularKey); + }); + + it('should create valid ' + algorithm + ' key from utf8 input', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const key = await cryptoFunctionService.hkdf(ikm, utf8Salt, utf8Info, 32, algorithm); + expect(Utils.fromBufferToB64(key)).toBe(utf8Key); + }); + + it('should create valid ' + algorithm + ' key from unicode input', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const key = await cryptoFunctionService.hkdf(ikm, unicodeSalt, unicodeInfo, 32, algorithm); + expect(Utils.fromBufferToB64(key)).toBe(unicodeKey); + }); + + it('should create valid ' + algorithm + ' key from array buffer input', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const key = await cryptoFunctionService.hkdf(ikm, Utils.fromUtf8ToArray(regularSalt).buffer, + Utils.fromUtf8ToArray(regularInfo).buffer, 32, algorithm); + expect(Utils.fromBufferToB64(key)).toBe(regularKey); + }); +} + +function testHkdfExpand(algorithm: 'sha256' | 'sha512', b64prk: string, outputByteSize: number, + b64ExpectedOkm: string) { + const info = 'info'; + + it('should create valid ' + algorithm + ' ' + outputByteSize + ' byte okm', async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const okm = await cryptoFunctionService.hkdfExpand(Utils.fromB64ToArray(b64prk), info, outputByteSize, + algorithm); + expect(Utils.fromBufferToB64(okm)).toBe(b64ExpectedOkm); + }); +} + function testHash(algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5', regularHash: string, utf8Hash: string, unicodeHash: string) { const regularValue = 'HashMe!!'; diff --git a/src/abstractions/cryptoFunction.service.ts b/src/abstractions/cryptoFunction.service.ts index 45b1014a29..ddb38d1fe7 100644 --- a/src/abstractions/cryptoFunction.service.ts +++ b/src/abstractions/cryptoFunction.service.ts @@ -4,6 +4,10 @@ import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; export abstract class CryptoFunctionService { pbkdf2: (password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', iterations: number) => Promise; + hkdf: (ikm: ArrayBuffer, salt: string | ArrayBuffer, info: string | ArrayBuffer, + outputByteSize: number, algorithm: 'sha256' | 'sha512') => Promise + hkdfExpand: (prk: ArrayBuffer, info: string | ArrayBuffer, outputByteSize: number, + algorithm: 'sha256' | 'sha512') => Promise; hash: (value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5') => Promise; hmac: (value: ArrayBuffer, key: ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512') => Promise; compare: (a: ArrayBuffer, b: ArrayBuffer) => Promise; diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 2037a57d85..df44ef7789 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -182,8 +182,8 @@ export class CryptoService implements CryptoServiceAbstraction { throw new Error('No public key available.'); } const keyFingerprint = await this.cryptoFunctionService.hash(publicKey, 'sha256'); - const userFingerprint = await this.hkdfExpand(keyFingerprint, Utils.fromUtf8ToArray(userId), 32); - return this.hashPhrase(userFingerprint.buffer); + const userFingerprint = await this.cryptoFunctionService.hkdfExpand(keyFingerprint, userId, 32, 'sha256'); + return this.hashPhrase(userFingerprint); } @sequentialize(() => 'getOrgKeys') @@ -679,28 +679,13 @@ export class CryptoService implements CryptoServiceAbstraction { private async stretchKey(key: SymmetricCryptoKey): Promise { const newKey = new Uint8Array(64); - newKey.set(await this.hkdfExpand(key.key, Utils.fromUtf8ToArray('enc'), 32)); - newKey.set(await this.hkdfExpand(key.key, Utils.fromUtf8ToArray('mac'), 32), 32); + const encKey = await this.cryptoFunctionService.hkdfExpand(key.key, 'enc', 32, 'sha256'); + const macKey = await this.cryptoFunctionService.hkdfExpand(key.key, 'mac', 32, 'sha256'); + newKey.set(new Uint8Array(encKey)); + newKey.set(new Uint8Array(macKey), 32); return new SymmetricCryptoKey(newKey.buffer); } - // ref: https://tools.ietf.org/html/rfc5869 - private async hkdfExpand(prk: ArrayBuffer, info: Uint8Array, size: number) { - const hashLen = 32; // sha256 - const okm = new Uint8Array(size); - let previousT = new Uint8Array(0); - const n = Math.ceil(size / hashLen); - for (let i = 0; i < n; i++) { - const t = new Uint8Array(previousT.length + info.length + 1); - t.set(previousT); - t.set(info, previousT.length); - t.set([i + 1], t.length - 1); - previousT = new Uint8Array(await this.cryptoFunctionService.hmac(t.buffer, prk, 'sha256')); - okm.set(previousT, i * hashLen); - } - return okm; - } - private async hashPhrase(hash: ArrayBuffer, minimumEntropy: number = 64) { const entropyPerWord = Math.log(EEFLongWordList.length) / Math.log(2); let numWords = Math.ceil(minimumEntropy / entropyPerWord); diff --git a/src/services/nodeCryptoFunction.service.ts b/src/services/nodeCryptoFunction.service.ts index d88be72d99..9e8d9131e6 100644 --- a/src/services/nodeCryptoFunction.service.ts +++ b/src/services/nodeCryptoFunction.service.ts @@ -26,6 +26,46 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { }); } + // ref: https://tools.ietf.org/html/rfc5869 + async hkdf(ikm: ArrayBuffer, salt: string | ArrayBuffer, info: string | ArrayBuffer, + outputByteSize: number, algorithm: 'sha256' | 'sha512'): Promise { + const saltBuf = this.toArrayBuffer(salt); + const prk = await this.hmac(ikm, saltBuf, algorithm); + return this.hkdfExpand(prk, info, outputByteSize, algorithm); + } + + // ref: https://tools.ietf.org/html/rfc5869 + async hkdfExpand(prk: ArrayBuffer, info: string | ArrayBuffer, outputByteSize: number, + algorithm: 'sha256' | 'sha512'): Promise { + const hashLen = algorithm === 'sha256' ? 32 : 64; + if (outputByteSize > 255 * hashLen) { + throw new Error('outputByteSize is too large.'); + } + const prkArr = new Uint8Array(prk); + if (prkArr.length < hashLen) { + throw new Error('prk is too small.'); + } + const infoBuf = this.toArrayBuffer(info); + const infoArr = new Uint8Array(infoBuf); + let runningOkmLength = 0; + let previousT = new Uint8Array(0); + const n = Math.ceil(outputByteSize / hashLen); + const okm = new Uint8Array(n * hashLen); + for (let i = 0; i < n; i++) { + const t = new Uint8Array(previousT.length + infoArr.length + 1); + t.set(previousT); + t.set(infoArr, previousT.length); + t.set([i + 1], t.length - 1); + previousT = new Uint8Array(await this.hmac(t.buffer, prk, algorithm)); + okm.set(previousT, runningOkmLength); + runningOkmLength += previousT.length; + if (runningOkmLength >= outputByteSize) { + break; + } + } + return okm.slice(0, outputByteSize).buffer; + } + hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5'): Promise { const nodeValue = this.toNodeValue(value); const hash = crypto.createHash(algorithm); @@ -196,8 +236,14 @@ export class NodeCryptoFunctionService implements CryptoFunctionService { return Buffer.from(new Uint8Array(value) as any); } - private toArrayBuffer(buf: Buffer): ArrayBuffer { - return new Uint8Array(buf).buffer; + private toArrayBuffer(value: Buffer | string | ArrayBuffer): ArrayBuffer { + let buf: ArrayBuffer; + if (typeof (value) === 'string') { + buf = Utils.fromUtf8ToArray(value).buffer; + } else { + buf = new Uint8Array(value).buffer; + } + return buf; } private toPemPrivateKey(key: ArrayBuffer): string { diff --git a/src/services/webCryptoFunction.service.ts b/src/services/webCryptoFunction.service.ts index a651d778d3..006102ebd0 100644 --- a/src/services/webCryptoFunction.service.ts +++ b/src/services/webCryptoFunction.service.ts @@ -49,6 +49,55 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return await this.subtle.deriveBits(pbkdf2Params, impKey, wcLen); } + async hkdf(ikm: ArrayBuffer, salt: string | ArrayBuffer, info: string | ArrayBuffer, + outputByteSize: number, algorithm: 'sha256' | 'sha512'): Promise { + const saltBuf = this.toBuf(salt); + const infoBuf = this.toBuf(info); + + const hkdfParams: HkdfParams = { + name: 'HKDF', + salt: saltBuf, + info: infoBuf, + hash: { name: this.toWebCryptoAlgorithm(algorithm) }, + }; + + const impKey = await this.subtle.importKey('raw', ikm, { name: 'HKDF' } as any, + false, ['deriveBits']); + return await this.subtle.deriveBits(hkdfParams as any, impKey, outputByteSize * 8); + } + + // ref: https://tools.ietf.org/html/rfc5869 + async hkdfExpand(prk: ArrayBuffer, info: string | ArrayBuffer, outputByteSize: number, + algorithm: 'sha256' | 'sha512'): Promise { + const hashLen = algorithm === 'sha256' ? 32 : 64; + if (outputByteSize > 255 * hashLen) { + throw new Error('outputByteSize is too large.'); + } + const prkArr = new Uint8Array(prk); + if (prkArr.length < hashLen) { + throw new Error('prk is too small.'); + } + const infoBuf = this.toBuf(info); + const infoArr = new Uint8Array(infoBuf); + let runningOkmLength = 0; + let previousT = new Uint8Array(0); + const n = Math.ceil(outputByteSize / hashLen); + const okm = new Uint8Array(n * hashLen); + for (let i = 0; i < n; i++) { + const t = new Uint8Array(previousT.length + infoArr.length + 1); + t.set(previousT); + t.set(infoArr, previousT.length); + t.set([i + 1], t.length - 1); + previousT = new Uint8Array(await this.hmac(t.buffer, prk, algorithm)); + okm.set(previousT, runningOkmLength); + runningOkmLength += previousT.length; + if (runningOkmLength >= outputByteSize) { + break; + } + } + return okm.slice(0, outputByteSize).buffer; + } + async hash(value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5'): Promise { if ((this.isIE && algorithm === 'sha1') || algorithm === 'md5') { const md = algorithm === 'md5' ? forge.md.md5.create() : forge.md.sha1.create(); From 5e50aa1a195bde11fdc14e9bdf71542766fdbb8d Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Mon, 2 Nov 2020 12:50:58 -0600 Subject: [PATCH 1091/1626] Added terms/privacy variables & logic (#193) --- src/angular/components/register.component.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index 062b1fa0f2..a0f49cf5f1 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -24,6 +24,8 @@ export class RegisterComponent { formPromise: Promise; masterPasswordScore: number; referenceData: ReferenceEventRequest; + showTerms = true; + acceptPolicies: boolean = false; protected successRoute = 'login'; private masterPasswordStrengthTimeout: any; @@ -32,7 +34,9 @@ export class RegisterComponent { protected i18nService: I18nService, protected cryptoService: CryptoService, protected apiService: ApiService, protected stateService: StateService, protected platformUtilsService: PlatformUtilsService, - protected passwordGenerationService: PasswordGenerationService) { } + protected passwordGenerationService: PasswordGenerationService) { + this.showTerms = !platformUtilsService.isSelfHost(); + } get masterPasswordScoreWidth() { return this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20; @@ -65,6 +69,12 @@ export class RegisterComponent { } async submit() { + if (!this.acceptPolicies && this.showTerms) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('acceptPoliciesError')); + return; + } + if (this.email == null || this.email === '') { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('emailRequired')); From 0e9e73ce95a321ee05edbb62c50f9e1828f69c5a Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 2 Nov 2020 15:58:18 -0500 Subject: [PATCH 1092/1626] Some groundwork for Send (#192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * send work * New method to update the last used index (#184) Instead of updating it every time you call getNext(), it will be updated in a separate call, to avoid updating the index when the cipher did not auto-fill correctly (e.g wrong frame) Fixes #1392 * added OnlyOrg to PolicyType enum (#183) * [Require SSO] Add policy type enumeration (#186) * Added SsoAuthentication policy type * Updated policy type name // added comments for clarification of what each type controls * [SSO] New user provision flow (#173) * Initial commit of new user sso flow * Adjusted stateSplit conditional per review * Add logging to lowdb storage service (#188) * Fix lint errors/warnings (#187) * remove password api * access id * makeSendKey Co-authored-by: Josep Marí Co-authored-by: Addison Beck Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com> --- src/abstractions/api.service.ts | 13 ++ src/abstractions/crypto.service.ts | 1 + src/enums/sendType.ts | 4 + src/misc/utils.ts | 22 +++- src/models/api/sendFileApi.ts | 23 ++++ src/models/api/sendTextApi.ts | 15 +++ src/models/data/sendData.ts | 57 +++++++++ src/models/data/sendFileData.ts | 23 ++++ src/models/data/sendTextData.ts | 15 +++ src/models/domain/cipherString.ts | 10 +- src/models/domain/domainBase.ts | 7 +- src/models/domain/send.ts | 140 ++++++++++++++++++++++ src/models/domain/sendAccess.ts | 65 ++++++++++ src/models/domain/sendFile.ts | 49 ++++++++ src/models/domain/sendText.ts | 39 ++++++ src/models/request/sendAccessRequest.ts | 3 + src/models/request/sendRequest.ts | 46 +++++++ src/models/response/baseResponse.ts | 2 +- src/models/response/sendAccessResponse.ts | 31 +++++ src/models/response/sendResponse.ts | 51 ++++++++ src/models/view/sendAccessView.ts | 24 ++++ src/models/view/sendFileView.ts | 31 +++++ src/models/view/sendTextView.ts | 20 ++++ src/models/view/sendView.ts | 49 ++++++++ src/services/api.service.ts | 56 ++++++++- src/services/crypto.service.ts | 5 + 26 files changed, 783 insertions(+), 18 deletions(-) create mode 100644 src/enums/sendType.ts create mode 100644 src/models/api/sendFileApi.ts create mode 100644 src/models/api/sendTextApi.ts create mode 100644 src/models/data/sendData.ts create mode 100644 src/models/data/sendFileData.ts create mode 100644 src/models/data/sendTextData.ts create mode 100644 src/models/domain/send.ts create mode 100644 src/models/domain/sendAccess.ts create mode 100644 src/models/domain/sendFile.ts create mode 100644 src/models/domain/sendText.ts create mode 100644 src/models/request/sendAccessRequest.ts create mode 100644 src/models/request/sendRequest.ts create mode 100644 src/models/response/sendAccessResponse.ts create mode 100644 src/models/response/sendResponse.ts create mode 100644 src/models/view/sendAccessView.ts create mode 100644 src/models/view/sendFileView.ts create mode 100644 src/models/view/sendTextView.ts create mode 100644 src/models/view/sendView.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index a8faa65bb7..189b4c9c7c 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -42,6 +42,8 @@ import { PreloginRequest } from '../models/request/preloginRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { SeatRequest } from '../models/request/seatRequest'; import { SelectionReadOnlyRequest } from '../models/request/selectionReadOnlyRequest'; +import { SendAccessRequest } from '../models/request/sendAccessRequest'; +import { SendRequest } from '../models/request/sendRequest'; import { SetPasswordRequest } from '../models/request/setPasswordRequest'; import { StorageRequest } from '../models/request/storageRequest'; import { TaxInfoUpdateRequest } from '../models/request/taxInfoUpdateRequest'; @@ -92,6 +94,8 @@ import { PolicyResponse } from '../models/response/policyResponse'; import { PreloginResponse } from '../models/response/preloginResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SelectionReadOnlyResponse } from '../models/response/selectionReadOnlyResponse'; +import { SendAccessResponse } from '../models/response/sendAccessResponse'; +import { SendResponse } from '../models/response/sendResponse'; import { SubscriptionResponse } from '../models/response/subscriptionResponse'; import { SyncResponse } from '../models/response/syncResponse'; import { TaxInfoResponse } from '../models/response/taxInfoResponse'; @@ -155,6 +159,15 @@ export abstract class ApiService { putFolder: (id: string, request: FolderRequest) => Promise; deleteFolder: (id: string) => Promise; + getSend: (id: string) => Promise; + postSendAccess: (id: string, request: SendAccessRequest) => Promise; + getSends: () => Promise>; + postSend: (request: SendRequest) => Promise; + postSendFile: (data: FormData) => Promise; + putSend: (id: string, request: SendRequest) => Promise; + putSendRemovePassword: (id: string) => Promise; + deleteSend: (id: string) => Promise; + getCipher: (id: string) => Promise; getCipherAdmin: (id: string) => Promise; getCiphersOrganization: (organizationId: string) => Promise>; diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 2805e1e983..53376e19e9 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -35,6 +35,7 @@ export abstract class CryptoService { makeShareKey: () => Promise<[CipherString, SymmetricCryptoKey]>; makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, CipherString]>; makePinKey: (pin: string, salt: string, kdf: KdfType, kdfIterations: number) => Promise; + makeSendKey: (keyMaterial: ArrayBuffer) => Promise; hashPassword: (password: string, key: SymmetricCryptoKey) => Promise; makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; remakeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; diff --git a/src/enums/sendType.ts b/src/enums/sendType.ts new file mode 100644 index 0000000000..f5715d869b --- /dev/null +++ b/src/enums/sendType.ts @@ -0,0 +1,4 @@ +export enum SendType { + Text = 0, + File = 1, +} diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 4d9bf8533e..cacda81e3e 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -43,6 +43,10 @@ export class Utils { } } + static fromUrlB64ToArray(str: string): Uint8Array { + return Utils.fromB64ToArray(Utils.fromUrlB64ToB64(str)); + } + static fromHexToArray(str: string): Uint8Array { if (Utils.isNode || Utils.isNativeScript) { return new Uint8Array(Buffer.from(str, 'hex')); @@ -90,11 +94,13 @@ export class Utils { } static fromBufferToUrlB64(buffer: ArrayBuffer): string { - const output = this.fromBufferToB64(buffer) - .replace(/\+/g, '-') + return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer)) + } + + static fromB64toUrlB64(b64Str: string) { + return b64Str.replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); - return output; } static fromBufferToUtf8(buffer: ArrayBuffer): string { @@ -121,8 +127,8 @@ export class Utils { } } - static fromUrlB64ToUtf8(b64Str: string): string { - let output = b64Str.replace(/-/g, '+').replace(/_/g, '/'); + static fromUrlB64ToB64(urlB64Str: string): string { + let output = urlB64Str.replace(/-/g, '+').replace(/_/g, '/'); switch (output.length % 4) { case 0: break; @@ -136,7 +142,11 @@ export class Utils { throw new Error('Illegal base64url string!'); } - return Utils.fromB64ToUtf8(output); + return output; + } + + static fromUrlB64ToUtf8(urlB64Str: string): string { + return Utils.fromB64ToUtf8(Utils.fromUrlB64ToB64(urlB64Str)); } static fromB64ToUtf8(b64Str: string): string { diff --git a/src/models/api/sendFileApi.ts b/src/models/api/sendFileApi.ts new file mode 100644 index 0000000000..96d386711d --- /dev/null +++ b/src/models/api/sendFileApi.ts @@ -0,0 +1,23 @@ +import { BaseResponse } from '../response/baseResponse'; + +export class SendFileApi extends BaseResponse { + id: string; + url: string; + fileName: string; + key: string; + size: string; + sizeName: string; + + constructor(data: any = null) { + super(data); + if (data == null) { + return; + } + this.id = this.getResponseProperty('Id'); + this.url = this.getResponseProperty('Url'); + this.fileName = this.getResponseProperty('FileName'); + this.key = this.getResponseProperty('Key'); + this.size = this.getResponseProperty('Size'); + this.sizeName = this.getResponseProperty('SizeName'); + } +} diff --git a/src/models/api/sendTextApi.ts b/src/models/api/sendTextApi.ts new file mode 100644 index 0000000000..083077c389 --- /dev/null +++ b/src/models/api/sendTextApi.ts @@ -0,0 +1,15 @@ +import { BaseResponse } from '../response/baseResponse'; + +export class SendTextApi extends BaseResponse { + text: string; + hidden: boolean; + + constructor(data: any = null) { + super(data); + if (data == null) { + return; + } + this.text = this.getResponseProperty('Text'); + this.hidden = this.getResponseProperty('Hidden') || false; + } +} diff --git a/src/models/data/sendData.ts b/src/models/data/sendData.ts new file mode 100644 index 0000000000..2d60a37115 --- /dev/null +++ b/src/models/data/sendData.ts @@ -0,0 +1,57 @@ +import { SendType } from '../../enums/sendType'; + +import { SendFileData } from './sendFileData'; +import { SendTextData } from './sendTextData'; + +import { SendResponse } from '../response/sendResponse'; + +export class SendData { + id: string; + accessId: string; + userId: string; + type: SendType; + name: string; + notes: string; + file: SendFileData; + text: SendTextData; + key: string; + maxAccessCount?: number; + accessCount: number; + revisionDate: string; + expirationDate: string; + deletionDate: string; + password: string; + disabled: boolean; + + constructor(response?: SendResponse, userId?: string) { + if (response == null) { + return; + } + + this.id = response.id; + this.accessId = response.accessId; + this.userId = userId; + this.type = response.type; + this.name = response.name; + this.notes = response.notes; + this.key = response.key; + this.maxAccessCount = response.maxAccessCount; + this.accessCount = response.accessCount; + this.revisionDate = response.revisionDate; + this.expirationDate = response.expirationDate; + this.deletionDate = response.deletionDate; + this.password = response.password; + this.disabled = response.disable; + + switch (this.type) { + case SendType.Text: + this.text = new SendTextData(response.text); + break; + case SendType.File: + this.file = new SendFileData(response.file); + break; + default: + break; + } + } +} diff --git a/src/models/data/sendFileData.ts b/src/models/data/sendFileData.ts new file mode 100644 index 0000000000..5301f7d11e --- /dev/null +++ b/src/models/data/sendFileData.ts @@ -0,0 +1,23 @@ +import { SendFileApi } from '../api/sendFileApi'; + +export class SendFileData { + id: string; + url: string; + fileName: string; + key: string; + size: string; + sizeName: string; + + constructor(data?: SendFileApi) { + if (data == null) { + return; + } + + this.id = data.id; + this.url = data.url; + this.fileName = data.fileName; + this.key = data.key; + this.size = data.size; + this.sizeName = data.sizeName; + } +} diff --git a/src/models/data/sendTextData.ts b/src/models/data/sendTextData.ts new file mode 100644 index 0000000000..1d34addea2 --- /dev/null +++ b/src/models/data/sendTextData.ts @@ -0,0 +1,15 @@ +import { SendTextApi } from '../api/sendTextApi'; + +export class SendTextData { + text: string; + hidden: boolean; + + constructor(data?: SendTextApi) { + if (data == null) { + return; + } + + this.text = data.text; + this.hidden = data.hidden; + } +} diff --git a/src/models/domain/cipherString.ts b/src/models/domain/cipherString.ts index 703945bd5b..d3fd879b7c 100644 --- a/src/models/domain/cipherString.ts +++ b/src/models/domain/cipherString.ts @@ -4,6 +4,8 @@ import { CryptoService } from '../../abstractions/crypto.service'; import { Utils } from '../../misc/utils'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; + export class CipherString { encryptedString?: string; encryptionType?: EncryptionType; @@ -89,7 +91,7 @@ export class CipherString { } } - async decrypt(orgId: string): Promise { + async decrypt(orgId: string, key: SymmetricCryptoKey = null): Promise { if (this.decryptedValue != null) { return this.decryptedValue; } @@ -103,8 +105,10 @@ export class CipherString { } try { - const orgKey = await cryptoService.getOrgKey(orgId); - this.decryptedValue = await cryptoService.decryptToUtf8(this, orgKey); + if (key == null) { + key = await cryptoService.getOrgKey(orgId); + } + this.decryptedValue = await cryptoService.decryptToUtf8(this, key); } catch (e) { this.decryptedValue = '[error: cannot decrypt]'; } diff --git a/src/models/domain/domainBase.ts b/src/models/domain/domainBase.ts index b73297bcd5..bac499c6b2 100644 --- a/src/models/domain/domainBase.ts +++ b/src/models/domain/domainBase.ts @@ -2,6 +2,8 @@ import { CipherString } from './cipherString'; import { View } from '../view/view'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; + export default class Domain { protected buildDomainModel(domain: D, dataObj: any, map: any, alreadyEncrypted: boolean, notEncList: any[] = []) { @@ -33,7 +35,8 @@ export default class Domain { } } - protected async decryptObj(viewModel: T, map: any, orgId: string): Promise { + protected async decryptObj(viewModel: T, map: any, orgId: string, + key: SymmetricCryptoKey = null): Promise { const promises = []; const self: any = this; @@ -47,7 +50,7 @@ export default class Domain { const p = Promise.resolve().then(() => { const mapProp = map[theProp] || theProp; if (self[mapProp]) { - return self[mapProp].decrypt(orgId); + return self[mapProp].decrypt(orgId, key); } return null; }).then((val: any) => { diff --git a/src/models/domain/send.ts b/src/models/domain/send.ts new file mode 100644 index 0000000000..c80ceac56d --- /dev/null +++ b/src/models/domain/send.ts @@ -0,0 +1,140 @@ +import { CryptoService } from '../../abstractions/crypto.service'; + +import { SendType } from '../../enums/sendType'; + +import { Utils } from '../../misc/utils'; + +import { SendData } from '../data/sendData'; + +import { SendView } from '../view/sendView'; + +import { CipherString } from './cipherString'; +import Domain from './domainBase'; +import { SendFile } from './sendFile'; +import { SendText } from './sendText'; + +export class Send extends Domain { + id: string; + accessId: string; + userId: string; + type: SendType; + name: CipherString; + notes: CipherString; + file: SendFile; + text: SendText; + key: CipherString; + maxAccessCount?: number; + accessCount: number; + revisionDate: Date; + expirationDate: Date; + deletionDate: Date; + password: string; + disabled: boolean; + + constructor(obj?: SendData, alreadyEncrypted: boolean = false) { + super(); + if (obj == null) { + return; + } + + this.buildDomainModel(this, obj, { + id: null, + accessId: null, + userId: null, + name: null, + notes: null, + key: null, + }, alreadyEncrypted, ['id', 'accessId', 'userId']); + + this.type = obj.type; + this.maxAccessCount = obj.maxAccessCount; + this.accessCount = obj.accessCount; + this.password = obj.password; + this.disabled = obj.disabled; + this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null; + this.deletionDate = obj.deletionDate != null ? new Date(obj.deletionDate) : null; + this.expirationDate = obj.expirationDate != null ? new Date(obj.expirationDate) : null; + + switch (this.type) { + case SendType.Text: + this.text = new SendText(obj.text, alreadyEncrypted); + break; + case SendType.File: + this.file = new SendFile(obj.file, alreadyEncrypted); + break; + default: + break; + } + } + + async decrypt(): Promise { + const model = new SendView(this); + + let cryptoService: CryptoService; + const containerService = (Utils.global as any).bitwardenContainerService; + if (containerService) { + cryptoService = containerService.getCryptoService(); + } else { + throw new Error('global bitwardenContainerService not initialized.'); + } + + try { + model.key = await cryptoService.decryptToBytes(this.key, null); + model.cryptoKey = await cryptoService.makeSendKey(model.key); + } catch (e) { + // TODO: error? + } + + await this.decryptObj(model, { + name: null, + notes: null, + }, null, model.cryptoKey); + + switch (this.type) { + case SendType.File: + model.file = await this.file.decrypt(model.cryptoKey); + break; + case SendType.Text: + model.text = await this.text.decrypt(model.cryptoKey); + break; + default: + break; + } + + return model; + } + + toSendData(userId: string): SendData { + const s = new SendData(); + s.id = this.id; + s.accessId = this.accessId; + s.userId = userId; + s.maxAccessCount = this.maxAccessCount; + s.accessCount = this.accessCount; + s.disabled = this.disabled; + s.password = this.password; + s.revisionDate = this.revisionDate != null ? this.revisionDate.toISOString() : null; + s.deletionDate = this.deletionDate != null ? this.deletionDate.toISOString() : null; + s.expirationDate = this.expirationDate != null ? this.expirationDate.toISOString() : null; + s.type = this.type; + + this.buildDataModel(this, s, { + name: null, + notes: null, + key: null, + }); + + switch (s.type) { + case SendType.File: + s.text = this.text.toSendTextData(); + break; + case SendType.Text: + s.file = this.file.toSendFileData(); + break; + default: + break; + } + + return s; + } +} diff --git a/src/models/domain/sendAccess.ts b/src/models/domain/sendAccess.ts new file mode 100644 index 0000000000..155f0c4fbc --- /dev/null +++ b/src/models/domain/sendAccess.ts @@ -0,0 +1,65 @@ +import { SendType } from '../../enums/sendType'; + +import { SendAccessResponse } from '../response/sendAccessResponse'; + +import { SendAccessView } from '../view/sendAccessView'; + +import { CipherString } from './cipherString'; +import Domain from './domainBase'; +import { SendFile } from './sendFile'; +import { SendText } from './sendText'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; + +export class SendAccess extends Domain { + id: string; + type: SendType; + name: CipherString; + file: SendFile; + text: SendText; + + constructor(obj?: SendAccessResponse, alreadyEncrypted: boolean = false) { + super(); + if (obj == null) { + return; + } + + this.buildDomainModel(this, obj, { + id: null, + name: null, + }, alreadyEncrypted, ['id']); + + this.type = obj.type; + + switch (this.type) { + case SendType.Text: + this.text = new SendText(obj.text, alreadyEncrypted); + break; + case SendType.File: + this.file = new SendFile(obj.file, alreadyEncrypted); + break; + default: + break; + } + } + + async decrypt(key: SymmetricCryptoKey): Promise { + const model = new SendAccessView(this); + + await this.decryptObj(model, { + name: null, + }, null, key); + + switch (this.type) { + case SendType.File: + model.file = await this.file.decrypt(key); + break; + case SendType.Text: + model.text = await this.text.decrypt(key); + break; + default: + break; + } + + return model; + } +} diff --git a/src/models/domain/sendFile.ts b/src/models/domain/sendFile.ts new file mode 100644 index 0000000000..ff462ce823 --- /dev/null +++ b/src/models/domain/sendFile.ts @@ -0,0 +1,49 @@ +import { CipherString } from './cipherString'; +import Domain from './domainBase'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; + +import { SendFileData } from '../data/sendFileData'; + +import { SendFileView } from '../view/sendFileView'; + +export class SendFile extends Domain { + id: string; + url: string; + size: string; + sizeName: string; + fileName: CipherString; + + constructor(obj?: SendFileData, 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']); + } + + async decrypt(key: SymmetricCryptoKey): Promise { + const view = await this.decryptObj(new SendFileView(this), { + fileName: null, + }, null, key); + return view; + } + + toSendFileData(): SendFileData { + const f = new SendFileData(); + f.size = this.size; + this.buildDataModel(this, f, { + id: null, + url: null, + sizeName: null, + fileName: null, + }, ['id', 'url', 'sizeName']); + return f; + } +} diff --git a/src/models/domain/sendText.ts b/src/models/domain/sendText.ts new file mode 100644 index 0000000000..592a085290 --- /dev/null +++ b/src/models/domain/sendText.ts @@ -0,0 +1,39 @@ +import { CipherString } from './cipherString'; +import Domain from './domainBase'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; + +import { SendTextData } from '../data/sendTextData'; + +import { SendTextView } from '../view/sendTextView'; + +export class SendText extends Domain { + text: CipherString; + hidden: boolean; + + constructor(obj?: SendTextData, alreadyEncrypted: boolean = false) { + super(); + if (obj == null) { + return; + } + + this.hidden = obj.hidden; + this.buildDomainModel(this, obj, { + text: null, + }, alreadyEncrypted, []); + } + + decrypt(key: SymmetricCryptoKey): Promise { + return this.decryptObj(new SendTextView(this), { + text: null, + }, null, key); + } + + toSendTextData(): SendTextData { + const t = new SendTextData(); + this.buildDataModel(this, t, { + text: null, + hidden: null, + }, ['hidden']); + return t; + } +} diff --git a/src/models/request/sendAccessRequest.ts b/src/models/request/sendAccessRequest.ts new file mode 100644 index 0000000000..3d49c0c218 --- /dev/null +++ b/src/models/request/sendAccessRequest.ts @@ -0,0 +1,3 @@ +export class SendAccessRequest { + password: string; +} diff --git a/src/models/request/sendRequest.ts b/src/models/request/sendRequest.ts new file mode 100644 index 0000000000..a2c64a2ca0 --- /dev/null +++ b/src/models/request/sendRequest.ts @@ -0,0 +1,46 @@ +import { SendType } from '../../enums/sendType'; + +import { SendFileApi } from '../api/sendFileApi' +import { SendTextApi } from '../api/sendTextApi'; + +import { Send } from '../domain/send'; + +export class SendRequest { + type: SendType; + name: string; + notes: string; + key: string; + maxAccessCount?: number; + expirationDate: string; + deletionDate: string; + text: SendTextApi; + file: SendFileApi; + password: string; + disabled: boolean; + + constructor(send: Send) { + this.type = send.type; + this.name = send.name ? send.name.encryptedString : null; + this.notes = send.notes ? send.notes.encryptedString : null; + this.maxAccessCount = send.maxAccessCount; + this.expirationDate = send.expirationDate != null ? send.expirationDate.toISOString() : null; + this.deletionDate = send.deletionDate != null ? send.deletionDate.toISOString() : null; + this.key = send.key != null ? send.key.encryptedString : null; + this.password = send.password; + this.disabled = send.disabled; + + switch (this.type) { + case SendType.Text: + this.text = new SendTextApi(); + this.text.text = send.text.text != null ? send.text.text.encryptedString : null; + this.text.hidden = send.text.hidden; + break; + case SendType.File: + this.file = new SendFileApi(); + this.file.fileName = send.file.fileName != null ? send.file.fileName.encryptedString : null; + break; + default: + break; + } + } +} diff --git a/src/models/response/baseResponse.ts b/src/models/response/baseResponse.ts index 645689f8f8..3818432237 100644 --- a/src/models/response/baseResponse.ts +++ b/src/models/response/baseResponse.ts @@ -1,5 +1,5 @@ export abstract class BaseResponse { - protected response: any; + private response: any; constructor(response: any) { this.response = response; diff --git a/src/models/response/sendAccessResponse.ts b/src/models/response/sendAccessResponse.ts new file mode 100644 index 0000000000..89ae9aba9b --- /dev/null +++ b/src/models/response/sendAccessResponse.ts @@ -0,0 +1,31 @@ +import { BaseResponse } from './baseResponse'; + +import { SendType } from '../../enums/sendType'; + +import { SendFileApi } from '../api/sendFileApi'; +import { SendTextApi } from '../api/sendTextApi'; + +export class SendAccessResponse extends BaseResponse { + id: string; + type: SendType; + name: string; + file: SendFileApi; + text: SendTextApi; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty('Id'); + this.type = this.getResponseProperty('Type'); + this.name = this.getResponseProperty('Name'); + + const text = this.getResponseProperty('Text'); + if (text != null) { + this.text = new SendTextApi(text); + } + + const file = this.getResponseProperty('File'); + if (file != null) { + this.file = new SendFileApi(file); + } + } +} diff --git a/src/models/response/sendResponse.ts b/src/models/response/sendResponse.ts new file mode 100644 index 0000000000..039efee4fe --- /dev/null +++ b/src/models/response/sendResponse.ts @@ -0,0 +1,51 @@ +import { BaseResponse } from './baseResponse'; + +import { SendType } from '../../enums/sendType'; + +import { SendFileApi } from '../api/sendFileApi'; +import { SendTextApi } from '../api/sendTextApi'; + +export class SendResponse extends BaseResponse { + id: string; + accessId: string; + type: SendType; + name: string; + notes: string; + file: SendFileApi; + text: SendTextApi; + key: string; + maxAccessCount?: number; + accessCount: number; + revisionDate: string; + expirationDate: string; + deletionDate: string; + password: string; + disable: boolean; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty('Id'); + this.accessId = this.getResponseProperty('AccessId'); + this.type = this.getResponseProperty('Type'); + this.name = this.getResponseProperty('Name'); + this.notes = this.getResponseProperty('Notes'); + this.key = this.getResponseProperty('Key'); + this.maxAccessCount = this.getResponseProperty('MaxAccessCount'); + this.accessCount = this.getResponseProperty('AccessCount'); + this.revisionDate = this.getResponseProperty('RevisionDate'); + this.expirationDate = this.getResponseProperty('ExpirationDate'); + this.deletionDate = this.getResponseProperty('DeletionDate'); + this.password = this.getResponseProperty('Password'); + this.disable = this.getResponseProperty('Disabled') || false; + + const text = this.getResponseProperty('Text'); + if (text != null) { + this.text = new SendTextApi(text); + } + + const file = this.getResponseProperty('File'); + if (file != null) { + this.file = new SendFileApi(file); + } + } +} diff --git a/src/models/view/sendAccessView.ts b/src/models/view/sendAccessView.ts new file mode 100644 index 0000000000..03bc67f10c --- /dev/null +++ b/src/models/view/sendAccessView.ts @@ -0,0 +1,24 @@ +import { SendType } from '../../enums/sendType'; + +import { SendAccess } from '../domain/sendAccess'; + +import { SendFileView } from './sendFileView'; +import { SendTextView } from './sendTextView'; +import { View } from './view'; + +export class SendAccessView implements View { + id: string = null; + name: string = null; + type: SendType = null; + text = new SendTextView(); + file = new SendFileView(); + + constructor(s?: SendAccess) { + if (!s) { + return; + } + + this.id = s.id; + this.type = s.type; + } +} diff --git a/src/models/view/sendFileView.ts b/src/models/view/sendFileView.ts new file mode 100644 index 0000000000..07ddb8c857 --- /dev/null +++ b/src/models/view/sendFileView.ts @@ -0,0 +1,31 @@ +import { View } from './view'; + +import { SendFile } from '../domain/sendFile'; + +export class SendFileView implements View { + id: string = null; + url: string = null; + size: string = null; + sizeName: string = null; + fileName: string = null; + + constructor(f?: SendFile) { + if (!f) { + return; + } + + this.id = f.id; + this.url = f.url; + this.size = f.size; + this.sizeName = f.sizeName; + } + + get fileSize(): number { + try { + if (this.size != null) { + return parseInt(this.size, null); + } + } catch { } + return 0; + } +} diff --git a/src/models/view/sendTextView.ts b/src/models/view/sendTextView.ts new file mode 100644 index 0000000000..b6ec45ee85 --- /dev/null +++ b/src/models/view/sendTextView.ts @@ -0,0 +1,20 @@ +import { View } from './view'; + +import { SendText } from '../domain/sendText'; + +export class SendTextView implements View { + text: string = null; + hidden: boolean; + + constructor(t?: SendText) { + if (!t) { + return; + } + + this.hidden = t.hidden; + } + + get maskedText(): string { + return this.text != null ? '••••••••' : null; + } +} diff --git a/src/models/view/sendView.ts b/src/models/view/sendView.ts new file mode 100644 index 0000000000..61c565dcf0 --- /dev/null +++ b/src/models/view/sendView.ts @@ -0,0 +1,49 @@ +import { SendType } from '../../enums/sendType'; +import { Utils } from '../../misc/utils'; + +import { Send } from '../domain/send'; +import { SymmetricCryptoKey } from '../domain/symmetricCryptoKey'; + +import { SendFileView } from './sendFileView'; +import { SendTextView } from './sendTextView'; +import { View } from './view'; + +export class SendView implements View { + id: string = null; + accessId: string = null; + name: string = null; + notes: string = null; + key: ArrayBuffer; + cryptoKey: SymmetricCryptoKey; + type: SendType = null; + text = new SendTextView(); + file = new SendFileView(); + maxAccessCount?: number = null; + accessCount: number = 0; + revisionDate: Date = null; + deletionDate: Date = null; + expirationDate: Date = null; + password: string = null; + disabled: boolean = false; + + constructor(s?: Send) { + if (!s) { + return; + } + + this.id = s.id; + this.accessId = s.accessId; + this.type = s.type; + this.maxAccessCount = s.maxAccessCount; + this.accessCount = s.accessCount; + this.revisionDate = s.revisionDate; + this.deletionDate = s.deletionDate; + this.expirationDate = s.expirationDate; + this.disabled = s.disabled; + this.password = s.password; + } + + get urlB64Key(): string { + return Utils.fromBufferToUrlB64(this.key); + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index a7ba94045e..e5656ab602 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -46,6 +46,8 @@ import { PreloginRequest } from '../models/request/preloginRequest'; import { RegisterRequest } from '../models/request/registerRequest'; import { SeatRequest } from '../models/request/seatRequest'; import { SelectionReadOnlyRequest } from '../models/request/selectionReadOnlyRequest'; +import { SendAccessRequest } from '../models/request/sendAccessRequest'; +import { SendRequest } from '../models/request/sendRequest'; import { SetPasswordRequest } from '../models/request/setPasswordRequest'; import { StorageRequest } from '../models/request/storageRequest'; import { TaxInfoUpdateRequest } from '../models/request/taxInfoUpdateRequest'; @@ -97,6 +99,8 @@ import { PolicyResponse } from '../models/response/policyResponse'; import { PreloginResponse } from '../models/response/preloginResponse'; import { ProfileResponse } from '../models/response/profileResponse'; import { SelectionReadOnlyResponse } from '../models/response/selectionReadOnlyResponse'; +import { SendAccessResponse } from '../models/response/sendAccessResponse'; +import { SendResponse } from '../models/response/sendResponse'; import { SubscriptionResponse } from '../models/response/subscriptionResponse'; import { SyncResponse } from '../models/response/syncResponse'; import { TaxInfoResponse } from '../models/response/taxInfoResponse'; @@ -377,6 +381,47 @@ export class ApiService implements ApiServiceAbstraction { return this.send('DELETE', '/folders/' + id, null, true, false); } + // Send APIs + + async getSend(id: string): Promise { + const r = await this.send('GET', '/sends/' + id, null, true, true); + return new SendResponse(r); + } + + async postSendAccess(id: string, request: SendAccessRequest): Promise { + const r = await this.send('POST', '/sends/access/' + id, request, false, true); + return new SendAccessResponse(r); + } + + async getSends(): Promise> { + const r = await this.send('GET', '/sends', null, true, true); + return new ListResponse(r, SendResponse); + } + + async postSend(request: SendRequest): Promise { + const r = await this.send('POST', '/sends', request, true, true); + return new SendResponse(r); + } + + async postSendFile(data: FormData): Promise { + const r = await this.send('POST', '/sends/file', data, true, true); + return new SendResponse(r); + } + + async putSend(id: string, request: SendRequest): Promise { + const r = await this.send('PUT', '/sends/' + id, request, true, true); + return new SendResponse(r); + } + + async putSendRemovePassword(id: string): Promise { + const r = await this.send('PUT', '/sends/' + id + '/remove-password', null, true, true); + return new SendResponse(r); + } + + deleteSend(id: string): Promise { + return this.send('DELETE', '/sends/' + id, null, true, false); + } + // Cipher APIs async getCipher(id: string): Promise { @@ -1040,7 +1085,6 @@ export class ApiService implements ApiServiceAbstraction { } async preValidateSso(identifier: string): Promise { - if (identifier == null || identifier === '') { throw new Error('Organization Identifier was not provided.'); } @@ -1063,7 +1107,7 @@ export class ApiService implements ApiServiceAbstraction { if (response.status === 200) { return true; } else { - const error = await this.handleError(response, false); + const error = await this.handleError(response, false, true); return Promise.reject(error); } } @@ -1111,13 +1155,13 @@ export class ApiService implements ApiServiceAbstraction { const responseJson = await response.json(); return responseJson; } else if (response.status !== 200) { - const error = await this.handleError(response, false); + const error = await this.handleError(response, false, authed); return Promise.reject(error); } } - private async handleError(response: Response, tokenError: boolean): Promise { - if ((tokenError && response.status === 400) || response.status === 401 || response.status === 403) { + private async handleError(response: Response, tokenError: boolean, authed: boolean): Promise { + if (authed && ((tokenError && response.status === 400) || response.status === 401 || response.status === 403)) { await this.logoutCallback(true); return null; } @@ -1163,7 +1207,7 @@ export class ApiService implements ApiServiceAbstraction { await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken); return tokenResponse; } else { - const error = await this.handleError(response, true); + const error = await this.handleError(response, true, true); return Promise.reject(error); } } diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index df44ef7789..a65402b322 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -353,6 +353,11 @@ export class CryptoService implements CryptoServiceAbstraction { return await this.stretchKey(pinKey); } + async makeSendKey(keyMaterial: ArrayBuffer): Promise { + const sendKey = await this.cryptoFunctionService.hkdf(keyMaterial, 'bitwarden-send', 'send', 64, 'sha256'); + return new SymmetricCryptoKey(sendKey); + } + async hashPassword(password: string, key: SymmetricCryptoKey): Promise { if (key == null) { key = await this.getKey(); From 6e89c04f3ed2a975091eabecd828527cac975626 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Tue, 3 Nov 2020 14:36:19 -0500 Subject: [PATCH 1093/1626] Added missing member, showValue to Field (#195) --- src/models/view/fieldView.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/models/view/fieldView.ts b/src/models/view/fieldView.ts index c839e1ecb6..66c36744ac 100644 --- a/src/models/view/fieldView.ts +++ b/src/models/view/fieldView.ts @@ -9,6 +9,7 @@ export class FieldView implements View { value: string = null; type: FieldType = null; newField: boolean = false; // Marks if the field is new and hasn't been saved + showValue: boolean = false; constructor(f?: Field) { if (!f) { From 9aa3cbf73d9df9a2641654270911359593bcb5c5 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Wed, 4 Nov 2020 12:21:29 -0500 Subject: [PATCH 1094/1626] Turned off background throttling for browserWindow (#196) --- src/electron/window.main.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index bf4d28dbbb..e650ad38d6 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -114,6 +114,7 @@ export class WindowMain { webPreferences: { nodeIntegration: true, webviewTag: true, + backgroundThrottling: false, }, }); From 79b856cb6e73f126a263a0e4a61d0161828a40dd Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Tue, 10 Nov 2020 15:15:40 -0500 Subject: [PATCH 1095/1626] Implement User-based API Keys (#197) * Added support for authenticating with an API key * added api service methods for user api keys * fixed a copy/pasted api endpoint url * Let toIdentityToken() use a a prestored client_id in place of the application client_id if one exists * Allowed for api key auth in the cli * Removed some commented out code commited for apiKey auth * Cleanup for ApiKey auth in the CLI * Removed cli prefix from client_crendential auth types * Removed ClientPrefix conditional from decoded token getters * Update src/services/api.service.ts Co-authored-by: Kyle Spearrin * formatting * changed command from login --apiKey to login --apikey Co-authored-by: Kyle Spearrin --- src/abstractions/api.service.ts | 2 + src/abstractions/auth.service.ts | 6 ++ .../components/two-factor.component.ts | 14 +++-- src/cli/commands/login.command.ts | 43 ++++++++++++-- src/models/request/tokenRequest.ts | 13 ++++- src/services/api.service.ts | 12 +++- src/services/auth.service.ts | 58 ++++++++++++++----- 7 files changed, 124 insertions(+), 24 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 189b4c9c7c..8d0e5652db 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -153,6 +153,8 @@ export abstract class ApiService { postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise; postAccountKdf: (request: KdfRequest) => Promise; getEnterprisePortalSignInToken: () => Promise; + postUserApiKey: (id: string, request: PasswordVerificationRequest) => Promise; + postUserRotateApiKey: (id: string, request: PasswordVerificationRequest) => Promise; getFolder: (id: string) => Promise; postFolder: (request: FolderRequest) => Promise; diff --git a/src/abstractions/auth.service.ts b/src/abstractions/auth.service.ts index 7a7b23f73a..d5041720ce 100644 --- a/src/abstractions/auth.service.ts +++ b/src/abstractions/auth.service.ts @@ -9,21 +9,27 @@ export abstract class AuthService { code: string; codeVerifier: string; ssoRedirectUrl: string; + clientId: string; + clientSecret: string; twoFactorProvidersData: Map; selectedTwoFactorProviderType: TwoFactorProviderType; logIn: (email: string, masterPassword: string) => Promise; logInSso: (code: string, codeVerifier: string, redirectUrl: string) => Promise; + logInApiKey: (clientId: string, clientSecret: string) => Promise; logInTwoFactor: (twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean) => Promise; logInComplete: (email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean) => Promise; logInSsoComplete: (code: string, codeVerifier: string, redirectUrl: string, twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean) => Promise; + logInApiKeyComplete: (clientId: string, clientSecret: string, twoFactorProvider: TwoFactorProviderType, + twoFactorToken: string, remember?: boolean) => Promise; logOut: (callback: Function) => void; getSupportedTwoFactorProviders: (win: Window) => any[]; getDefaultTwoFactorProvider: (u2fSupported: boolean) => TwoFactorProviderType; makePreloginKey: (masterPassword: string, email: string) => Promise; + authingWithApiKey: () => boolean; authingWithSso: () => boolean; authingWithPassword: () => boolean; } diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 4fe4e0c4b1..709ce2d4db 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -8,7 +8,6 @@ import { Router, } from '@angular/router'; -import { DeviceType } from '../../enums/deviceType'; import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; import { TwoFactorEmailRequest } from '../../models/request/twoFactorEmailRequest'; @@ -59,8 +58,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { } async ngOnInit() { - if ((!this.authService.authingWithSso() && !this.authService.authingWithPassword()) || - this.authService.twoFactorProvidersData == null) { + if (!this.authing || this.authService.twoFactorProvidersData == null) { this.router.navigate([this.loginRoute]); return; } @@ -75,7 +73,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { } }); - if (this.authService.authingWithSso()) { + if (this.needsLock) { this.successRoute = 'lock'; } @@ -246,4 +244,12 @@ export class TwoFactorComponent implements OnInit, OnDestroy { this.u2f.cleanup(); } } + + get authing(): boolean { + return this.authService.authingWithPassword() || this.authService.authingWithSso() || this.authService.authingWithApiKey() + } + + get needsLock(): boolean { + return this.authService.authingWithSso() || this.authService.authingWithApiKey(); + } } diff --git a/src/cli/commands/login.command.ts b/src/cli/commands/login.command.ts index 26d401df8e..396e37d2f2 100644 --- a/src/cli/commands/login.command.ts +++ b/src/cli/commands/login.command.ts @@ -46,7 +46,38 @@ export class LoginCommand { let ssoCodeVerifier: string = null; let ssoCode: string = null; - if (cmd.sso != null && this.canInteract) { + + let clientId: string = null; + let clientSecret: string = null; + + if (cmd.apikey != null) { + const storedClientId: string = process.env.BW_CLIENTID; + const storedClientSecret: string = process.env.BW_CLIENTSECRET; + if (storedClientId == null) { + if (this.canInteract) { + const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ + type: 'input', + name: 'clientId', + message: 'client_id:', + }); + clientId = answer.clientId; + } else { + clientId = null; + } + } else { + clientId = storedClientId; + } + if (this.canInteract && storedClientSecret == null) { + const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ + type: 'input', + name: 'clientSecret', + message: 'client_secret:', + }); + clientSecret = answer.clientSecret; + } else { + clientSecret = storedClientSecret; + } + } else if (cmd.sso != null && this.canInteract) { const passwordOptions: any = { type: 'password', length: 64, @@ -117,7 +148,10 @@ export class LoginCommand { let response: AuthResult = null; if (twoFactorToken != null && twoFactorMethod != null) { - if (ssoCode != null && ssoCodeVerifier != null) { + if (clientId != null && clientSecret != null) { + response = await this.authService.logInApiKeyComplete(clientId, clientSecret, twoFactorMethod, + twoFactorToken, false); + } else if (ssoCode != null && ssoCodeVerifier != null) { response = await this.authService.logInSsoComplete(ssoCode, ssoCodeVerifier, this.ssoRedirectUri, twoFactorMethod, twoFactorToken, false); } else { @@ -125,9 +159,10 @@ export class LoginCommand { twoFactorToken, false); } } else { - if (ssoCode != null && ssoCodeVerifier != null) { + if (clientId != null && clientSecret != null) { + response = await this.authService.logInApiKey(clientId, clientSecret); + } else if (ssoCode != null && ssoCodeVerifier != null) { response = await this.authService.logInSso(ssoCode, ssoCodeVerifier, this.ssoRedirectUri); - } else { response = await this.authService.logIn(email, password); } diff --git a/src/models/request/tokenRequest.ts b/src/models/request/tokenRequest.ts index f0ff702428..a3db628670 100644 --- a/src/models/request/tokenRequest.ts +++ b/src/models/request/tokenRequest.ts @@ -8,12 +8,14 @@ export class TokenRequest { code: string; codeVerifier: string; redirectUri: string; + clientId: string; + clientSecret: string; token: string; provider: TwoFactorProviderType; remember: boolean; device?: DeviceRequest; - constructor(credentials: string[], codes: string[], provider: TwoFactorProviderType, + constructor(credentials: string[], codes: string[], clientIdClientSecret: string[], provider: TwoFactorProviderType, token: string, remember: boolean, device?: DeviceRequest) { if (credentials != null && credentials.length > 1) { this.email = credentials[0]; @@ -22,6 +24,9 @@ export class TokenRequest { this.code = codes[0]; this.codeVerifier = codes[1]; this.redirectUri = codes[2]; + } else if (clientIdClientSecret != null && clientIdClientSecret.length > 1) { + this.clientId = clientIdClientSecret[0] + this.clientSecret = clientIdClientSecret[1] } this.token = token; this.provider = provider; @@ -35,7 +40,11 @@ export class TokenRequest { client_id: clientId, }; - if (this.masterPasswordHash != null && this.email != null) { + if (this.clientSecret != null) { + obj.scope = 'api'; + obj.grant_type = 'client_credentials'; + obj.client_secret = this.clientSecret; + } else if (this.masterPasswordHash != null && this.email != null) { obj.grant_type = 'password'; obj.username = this.email; obj.password = this.masterPasswordHash; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index e5656ab602..cfcdddb573 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -179,7 +179,7 @@ export class ApiService implements ApiServiceAbstraction { headers.set('User-Agent', this.customUserAgent); } const response = await this.fetch(new Request(this.identityBaseUrl + '/connect/token', { - body: this.qsStringify(request.toIdentityToken(this.platformUtilsService.identityClientId)), + body: this.qsStringify(request.toIdentityToken(request.clientId ?? this.platformUtilsService.identityClientId)), credentials: this.getCredentials(), cache: 'no-store', headers: headers, @@ -360,6 +360,16 @@ export class ApiService implements ApiServiceAbstraction { return this.send('GET', '/accounts/sso/user-identifier', null, true, true); } + async postUserApiKey(id: string, request: PasswordVerificationRequest): Promise { + const r = await this.send('POST', '/accounts/api-key', request, true, true); + return new ApiKeyResponse(r); + } + + async postUserRotateApiKey(id: string, request: PasswordVerificationRequest): Promise { + const r = await this.send('POST', '/accounts/rotate-api-key', request, true, true); + return new ApiKeyResponse(r); + } + // Folder APIs async getFolder(id: string): Promise { diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 5633d66dda..d8ec84277d 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -9,7 +9,6 @@ import { KeysRequest } from '../models/request/keysRequest'; import { PreloginRequest } from '../models/request/preloginRequest'; import { TokenRequest } from '../models/request/tokenRequest'; -import { ErrorResponse } from '../models/response/errorResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; @@ -81,6 +80,8 @@ export class AuthService implements AuthServiceAbstraction { code: string; codeVerifier: string; ssoRedirectUrl: string; + clientId: string; + clientSecret: string; twoFactorProvidersData: Map; selectedTwoFactorProviderType: TwoFactorProviderType = null; @@ -118,19 +119,27 @@ export class AuthService implements AuthServiceAbstraction { this.selectedTwoFactorProviderType = null; const key = await this.makePreloginKey(masterPassword, email); const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); - return await this.logInHelper(email, hashedPassword, null, null, null, key, - null, null, null); + return await this.logInHelper(email, hashedPassword, null, null, null, null, null, + key, null, null, null); } async logInSso(code: string, codeVerifier: string, redirectUrl: string): Promise { this.selectedTwoFactorProviderType = null; - return await this.logInHelper(null, null, code, codeVerifier, redirectUrl, null, null, null, null); + return await this.logInHelper(null, null, code, codeVerifier, redirectUrl, null, null, + null, null, null, null); + } + + async logInApiKey(clientId: string, clientSecret: string): Promise { + this.selectedTwoFactorProviderType = null; + return await this.logInHelper(null, null, null, null, null, clientId, clientSecret, + null, null, null, null); } async logInTwoFactor(twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean): Promise { return await this.logInHelper(this.email, this.masterPasswordHash, this.code, this.codeVerifier, - this.ssoRedirectUrl, this.key, twoFactorProvider, twoFactorToken, remember); + this.ssoRedirectUrl, this.clientId, this.clientSecret, this.key, twoFactorProvider, + twoFactorToken, remember); } async logInComplete(email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType, @@ -138,14 +147,21 @@ export class AuthService implements AuthServiceAbstraction { this.selectedTwoFactorProviderType = null; const key = await this.makePreloginKey(masterPassword, email); const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); - return await this.logInHelper(email, hashedPassword, null, null, null, key, twoFactorProvider, twoFactorToken, - remember); + return await this.logInHelper(email, hashedPassword, null, null, null, null, null, key, + twoFactorProvider, twoFactorToken, remember); } async logInSsoComplete(code: string, codeVerifier: string, redirectUrl: string, twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean): Promise { this.selectedTwoFactorProviderType = null; return await this.logInHelper(null, null, code, codeVerifier, redirectUrl, null, + null, null, twoFactorProvider, twoFactorToken, remember); + } + + async logInApiKeyComplete(clientId: string, clientSecret: string, twoFactorProvider: TwoFactorProviderType, + twoFactorToken: string, remember?: boolean): Promise { + this.selectedTwoFactorProviderType = null; + return await this.logInHelper(null, null, null, null, null, clientId, clientSecret, null, twoFactorProvider, twoFactorToken, remember); } @@ -233,6 +249,10 @@ export class AuthService implements AuthServiceAbstraction { return this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations); } + authingWithApiKey(): boolean { + return this.clientId != null && this.clientSecret != null; + } + authingWithSso(): boolean { return this.code != null && this.codeVerifier != null && this.ssoRedirectUrl != null; } @@ -242,14 +262,16 @@ export class AuthService implements AuthServiceAbstraction { } private async logInHelper(email: string, hashedPassword: string, code: string, codeVerifier: string, - redirectUrl: string, key: SymmetricCryptoKey, twoFactorProvider?: TwoFactorProviderType, - twoFactorToken?: string, remember?: boolean): Promise { + redirectUrl: string, clientId: string, clientSecret: string, key: SymmetricCryptoKey, + twoFactorProvider?: TwoFactorProviderType, twoFactorToken?: string, remember?: boolean): Promise { const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email); const appId = await this.appIdService.getAppId(); const deviceRequest = new DeviceRequest(appId, this.platformUtilsService); let emailPassword: string[] = []; let codeCodeVerifier: string[] = []; + let clientIdClientSecret: string[] = []; + if (email != null && hashedPassword != null) { emailPassword = [email, hashedPassword]; } else { @@ -260,16 +282,22 @@ export class AuthService implements AuthServiceAbstraction { } else { codeCodeVerifier = null; } + if (clientId != null && clientSecret != null) { + clientIdClientSecret = [clientId, clientSecret] + } else { + clientIdClientSecret = null; + } let request: TokenRequest; if (twoFactorToken != null && twoFactorProvider != null) { - request = new TokenRequest(emailPassword, codeCodeVerifier, twoFactorProvider, twoFactorToken, remember, - deviceRequest); + request = new TokenRequest(emailPassword, codeCodeVerifier, clientIdClientSecret, twoFactorProvider, + twoFactorToken, remember, deviceRequest); } else if (storedTwoFactorToken != null) { - request = new TokenRequest(emailPassword, codeCodeVerifier, TwoFactorProviderType.Remember, + request = new TokenRequest(emailPassword, codeCodeVerifier, clientIdClientSecret, TwoFactorProviderType.Remember, storedTwoFactorToken, false, deviceRequest); } else { - request = new TokenRequest(emailPassword, codeCodeVerifier, null, null, false, deviceRequest); + request = new TokenRequest(emailPassword, codeCodeVerifier, clientIdClientSecret, null, + null, false, deviceRequest); } const response = await this.apiService.postIdentityToken(request); @@ -286,6 +314,8 @@ export class AuthService implements AuthServiceAbstraction { this.code = code; this.codeVerifier = codeVerifier; this.ssoRedirectUrl = redirectUrl; + this.clientId = clientId; + this.clientSecret = clientSecret; this.key = this.setCryptoKeys ? key : null; this.twoFactorProvidersData = twoFactorResponse.twoFactorProviders2; result.twoFactorProviders = twoFactorResponse.twoFactorProviders2; @@ -343,6 +373,8 @@ export class AuthService implements AuthServiceAbstraction { this.code = null; this.codeVerifier = null; this.ssoRedirectUrl = null; + this.clientId = null; + this.clientSecret = null; this.twoFactorProvidersData = null; this.selectedTwoFactorProviderType = null; } From 40214e80a659f189b975eef88b48f8907f1de0e2 Mon Sep 17 00:00:00 2001 From: eliykat <31796059+eliykat@users.noreply.github.com> Date: Fri, 13 Nov 2020 00:37:46 +1000 Subject: [PATCH 1096/1626] expand contributing guide (#200) --- CONTRIBUTING.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ec7f6e2f9a..8acaca969e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,30 @@ -Code contributions are welcome! Please commit any pull requests against the `master` branch. +# How to Contribute + +Contributions of all kinds are welcome! + +Please visit our [Community Forums](https://community.bitwarden.com/) for general community discussion and the development roadmap. + +Here is how you can get involved: + +* **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one + +* **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code + +* **Report a bug or submit a bugfix:** Use Github issues and pull requests + +* **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help) + +* **Help other users:** Go to the [User-to-User Support category](https://community.bitwarden.com/c/support/) on the Community Forums + +## Contributor Agreement + +Please sign the [Contributor Agreement](https://cla-assistant.io/bitwarden/jslib) if you intend on contributing to any Github repository. Pull requests cannot be accepted and merged unless the author has signed the Contributor Agreement. + +## Pull Request Guidelines + +* use `npm run lint` and fix any linting suggestions before submitting a pull request +* commit any pull requests against the `master` branch +* include a link to your Community Forums post # Setting up your Local Dev environment for jslib In order to easily test, check and develop against local changes to jslib across each of the TypeScript/JavaScript clients it is recommended to use symlinks for the submodule so that you only have to make the change once and don't need to x-copy or wait for a commit+merge to checkout, pull and test against your other repos. From 37682e7146bd718bcb0ebbf63067641186176501 Mon Sep 17 00:00:00 2001 From: Marcos Soutullo Rodriguez Date: Fri, 13 Nov 2020 19:06:30 +0000 Subject: [PATCH 1097/1626] Bump https-proxy-agent dep. to 5.0.0 (#201) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c7d2532704..c62c9e61bc 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "electron-store": "1.3.0", "electron-updater": "4.3.5", "form-data": "2.3.2", - "https-proxy-agent": "4.0.0", + "https-proxy-agent": "5.0.0", "inquirer": "6.2.0", "jsdom": "13.2.0", "keytar": "4.13.0", From 85faee21eec7602eecc3a5368be4c8fb7bab1e67 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Fri, 13 Nov 2020 15:07:34 -0500 Subject: [PATCH 1098/1626] Fixed import for HttpsProxyAgent (#202) * Fixed import for HttpsProxyAgent * Try this fix again? * One more effort, otherwise need to revert * Syntax error * Lint error fix * revert https-proxy-agent version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c62c9e61bc..c7d2532704 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "electron-store": "1.3.0", "electron-updater": "4.3.5", "form-data": "2.3.2", - "https-proxy-agent": "5.0.0", + "https-proxy-agent": "4.0.0", "inquirer": "6.2.0", "jsdom": "13.2.0", "keytar": "4.13.0", From 6563dccf3b444c6f2ea97efe913eaccf9d9fd502 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 18 Nov 2020 13:56:41 -0500 Subject: [PATCH 1099/1626] send service and syncing send data (#205) * send service and syncing send data * Update send.service.ts --- src/abstractions/send.service.ts | 22 +++ src/models/response/syncResponse.ts | 7 + src/services/send.service.ts | 236 ++++++++++++++++++++++++++++ src/services/sync.service.ts | 14 +- 4 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 src/abstractions/send.service.ts create mode 100644 src/services/send.service.ts diff --git a/src/abstractions/send.service.ts b/src/abstractions/send.service.ts new file mode 100644 index 0000000000..03862de6bc --- /dev/null +++ b/src/abstractions/send.service.ts @@ -0,0 +1,22 @@ +import { SendData } from '../models/data/sendData'; + +import { Send } from '../models/domain/send'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; + +import { SendView } from '../models/view/sendView'; + +export abstract class SendService { + decryptedSendCache: SendView[]; + + clearCache: () => void; + encrypt: (model: SendView, file: File, password: string, key?: SymmetricCryptoKey) => Promise<[Send, ArrayBuffer]>; + get: (id: string) => Promise; + getAll: () => Promise; + getAllDecrypted: () => Promise; + saveWithServer: (sendData: [Send, ArrayBuffer]) => Promise; + upsert: (send: SendData | SendData[]) => Promise; + replace: (sends: { [id: string]: SendData; }) => Promise; + clear: (userId: string) => Promise; + delete: (id: string | string[]) => Promise; + deleteWithServer: (id: string) => Promise; +} diff --git a/src/models/response/syncResponse.ts b/src/models/response/syncResponse.ts index dafa6de554..1fb655a3b4 100644 --- a/src/models/response/syncResponse.ts +++ b/src/models/response/syncResponse.ts @@ -5,6 +5,7 @@ import { DomainsResponse } from './domainsResponse'; import { FolderResponse } from './folderResponse'; import { PolicyResponse } from './policyResponse'; import { ProfileResponse } from './profileResponse'; +import { SendResponse } from './sendResponse'; export class SyncResponse extends BaseResponse { profile?: ProfileResponse; @@ -13,6 +14,7 @@ export class SyncResponse extends BaseResponse { ciphers: CipherResponse[] = []; domains?: DomainsResponse; policies?: PolicyResponse[] = []; + sends: SendResponse[] = []; constructor(response: any) { super(response); @@ -46,5 +48,10 @@ export class SyncResponse extends BaseResponse { if (policies != null) { this.policies = policies.map((p: any) => new PolicyResponse(p)); } + + const sends = this.getResponseProperty('Sends'); + if (sends != null) { + this.sends = sends.map((s: any) => new SendResponse(s)); + } } } diff --git a/src/services/send.service.ts b/src/services/send.service.ts new file mode 100644 index 0000000000..06b866efd5 --- /dev/null +++ b/src/services/send.service.ts @@ -0,0 +1,236 @@ +import { SendData } from '../models/data/sendData'; + +import { SendRequest } from '../models/request/sendRequest'; + +import { SendResponse } from '../models/response/sendResponse'; + +import { Send } from '../models/domain/send'; +import { SendFile } from '../models/domain/sendFile'; +import { SendText } from '../models/domain/sendText'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; + +import { SendType } from '../enums/sendType'; + +import { SendView } from '../models/view/sendView'; + +import { ApiService } from '../abstractions/api.service'; +import { CryptoService } from '../abstractions/crypto.service'; +import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; +import { I18nService } from '../abstractions/i18n.service'; +import { SendService as SendServiceAbstraction } from '../abstractions/send.service'; +import { StorageService } from '../abstractions/storage.service'; +import { UserService } from '../abstractions/user.service'; + +import { Utils } from '../misc/utils'; + +const Keys = { + sendsPrefix: 'sends_', +}; + +export class SendService implements SendServiceAbstraction { + decryptedSendCache: SendView[]; + + constructor(private cryptoService: CryptoService, private userService: UserService, + private apiService: ApiService, private storageService: StorageService, + private i18nService: I18nService, private cryptoFunctionService: CryptoFunctionService) { } + + clearCache(): void { + this.decryptedSendCache = null; + } + + async encrypt(model: SendView, file: File, password: string, + key?: SymmetricCryptoKey): Promise<[Send, ArrayBuffer]> { + let fileData: ArrayBuffer = null; + const send = new Send(); + send.id = model.id; + send.type = model.type; + send.disabled = model.disabled; + send.maxAccessCount = model.maxAccessCount; + if (model.key == null) { + model.key = await this.cryptoFunctionService.randomBytes(16); + model.cryptoKey = await this.cryptoService.makeSendKey(model.key); + } + if (password != null) { + const passwordHash = await this.cryptoFunctionService.pbkdf2(password, model.key, 'sha256', 100000); + send.password = Utils.fromBufferToB64(passwordHash); + } + send.key = await this.cryptoService.encrypt(model.key, key); + send.name = await this.cryptoService.encrypt(model.name, model.cryptoKey); + send.notes = await this.cryptoService.encrypt(model.notes, model.cryptoKey); + if (send.type === SendType.Text) { + send.text = new SendText(); + send.text.text = await this.cryptoService.encrypt(model.text.text, model.cryptoKey); + send.text.hidden = model.text.hidden; + } else if (send.type === SendType.File) { + send.file = new SendFile(); + if (file != null) { + fileData = await this.parseFile(send, file, model.cryptoKey); + } + } + + return [send, fileData]; + } + + async get(id: string): Promise { + const userId = await this.userService.getUserId(); + const sends = await this.storageService.get<{ [id: string]: SendData; }>( + Keys.sendsPrefix + userId); + if (sends == null || !sends.hasOwnProperty(id)) { + return null; + } + + return new Send(sends[id]); + } + + async getAll(): Promise { + const userId = await this.userService.getUserId(); + const sends = await this.storageService.get<{ [id: string]: SendData; }>( + Keys.sendsPrefix + userId); + const response: Send[] = []; + for (const id in sends) { + if (sends.hasOwnProperty(id)) { + response.push(new Send(sends[id])); + } + } + return response; + } + + async getAllDecrypted(): Promise { + if (this.decryptedSendCache != null) { + return this.decryptedSendCache; + } + + const hasKey = await this.cryptoService.hasKey(); + if (!hasKey) { + throw new Error('No key.'); + } + + const decSends: SendView[] = []; + const promises: Promise[] = []; + const sends = await this.getAll(); + sends.forEach((send) => { + promises.push(send.decrypt().then((f) => decSends.push(f))); + }); + + await Promise.all(promises); + decSends.sort(Utils.getSortFunction(this.i18nService, 'name')); + + this.decryptedSendCache = decSends; + return this.decryptedSendCache; + } + + async saveWithServer(sendData: [Send, ArrayBuffer]): Promise { + const request = new SendRequest(sendData[0]); + let response: SendResponse; + if (sendData[0].id == null) { + if (sendData[0].type === SendType.Text) { + response = await this.apiService.postSend(request); + } else { + const fd = new FormData(); + try { + const blob = new Blob([sendData[1]], { type: 'application/octet-stream' }); + fd.append('model', JSON.stringify(request)); + fd.append('data', blob, sendData[0].file.fileName.encryptedString); + } catch (e) { + if (Utils.isNode && !Utils.isBrowser) { + fd.append('model', JSON.stringify(request)); + fd.append('data', Buffer.from(sendData[1]) as any, { + filepath: sendData[0].file.fileName.encryptedString, + contentType: 'application/octet-stream', + } as any); + } else { + throw e; + } + } + response = await this.apiService.postSendFile(fd); + } + sendData[0].id = response.id; + } else { + response = await this.apiService.putSend(sendData[0].id, request); + } + + const userId = await this.userService.getUserId(); + const data = new SendData(response, userId); + await this.upsert(data); + + } + + async upsert(send: SendData | SendData[]): Promise { + const userId = await this.userService.getUserId(); + let sends = await this.storageService.get<{ [id: string]: SendData; }>( + Keys.sendsPrefix + userId); + if (sends == null) { + sends = {}; + } + + if (send instanceof SendData) { + const s = send as SendData; + sends[s.id] = s; + } else { + (send as SendData[]).forEach((s) => { + sends[s.id] = s; + }); + } + + await this.storageService.save(Keys.sendsPrefix + userId, sends); + this.decryptedSendCache = null; + } + + async replace(sends: { [id: string]: SendData; }): Promise { + const userId = await this.userService.getUserId(); + await this.storageService.save(Keys.sendsPrefix + userId, sends); + this.decryptedSendCache = null; + } + + async clear(userId: string): Promise { + await this.storageService.remove(Keys.sendsPrefix + userId); + this.decryptedSendCache = null; + } + + async delete(id: string | string[]): Promise { + const userId = await this.userService.getUserId(); + const sends = await this.storageService.get<{ [id: string]: SendData; }>( + Keys.sendsPrefix + userId); + if (sends == null) { + return; + } + + if (typeof id === 'string') { + if (sends[id] == null) { + return; + } + delete sends[id]; + } else { + (id as string[]).forEach((i) => { + delete sends[i]; + }); + } + + await this.storageService.save(Keys.sendsPrefix + userId, sends); + this.decryptedSendCache = null; + } + + async deleteWithServer(id: string): Promise { + await this.apiService.deleteSend(id); + await this.delete(id); + } + + private parseFile(send: Send, file: File, key: SymmetricCryptoKey): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsArrayBuffer(file); + reader.onload = async (evt) => { + try { + send.file.fileName = await this.cryptoService.encrypt(file.name, key); + const fileData = await this.cryptoService.encryptToBytes(evt.target.result as ArrayBuffer, key); + resolve(fileData); + } catch (e) { + reject(e); + } + }; + reader.onerror = (evt) => { + reject('Error reading file.'); + }; + }); + } +} diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index c97968384f..f60df18c90 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -5,6 +5,7 @@ import { CryptoService } from '../abstractions/crypto.service'; import { FolderService } from '../abstractions/folder.service'; import { MessagingService } from '../abstractions/messaging.service'; import { PolicyService } from '../abstractions/policy.service'; +import { SendService } from '../abstractions/send.service'; import { SettingsService } from '../abstractions/settings.service'; import { StorageService } from '../abstractions/storage.service'; import { SyncService as SyncServiceAbstraction } from '../abstractions/sync.service'; @@ -15,6 +16,7 @@ import { CollectionData } from '../models/data/collectionData'; import { FolderData } from '../models/data/folderData'; import { OrganizationData } from '../models/data/organizationData'; import { PolicyData } from '../models/data/policyData'; +import { SendData } from '../models/data/sendData'; import { CipherResponse } from '../models/response/cipherResponse'; import { CollectionDetailsResponse } from '../models/response/collectionResponse'; @@ -26,6 +28,7 @@ import { } from '../models/response/notificationResponse'; import { PolicyResponse } from '../models/response/policyResponse'; import { ProfileResponse } from '../models/response/profileResponse'; +import { SendResponse } from '../models/response/sendResponse'; const Keys = { lastSyncPrefix: 'lastSync_', @@ -39,7 +42,7 @@ export class SyncService implements SyncServiceAbstraction { private cipherService: CipherService, private cryptoService: CryptoService, private collectionService: CollectionService, private storageService: StorageService, private messagingService: MessagingService, private policyService: PolicyService, - private logoutCallback: (expired: boolean) => Promise) { + private sendService: SendService, private logoutCallback: (expired: boolean) => Promise) { } async getLastSync(): Promise { @@ -94,6 +97,7 @@ export class SyncService implements SyncServiceAbstraction { await this.syncFolders(userId, response.folders); await this.syncCollections(response.collections); await this.syncCiphers(userId, response.ciphers); + await this.syncSends(userId, response.sends); await this.syncSettings(userId, response.domains); await this.syncPolicies(response.policies); @@ -287,6 +291,14 @@ export class SyncService implements SyncServiceAbstraction { return await this.cipherService.replace(ciphers); } + private async syncSends(userId: string, response: SendResponse[]) { + const sends: { [id: string]: SendData; } = {}; + response.forEach((s) => { + sends[s.id] = new SendData(s, userId); + }); + return await this.sendService.replace(sends); + } + private async syncSettings(userId: string, response: DomainsResponse) { let eqDomains: string[][] = []; if (response != null && response.equivalentDomains != null) { From f44e99d74dc011c026525d171f7d2940b60b6587 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 18 Nov 2020 15:58:12 -0500 Subject: [PATCH 1100/1626] sr lang (#206) --- src/services/i18n.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/i18n.service.ts b/src/services/i18n.service.ts index 457d0489cd..73d5f98966 100644 --- a/src/services/i18n.service.ts +++ b/src/services/i18n.service.ts @@ -41,6 +41,7 @@ export class I18nService implements I18nServiceAbstraction { ['ro', 'română'], ['ru', 'русский'], ['sk', 'slovenčina'], + ['sr', 'Српски'], ['sv', 'svenska'], ['th', 'ไทย'], ['tr', 'Türkçe'], From 9e4d000b4d02109f8ad3a870327b8dcbb12eb176 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Wed, 18 Nov 2020 22:10:57 +0100 Subject: [PATCH 1101/1626] Browser <-> desktop communication (#185) * Add electron constant for browser integration * Add constant for browser biometrics. Ensure biometry is locked on lock. * Avoid saving keys outside desktop * Fix eslint warning * Add supportsSecureStorage helper to platformUtils to improve readability --- src/abstractions/platformUtils.service.ts | 1 + src/angular/components/lock.component.ts | 3 +-- src/cli/services/cliPlatformUtils.service.ts | 4 ++++ src/electron/electronConstants.ts | 1 + src/electron/services/electronPlatformUtils.service.ts | 4 ++++ src/services/constants.service.ts | 2 ++ src/services/crypto.service.ts | 7 ++++--- src/services/vaultTimeout.service.ts | 3 ++- 8 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index f40e07504f..c717b8962b 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -34,4 +34,5 @@ export abstract class PlatformUtilsService { readFromClipboard: (options?: any) => Promise; supportsBiometric: () => Promise; authenticateBiometric: () => Promise; + supportsSecureStorage: () => boolean; } diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 423476d891..d14901e713 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -50,7 +50,7 @@ export class LockComponent implements OnInit { this.pinSet = await this.vaultTimeoutService.isPinLockSet(); this.pinLock = (this.pinSet[0] && this.vaultTimeoutService.pinProtectedKey != null) || this.pinSet[1]; this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); - this.biometricLock = await this.vaultTimeoutService.isBiometricLockSet() && await this.cryptoService.hasKey(); + this.biometricLock = await this.vaultTimeoutService.isBiometricLockSet() && (await this.cryptoService.hasKey() || !this.platformUtilsService.supportsSecureStorage()); this.biometricText = await this.storageService.get(ConstantsService.biometricText); this.email = await this.userService.getEmail(); let vaultUrl = this.environmentService.getWebVaultUrl(); @@ -158,7 +158,6 @@ export class LockComponent implements OnInit { } const success = await this.platformUtilsService.authenticateBiometric(); - this.vaultTimeoutService.biometricLocked = !success; if (success) { await this.doContinue(); } diff --git a/src/cli/services/cliPlatformUtils.service.ts b/src/cli/services/cliPlatformUtils.service.ts index f86adfeeac..934ee8a7b0 100644 --- a/src/cli/services/cliPlatformUtils.service.ts +++ b/src/cli/services/cliPlatformUtils.service.ts @@ -145,4 +145,8 @@ export class CliPlatformUtilsService implements PlatformUtilsService { authenticateBiometric(): Promise { return Promise.resolve(false); } + + supportsSecureStorage(): boolean { + return false; + } } diff --git a/src/electron/electronConstants.ts b/src/electron/electronConstants.ts index 8d88ef6b52..849cd7722e 100644 --- a/src/electron/electronConstants.ts +++ b/src/electron/electronConstants.ts @@ -6,4 +6,5 @@ export class ElectronConstants { static readonly enableAlwaysOnTopKey: string = 'enableAlwaysOnTopKey'; static readonly minimizeOnCopyToClipboardKey: string = 'minimizeOnCopyToClipboardKey'; static readonly enableBiometric: string = 'enabledBiometric'; + static readonly enableBrowserIntegration: string = 'enableBrowserIntegration'; } diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index dcefdc8883..f4dfacf980 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -219,4 +219,8 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { resolve(val); }); } + + supportsSecureStorage(): boolean { + return true; + } } diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index 7e077933d4..d3cca66aec 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -27,6 +27,7 @@ export class ConstantsService { static readonly ssoStateKey: string = 'ssoState'; static readonly biometricUnlockKey: string = 'biometric'; static readonly biometricText: string = 'biometricText'; + static readonly biometricAwaitingAcceptance: string = 'biometricAwaitingAcceptance'; readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; readonly disableGaKey: string = ConstantsService.disableGaKey; @@ -55,4 +56,5 @@ export class ConstantsService { readonly ssoStateKey: string = ConstantsService.ssoStateKey; readonly biometricUnlockKey: string = ConstantsService.biometricUnlockKey; readonly biometricText: string = ConstantsService.biometricText; + readonly biometricAwaitingAcceptance: string = ConstantsService.biometricAwaitingAcceptance; } diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index a65402b322..edc63862df 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -10,6 +10,7 @@ import { ProfileOrganizationResponse } from '../models/response/profileOrganizat import { CryptoService as CryptoServiceAbstraction } from '../abstractions/crypto.service'; import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; +import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { StorageService } from '../abstractions/storage.service'; import { ConstantsService } from './constants.service'; @@ -36,14 +37,14 @@ export class CryptoService implements CryptoServiceAbstraction { private orgKeys: Map; constructor(private storageService: StorageService, private secureStorageService: StorageService, - private cryptoFunctionService: CryptoFunctionService) { } + private cryptoFunctionService: CryptoFunctionService, private platformUtilService: PlatformUtilsService) { } async setKey(key: SymmetricCryptoKey): Promise { this.key = key; const option = await this.storageService.get(ConstantsService.vaultTimeoutKey); const biometric = await this.storageService.get(ConstantsService.biometricUnlockKey); - if (option != null && !biometric) { + if (option != null && !(biometric && this.platformUtilService.supportsSecureStorage())) { // if we have a lock option set, we do not store the key return; } @@ -293,7 +294,7 @@ export class CryptoService implements CryptoServiceAbstraction { const key = await this.getKey(); const option = await this.storageService.get(ConstantsService.vaultTimeoutKey); const biometric = await this.storageService.get(ConstantsService.biometricUnlockKey); - if (!biometric && (option != null || option === 0)) { + if ((!biometric && this.platformUtilService.supportsSecureStorage()) && (option != null || option === 0)) { // if we have a lock option set, clear the key await this.clearKey(); this.key = key; diff --git a/src/services/vaultTimeout.service.ts b/src/services/vaultTimeout.service.ts index f801b554b8..98e5c02f26 100644 --- a/src/services/vaultTimeout.service.ts +++ b/src/services/vaultTimeout.service.ts @@ -97,9 +97,10 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { return; } + this.biometricLocked = true; if (allowSoftLock) { const biometricLocked = await this.isBiometricLockSet(); - if (biometricLocked) { + if (biometricLocked && this.platformUtilsService.supportsSecureStorage()) { this.messagingService.send('locked'); if (this.lockedCallback != null) { await this.lockedCallback(); From d9d13bbf0e2b5b6d93459f3eca22c65c00543716 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Sun, 22 Nov 2020 08:46:56 -0600 Subject: [PATCH 1102/1626] Update success route before navigate action (#208) --- src/angular/components/two-factor.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 709ce2d4db..0221bceede 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -197,12 +197,12 @@ export class TwoFactorComponent implements OnInit, OnDestroy { this.onSuccessfulLogin(); } this.platformUtilsService.eventTrack('Logged In From Two-step'); + if (response.resetMasterPassword) { + this.successRoute = 'set-password'; + } if (this.onSuccessfulLoginNavigate != null) { this.onSuccessfulLoginNavigate(); } else { - if (response.resetMasterPassword) { - this.successRoute = 'set-password'; - } this.router.navigate([this.successRoute], { queryParams: { identifier: this.identifier, From cd6b3d47c2c8a04f37d2dc7468ee29990a6762fc Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Mon, 23 Nov 2020 08:48:40 -0600 Subject: [PATCH 1103/1626] Include revision date in cipher requests (#203) This is in conjunction with API changes that validates the last known revision date provided here with the actual last revision date on the server to block potential data loss due to client desyncs. Co-authored-by: Matt Gibson --- src/models/request/cipherRequest.ts | 2 ++ src/services/cipher.service.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/models/request/cipherRequest.ts b/src/models/request/cipherRequest.ts index 9333e753f5..5c1e950311 100644 --- a/src/models/request/cipherRequest.ts +++ b/src/models/request/cipherRequest.ts @@ -28,6 +28,7 @@ export class CipherRequest { // Deprecated, remove at some point and rename attachments2 to attachments attachments: { [id: string]: string; }; attachments2: { [id: string]: AttachmentRequest; }; + lastKnownRevisionDate: Date; constructor(cipher: Cipher) { this.type = cipher.type; @@ -36,6 +37,7 @@ export class CipherRequest { this.name = cipher.name ? cipher.name.encryptedString : null; this.notes = cipher.notes ? cipher.notes.encryptedString : null; this.favorite = cipher.favorite; + this.lastKnownRevisionDate = cipher.revisionDate; switch (this.type) { case CipherType.Login: diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 6cd58de8d5..c45965367d 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -143,6 +143,7 @@ export class CipherService implements CipherServiceAbstraction { cipher.organizationId = model.organizationId; cipher.type = model.type; cipher.collectionIds = model.collectionIds; + cipher.revisionDate = model.revisionDate; if (key == null && cipher.organizationId != null) { key = await this.cryptoService.getOrgKey(cipher.organizationId); From adcc618b42a7643b2500c7ed31fbe62a9a2ef3ee Mon Sep 17 00:00:00 2001 From: Fredrik Ekre Date: Mon, 23 Nov 2020 18:09:09 +0100 Subject: [PATCH 1104/1626] sync: move try-catch out of needsSyncing and handle errors it in fullSync (#207) The motivation for this is https://github.com/bitwarden/cli/issues/129 where failed sync's are swallowed by try-catch. By moving the try-catch to the outside it is possible to reuse the already existing allowThrowOnError argument which callers can use to signal whether fullSync should throw or ignore errors silently. This patch is companioned with a patch to the SyncCommand CLI command to pass allowThrowOnError. --- src/services/sync.service.ts | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index f60df18c90..055f09a702 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -76,12 +76,13 @@ export class SyncService implements SyncServiceAbstraction { } const now = new Date(); - const needsSyncResult = await this.needsSyncing(forceSync); - const needsSync = needsSyncResult[0]; - const skipped = needsSyncResult[1]; - - if (skipped) { - return this.syncCompleted(false); + var needsSync = false; + try { + needsSync = await this.needsSyncing(forceSync); + } catch (e) { + if (allowThrowOnError) { + throw e; + } } if (!needsSync) { @@ -226,23 +227,19 @@ export class SyncService implements SyncServiceAbstraction { private async needsSyncing(forceSync: boolean) { if (forceSync) { - return [true, false]; + return true; } const lastSync = await this.getLastSync(); if (lastSync == null || lastSync.getTime() === 0) { - return [true, false]; + return true; } - try { - const response = await this.apiService.getAccountRevisionDate(); - if (new Date(response) <= lastSync) { - return [false, false]; - } - return [true, false]; - } catch (e) { - return [false, true]; + const response = await this.apiService.getAccountRevisionDate(); + if (new Date(response) <= lastSync) { + return false; } + return true; } private async syncProfile(response: ProfileResponse) { From 0a73b6fca83b33573cf6ea0a41899ead34c23c46 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Mon, 23 Nov 2020 12:21:45 -0500 Subject: [PATCH 1105/1626] Fix lint error (#209) --- src/services/sync.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index 055f09a702..791d5f0240 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -76,7 +76,7 @@ export class SyncService implements SyncServiceAbstraction { } const now = new Date(); - var needsSync = false; + let needsSync = false; try { needsSync = await this.needsSyncing(forceSync); } catch (e) { From ea6fd5ac38c3f2af1f75d31b0a0b2f60a69fe5a7 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Mon, 23 Nov 2020 16:12:28 -0500 Subject: [PATCH 1106/1626] Update SSO component to parse state for comparison (#210) * Update sso component to parse state for comparison * No more truthy or dare * fix lint errors for === --- src/angular/components/sso.component.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/angular/components/sso.component.ts b/src/angular/components/sso.component.ts index f5dbaab7dd..f0b2f1d41e 100644 --- a/src/angular/components/sso.component.ts +++ b/src/angular/components/sso.component.ts @@ -51,8 +51,8 @@ export class SsoComponent { const state = await this.storageService.get(ConstantsService.ssoStateKey); await this.storageService.remove(ConstantsService.ssoCodeVerifierKey); await this.storageService.remove(ConstantsService.ssoStateKey); - if (qParams.code != null && codeVerifier != null && state != null && state === qParams.state) { - await this.logIn(qParams.code, codeVerifier, this.getOrgIdentiferFromState(state)); + if (qParams.code != null && codeVerifier != null && state != null && this.checkState(state, qParams.state)) { + await this.logIn(qParams.code, codeVerifier, this.getOrgIdentiferFromState(qParams.state)); } } else if (qParams.clientId != null && qParams.redirectUri != null && qParams.state != null && qParams.codeChallenge != null) { @@ -177,11 +177,24 @@ export class SsoComponent { } private getOrgIdentiferFromState(state: string): string { - if (!state) { + if (state === null || state === undefined) { return null; } const stateSplit = state.split('_identifier='); return stateSplit.length > 1 ? stateSplit[1] : null; } + + private checkState(state: string, checkState: string): boolean { + if (state === null || state === undefined) { + return false; + } + if (checkState === null || checkState === undefined) { + return false; + } + + const stateSplit = state.split('_identifier='); + const checkStateSplit = checkState.split('_identifier='); + return stateSplit[0] === checkStateSplit[0]; + } } From abb54f007305eabd77996623dd20cbe45345e82a Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Mon, 23 Nov 2020 16:45:09 -0500 Subject: [PATCH 1107/1626] Add normalized state compare for CLI (#211) --- src/cli/commands/login.command.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/cli/commands/login.command.ts b/src/cli/commands/login.command.ts index 396e37d2f2..0f58cb361b 100644 --- a/src/cli/commands/login.command.ts +++ b/src/cli/commands/login.command.ts @@ -262,7 +262,7 @@ export class LoginCommand { const code = url.searchParams.get('code'); const receivedState = url.searchParams.get('state'); res.setHeader('Content-Type', 'text/html'); - if (code != null && receivedState != null && receivedState === state) { + if (code != null && receivedState != null && this.checkState(receivedState, state)) { res.writeHead(200); res.end('Success | Bitwarden CLI' + '

Successfully authenticated with the Bitwarden CLI

' + @@ -300,4 +300,17 @@ export class LoginCommand { } }); } + + private checkState(state: string, checkState: string): boolean { + if (state === null || state === undefined) { + return false; + } + if (checkState === null || checkState === undefined) { + return false; + } + + const stateSplit = state.split('_identifier='); + const checkStateSplit = checkState.split('_identifier='); + return stateSplit[0] === checkStateSplit[0]; + } } From 93a3053f54bb654e689962543a07573b4c090515 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 3 Dec 2020 15:20:38 -0500 Subject: [PATCH 1108/1626] support for encrypted json export (#216) * support for encrypted json export * adjust filename prefix for encrypted formats * flip if logic * remove format param from encrypted export * encryptedFormat getter --- src/abstractions/export.service.ts | 4 +- src/angular/components/export.component.ts | 17 ++- src/models/export/card.ts | 25 +++-- src/models/export/cipher.ts | 21 +++- src/models/export/cipherWithIds.ts | 4 +- src/models/export/collection.ts | 10 +- src/models/export/collectionWithId.ts | 4 +- src/models/export/field.ts | 13 ++- src/models/export/folder.ts | 10 +- src/models/export/folderWithId.ts | 4 +- src/models/export/identity.ts | 61 +++++++---- src/models/export/login.ts | 22 +++- src/models/export/loginUri.ts | 10 +- src/models/export/secureNote.ts | 4 +- src/services/export.service.ts | 115 +++++++++++++++++++-- 15 files changed, 263 insertions(+), 61 deletions(-) diff --git a/src/abstractions/export.service.ts b/src/abstractions/export.service.ts index a6b719e8b3..74b506e954 100644 --- a/src/abstractions/export.service.ts +++ b/src/abstractions/export.service.ts @@ -1,5 +1,5 @@ export abstract class ExportService { - getExport: (format?: 'csv' | 'json') => Promise; - getOrganizationExport: (organizationId: string, format?: 'csv' | 'json') => Promise; + getExport: (format?: 'csv' | 'json' | 'encrypted_json') => Promise; + getOrganizationExport: (organizationId: string, format?: 'csv' | 'json' | 'encrypted_json') => Promise; getFileName: (prefix?: string, extension?: string) => string; } diff --git a/src/angular/components/export.component.ts b/src/angular/components/export.component.ts index 319134d519..38ade297ea 100644 --- a/src/angular/components/export.component.ts +++ b/src/angular/components/export.component.ts @@ -17,13 +17,17 @@ export class ExportComponent { formPromise: Promise; masterPassword: string; - format: 'json' | 'csv' = 'json'; + format: 'json' | 'encrypted_json' | 'csv' = 'json'; showPassword = false; constructor(protected cryptoService: CryptoService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected exportService: ExportService, protected eventService: EventService, protected win: Window) { } + get encryptedFormat() { + return this.format === 'encrypted_json'; + } + async submit() { if (this.masterPassword == null || this.masterPassword === '') { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), @@ -63,7 +67,16 @@ export class ExportComponent { } protected getFileName(prefix?: string) { - return this.exportService.getFileName(prefix, this.format); + let extension = this.format; + if (this.format === 'encrypted_json') { + if (prefix == null) { + prefix = 'encrypted'; + } else { + prefix = 'encrypted_' + prefix; + } + extension = 'json'; + } + return this.exportService.getFileName(prefix, extension); } protected async collectEvent(): Promise { diff --git a/src/models/export/card.ts b/src/models/export/card.ts index fda37089c9..380f112612 100644 --- a/src/models/export/card.ts +++ b/src/models/export/card.ts @@ -1,5 +1,7 @@ import { CardView } from '../view/cardView'; +import { Card as CardDomain } from '../domain/card'; + export class Card { static template(): Card { const req = new Card(); @@ -29,16 +31,25 @@ export class Card { expYear: string; code: string; - constructor(o?: CardView) { + constructor(o?: CardView | CardDomain) { if (o == null) { return; } - this.cardholderName = o.cardholderName; - this.brand = o.brand; - this.number = o.number; - this.expMonth = o.expMonth; - this.expYear = o.expYear; - this.code = o.code; + if (o instanceof CardView) { + this.cardholderName = o.cardholderName; + this.brand = o.brand; + this.number = o.number; + this.expMonth = o.expMonth; + this.expYear = o.expYear; + this.code = o.code; + } else { + this.cardholderName = o.cardholderName?.encryptedString; + this.brand = o.brand?.encryptedString; + this.number = o.number?.encryptedString; + this.expMonth = o.expMonth?.encryptedString; + this.expYear = o.expYear?.encryptedString; + this.code = o.code?.encryptedString; + } } } diff --git a/src/models/export/cipher.ts b/src/models/export/cipher.ts index 82f9326021..5fb2462f25 100644 --- a/src/models/export/cipher.ts +++ b/src/models/export/cipher.ts @@ -2,6 +2,8 @@ import { CipherType } from '../../enums/cipherType'; import { CipherView } from '../view/cipherView'; +import { Cipher as CipherDomain } from '../domain/cipher'; + import { Card } from './card'; import { Field } from './field'; import { Identity } from './identity'; @@ -70,16 +72,27 @@ export class Cipher { identity: Identity; // Use build method instead of ctor so that we can control order of JSON stringify for pretty print - build(o: CipherView) { + build(o: CipherView | CipherDomain) { this.organizationId = o.organizationId; this.folderId = o.folderId; this.type = o.type; - this.name = o.name; - this.notes = o.notes; + + if (o instanceof CipherView) { + this.name = o.name; + this.notes = o.notes; + } else { + this.name = o.name?.encryptedString; + this.notes = o.notes?.encryptedString; + } + this.favorite = o.favorite; if (o.fields != null) { - this.fields = o.fields.map((f) => new Field(f)); + if (o instanceof CipherView) { + this.fields = o.fields.map((f) => new Field(f)); + } else { + this.fields = o.fields.map((f) => new Field(f)); + } } switch (o.type) { diff --git a/src/models/export/cipherWithIds.ts b/src/models/export/cipherWithIds.ts index a9635b999a..184cd542a2 100644 --- a/src/models/export/cipherWithIds.ts +++ b/src/models/export/cipherWithIds.ts @@ -2,12 +2,14 @@ import { Cipher } from './cipher'; import { CipherView } from '../view/cipherView'; +import { Cipher as CipherDomain } from '../domain/cipher'; + export class CipherWithIds extends Cipher { id: string; collectionIds: string[]; // Use build method instead of ctor so that we can control order of JSON stringify for pretty print - build(o: CipherView) { + build(o: CipherView | CipherDomain) { this.id = o.id; super.build(o); this.collectionIds = o.collectionIds; diff --git a/src/models/export/collection.ts b/src/models/export/collection.ts index d4d9c7c3b1..03b2a42599 100644 --- a/src/models/export/collection.ts +++ b/src/models/export/collection.ts @@ -1,5 +1,7 @@ import { CollectionView } from '../view/collectionView'; +import { Collection as CollectionDomain } from '../domain/collection'; + export class Collection { static template(): Collection { const req = new Collection(); @@ -23,9 +25,13 @@ export class Collection { externalId: string; // Use build method instead of ctor so that we can control order of JSON stringify for pretty print - build(o: CollectionView) { + build(o: CollectionView | CollectionDomain) { this.organizationId = o.organizationId; - this.name = o.name; + if (o instanceof CollectionView) { + this.name = o.name; + } else { + this.name = o.name?.encryptedString; + } this.externalId = o.externalId; } } diff --git a/src/models/export/collectionWithId.ts b/src/models/export/collectionWithId.ts index 10d8181377..ef48ddd941 100644 --- a/src/models/export/collectionWithId.ts +++ b/src/models/export/collectionWithId.ts @@ -2,11 +2,13 @@ import { Collection } from './collection'; import { CollectionView } from '../view/collectionView'; +import { Collection as CollectionDomain } from '../domain/collection'; + export class CollectionWithId extends Collection { id: string; // Use build method instead of ctor so that we can control order of JSON stringify for pretty print - build(o: CollectionView) { + build(o: CollectionView | CollectionDomain) { this.id = o.id; super.build(o); } diff --git a/src/models/export/field.ts b/src/models/export/field.ts index 3e07c65eed..3029b2338e 100644 --- a/src/models/export/field.ts +++ b/src/models/export/field.ts @@ -2,6 +2,8 @@ import { FieldType } from '../../enums/fieldType'; import { FieldView } from '../view/fieldView'; +import { Field as FieldDomain } from '../domain/field'; + export class Field { static template(): Field { const req = new Field(); @@ -22,13 +24,18 @@ export class Field { value: string; type: FieldType; - constructor(o?: FieldView) { + constructor(o?: FieldView | FieldDomain) { if (o == null) { return; } - this.name = o.name; - this.value = o.value; + if (o instanceof FieldView) { + this.name = o.name; + this.value = o.value; + } else { + this.name = o.name?.encryptedString; + this.value = o.value?.encryptedString; + } this.type = o.type; } } diff --git a/src/models/export/folder.ts b/src/models/export/folder.ts index 8c2acf093a..b8d6f43977 100644 --- a/src/models/export/folder.ts +++ b/src/models/export/folder.ts @@ -1,5 +1,7 @@ import { FolderView } from '../view/folderView'; +import { Folder as FolderDomain } from '../domain/folder'; + export class Folder { static template(): Folder { const req = new Folder(); @@ -15,7 +17,11 @@ export class Folder { name: string; // Use build method instead of ctor so that we can control order of JSON stringify for pretty print - build(o: FolderView) { - this.name = o.name; + build(o: FolderView | FolderDomain) { + if (o instanceof FolderView) { + this.name = o.name; + } else { + this.name = o.name?.encryptedString; + } } } diff --git a/src/models/export/folderWithId.ts b/src/models/export/folderWithId.ts index 775376d203..83e57e710f 100644 --- a/src/models/export/folderWithId.ts +++ b/src/models/export/folderWithId.ts @@ -2,11 +2,13 @@ import { Folder } from './folder'; import { FolderView } from '../view/folderView'; +import { Folder as FolderDomain } from '../domain/folder'; + export class FolderWithId extends Folder { id: string; // Use build method instead of ctor so that we can control order of JSON stringify for pretty print - build(o: FolderView) { + build(o: FolderView | FolderDomain) { this.id = o.id; super.build(o); } diff --git a/src/models/export/identity.ts b/src/models/export/identity.ts index a1aae09850..20546ac7f0 100644 --- a/src/models/export/identity.ts +++ b/src/models/export/identity.ts @@ -1,5 +1,7 @@ import { IdentityView } from '../view/identityView'; +import { Identity as IdentityDomain } from '../domain/identity'; + export class Identity { static template(): Identity { const req = new Identity(); @@ -65,28 +67,49 @@ export class Identity { passportNumber: string; licenseNumber: string; - constructor(o?: IdentityView) { + constructor(o?: IdentityView | IdentityDomain) { if (o == null) { return; } - this.title = o.title; - this.firstName = o.firstName; - this.middleName = o.middleName; - this.lastName = o.lastName; - this.address1 = o.address1; - this.address2 = o.address2; - this.address3 = o.address3; - this.city = o.city; - this.state = o.state; - this.postalCode = o.postalCode; - this.country = o.country; - this.company = o.company; - this.email = o.email; - this.phone = o.phone; - this.ssn = o.ssn; - this.username = o.username; - this.passportNumber = o.passportNumber; - this.licenseNumber = o.licenseNumber; + if (o instanceof IdentityView) { + this.title = o.title; + this.firstName = o.firstName; + this.middleName = o.middleName; + this.lastName = o.lastName; + this.address1 = o.address1; + this.address2 = o.address2; + this.address3 = o.address3; + this.city = o.city; + this.state = o.state; + this.postalCode = o.postalCode; + this.country = o.country; + this.company = o.company; + this.email = o.email; + this.phone = o.phone; + this.ssn = o.ssn; + this.username = o.username; + this.passportNumber = o.passportNumber; + this.licenseNumber = o.licenseNumber; + } else { + this.title = o.title?.encryptedString; + this.firstName = o.firstName?.encryptedString; + this.middleName = o.middleName?.encryptedString; + this.lastName = o.lastName?.encryptedString; + this.address1 = o.address1?.encryptedString; + this.address2 = o.address2?.encryptedString; + this.address3 = o.address3?.encryptedString; + this.city = o.city?.encryptedString; + this.state = o.state?.encryptedString; + this.postalCode = o.postalCode?.encryptedString; + this.country = o.country?.encryptedString; + this.company = o.company?.encryptedString; + this.email = o.email?.encryptedString; + this.phone = o.phone?.encryptedString; + this.ssn = o.ssn?.encryptedString; + this.username = o.username?.encryptedString; + this.passportNumber = o.passportNumber?.encryptedString; + this.licenseNumber = o.licenseNumber?.encryptedString; + } } } diff --git a/src/models/export/login.ts b/src/models/export/login.ts index b5401bc939..d8ad6918ab 100644 --- a/src/models/export/login.ts +++ b/src/models/export/login.ts @@ -2,6 +2,8 @@ import { LoginUri } from './loginUri'; import { LoginView } from '../view/loginView'; +import { Login as LoginDomain } from '../domain/login'; + export class Login { static template(): Login { const req = new Login(); @@ -27,17 +29,27 @@ export class Login { password: string; totp: string; - constructor(o?: LoginView) { + constructor(o?: LoginView | LoginDomain) { if (o == null) { return; } if (o.uris != null) { - this.uris = o.uris.map((u) => new LoginUri(u)); + if (o instanceof LoginView) { + this.uris = o.uris.map((u) => new LoginUri(u)); + } else { + this.uris = o.uris.map((u) => new LoginUri(u)); + } } - this.username = o.username; - this.password = o.password; - this.totp = o.totp; + if (o instanceof LoginView) { + this.username = o.username; + this.password = o.password; + this.totp = o.totp; + } else { + this.username = o.username?.encryptedString; + this.password = o.password?.encryptedString; + this.totp = o.totp?.encryptedString; + } } } diff --git a/src/models/export/loginUri.ts b/src/models/export/loginUri.ts index 94c3bbd032..3b416d9fdd 100644 --- a/src/models/export/loginUri.ts +++ b/src/models/export/loginUri.ts @@ -2,6 +2,8 @@ import { UriMatchType } from '../../enums/uriMatchType'; import { LoginUriView } from '../view/loginUriView'; +import { LoginUri as LoginUriDomain } from '../domain/loginUri'; + export class LoginUri { static template(): LoginUri { const req = new LoginUri(); @@ -19,12 +21,16 @@ export class LoginUri { uri: string; match: UriMatchType = null; - constructor(o?: LoginUriView) { + constructor(o?: LoginUriView | LoginUriDomain) { if (o == null) { return; } - this.uri = o.uri; + if (o instanceof LoginUriView) { + this.uri = o.uri; + } else { + this.uri = o.uri?.encryptedString; + } this.match = o.match; } } diff --git a/src/models/export/secureNote.ts b/src/models/export/secureNote.ts index c2115fc337..31a80c04df 100644 --- a/src/models/export/secureNote.ts +++ b/src/models/export/secureNote.ts @@ -2,6 +2,8 @@ import { SecureNoteType } from '../../enums/secureNoteType'; import { SecureNoteView } from '../view/secureNoteView'; +import { SecureNote as SecureNoteDomain } from '../domain/secureNote'; + export class SecureNote { static template(): SecureNote { const req = new SecureNote(); @@ -16,7 +18,7 @@ export class SecureNote { type: SecureNoteType; - constructor(o?: SecureNoteView) { + constructor(o?: SecureNoteView | SecureNoteDomain) { if (o == null) { return; } diff --git a/src/services/export.service.ts b/src/services/export.service.ts index a9b67f617e..03c499d9b1 100644 --- a/src/services/export.service.ts +++ b/src/services/export.service.ts @@ -13,6 +13,7 @@ import { FolderView } from '../models/view/folderView'; import { Cipher } from '../models/domain/cipher'; import { Collection } from '../models/domain/collection'; +import { Folder } from '../models/domain/folder'; import { CipherData } from '../models/data/cipherData'; import { CollectionData } from '../models/data/collectionData'; @@ -26,7 +27,34 @@ export class ExportService implements ExportServiceAbstraction { constructor(private folderService: FolderService, private cipherService: CipherService, private apiService: ApiService) { } - async getExport(format: 'csv' | 'json' = 'csv'): Promise { + async getExport(format: 'csv' | 'json' | 'encrypted_json' = 'csv'): Promise { + if (format === 'encrypted_json') { + return this.getEncryptedExport(); + } else { + return this.getDecryptedExport(format); + } + } + + async getOrganizationExport(organizationId: string, + format: 'csv' | 'json' | 'encrypted_json' = 'csv'): Promise { + if (format === 'encrypted_json') { + return this.getOrganizationEncryptedExport(organizationId); + } else { + return this.getOrganizationDecryptedExport(organizationId, format); + } + } + + getFileName(prefix: string = null, extension: string = 'csv'): string { + const now = new Date(); + const dateString = + now.getFullYear() + '' + this.padNumber(now.getMonth() + 1, 2) + '' + this.padNumber(now.getDate(), 2) + + this.padNumber(now.getHours(), 2) + '' + this.padNumber(now.getMinutes(), 2) + + this.padNumber(now.getSeconds(), 2); + + return 'bitwarden' + (prefix ? ('_' + prefix) : '') + '_export_' + dateString + '.' + extension; + } + + private async getDecryptedExport(format: 'json' | 'csv'): Promise { let decFolders: FolderView[] = []; let decCiphers: CipherView[] = []; const promises = []; @@ -97,7 +125,38 @@ export class ExportService implements ExportServiceAbstraction { } } - async getOrganizationExport(organizationId: string, format: 'csv' | 'json' = 'csv'): Promise { + private async getEncryptedExport(): Promise { + const folders = await this.folderService.getAll(); + const ciphers = await this.cipherService.getAll(); + + const jsonDoc: any = { + folders: [], + items: [], + }; + + folders.forEach((f) => { + if (f.id == null) { + return; + } + const folder = new FolderExport(); + folder.build(f); + jsonDoc.folders.push(folder); + }); + + ciphers.forEach((c) => { + if (c.organizationId != null) { + return; + } + const cipher = new CipherExport(); + cipher.build(c); + cipher.collectionIds = null; + jsonDoc.items.push(cipher); + }); + + return JSON.stringify(jsonDoc, null, ' '); + } + + private async getOrganizationDecryptedExport(organizationId: string, format: 'json' | 'csv'): Promise { const decCollections: CollectionView[] = []; const decCiphers: CipherView[] = []; const promises = []; @@ -175,14 +234,52 @@ export class ExportService implements ExportServiceAbstraction { } } - getFileName(prefix: string = null, extension: string = 'csv'): string { - const now = new Date(); - const dateString = - now.getFullYear() + '' + this.padNumber(now.getMonth() + 1, 2) + '' + this.padNumber(now.getDate(), 2) + - this.padNumber(now.getHours(), 2) + '' + this.padNumber(now.getMinutes(), 2) + - this.padNumber(now.getSeconds(), 2); + private async getOrganizationEncryptedExport(organizationId: string): Promise { + const collections: Collection[] = []; + const ciphers: Cipher[] = []; + const promises = []; - return 'bitwarden' + (prefix ? ('_' + prefix) : '') + '_export_' + dateString + '.' + extension; + promises.push(this.apiService.getCollections(organizationId).then((c) => { + const collectionPromises: any = []; + if (c != null && c.data != null && c.data.length > 0) { + c.data.forEach((r) => { + const collection = new Collection(new CollectionData(r as CollectionDetailsResponse)); + collections.push(collection); + }); + } + return Promise.all(collectionPromises); + })); + + promises.push(this.apiService.getCiphersOrganization(organizationId).then((c) => { + const cipherPromises: any = []; + if (c != null && c.data != null && c.data.length > 0) { + c.data.forEach((r) => { + const cipher = new Cipher(new CipherData(r)); + ciphers.push(cipher); + }); + } + return Promise.all(cipherPromises); + })); + + await Promise.all(promises); + + const jsonDoc: any = { + collections: [], + items: [], + }; + + collections.forEach((c) => { + const collection = new CollectionExport(); + collection.build(c); + jsonDoc.collections.push(collection); + }); + + ciphers.forEach((c) => { + const cipher = new CipherExport(); + cipher.build(c); + jsonDoc.items.push(cipher); + }); + return JSON.stringify(jsonDoc, null, ' '); } private padNumber(num: number, width: number, padCharacter: string = '0'): string { From 0565d6f66750ab084c0398e38309e222b3bc2963 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 4 Dec 2020 12:05:31 -0500 Subject: [PATCH 1109/1626] Implemented tax collection for subscriptions (#215) --- src/abstractions/api.service.ts | 2 ++ .../request/organizationUpgradeRequest.ts | 2 ++ src/models/response/taxRateResponse.ts | 18 ++++++++++++++++++ src/services/api.service.ts | 6 ++++++ 4 files changed, 28 insertions(+) create mode 100644 src/models/response/taxRateResponse.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 8d0e5652db..65b8356074 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -99,6 +99,7 @@ import { SendResponse } from '../models/response/sendResponse'; import { SubscriptionResponse } from '../models/response/subscriptionResponse'; import { SyncResponse } from '../models/response/syncResponse'; import { TaxInfoResponse } from '../models/response/taxInfoResponse'; +import { TaxRateResponse } from '../models/response/taxRateResponse'; import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; import { TwoFactorDuoResponse } from '../models/response/twoFactorDuoResponse'; import { TwoFactorEmailResponse } from '../models/response/twoFactorEmailResponse'; @@ -299,6 +300,7 @@ export abstract class ApiService { postOrganizationReinstate: (id: string) => Promise; deleteOrganization: (id: string, request: PasswordVerificationRequest) => Promise; getPlans: () => Promise>; + getTaxRates: () => Promise>; getEvents: (start: string, end: string, token: string) => Promise>; getEventsCipher: (id: string, start: string, end: string, token: string) => Promise>; diff --git a/src/models/request/organizationUpgradeRequest.ts b/src/models/request/organizationUpgradeRequest.ts index 7f8793b2d1..0c0bfa8a07 100644 --- a/src/models/request/organizationUpgradeRequest.ts +++ b/src/models/request/organizationUpgradeRequest.ts @@ -6,4 +6,6 @@ export class OrganizationUpgradeRequest { additionalSeats: number; additionalStorageGb: number; premiumAccessAddon: boolean; + billingAddressCountry: string; + billingAddressPostalCode: string; } diff --git a/src/models/response/taxRateResponse.ts b/src/models/response/taxRateResponse.ts new file mode 100644 index 0000000000..83970afac5 --- /dev/null +++ b/src/models/response/taxRateResponse.ts @@ -0,0 +1,18 @@ +import { BaseResponse } from './baseResponse'; + +export class TaxRateResponse extends BaseResponse { + id: string; + country: string; + state: string; + postalCode: string; + rate: number; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty('Id'); + this.country = this.getResponseProperty('Country'); + this.state = this.getResponseProperty('State'); + this.postalCode = this.getResponseProperty('PostalCode'); + this.rate = this.getResponseProperty('Rate'); + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index cfcdddb573..037c8c6d9c 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -104,6 +104,7 @@ import { SendResponse } from '../models/response/sendResponse'; import { SubscriptionResponse } from '../models/response/subscriptionResponse'; import { SyncResponse } from '../models/response/syncResponse'; import { TaxInfoResponse } from '../models/response/taxInfoResponse'; +import { TaxRateResponse } from '../models/response/taxRateResponse'; import { TwoFactorAuthenticatorResponse } from '../models/response/twoFactorAuthenticatorResponse'; import { TwoFactorDuoResponse } from '../models/response/twoFactorDuoResponse'; import { TwoFactorEmailResponse } from '../models/response/twoFactorEmailResponse'; @@ -760,6 +761,11 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/organizations/' + organizationId + '/import', request, true, false); } + async getTaxRates(): Promise> { + const r = await this.send('GET', '/plans/sales-tax-rates/', null, true, true); + return new ListResponse(r, TaxRateResponse); + } + // Settings APIs async getSettingsDomains(): Promise { From c9df039fa9f392462130b733b4a724a024672866 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Fri, 4 Dec 2020 18:21:34 +0100 Subject: [PATCH 1110/1626] Desktop fit & finish (#212) * Add context menu on right click to mac * Add hide dock setting * Change "hide dock" to "always show dock" * Add support on mac for minimize to menu bar on close, minimize or start * Add "openAtLogin" to ElectronConstants * Add "restoreFromTray" to TrayMainService --- src/electron/electronConstants.ts | 2 + src/electron/tray.main.ts | 70 +++++++++++++++++++++---------- src/electron/window.main.ts | 2 +- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/electron/electronConstants.ts b/src/electron/electronConstants.ts index 849cd7722e..47510bb68b 100644 --- a/src/electron/electronConstants.ts +++ b/src/electron/electronConstants.ts @@ -7,4 +7,6 @@ export class ElectronConstants { static readonly minimizeOnCopyToClipboardKey: string = 'minimizeOnCopyToClipboardKey'; static readonly enableBiometric: string = 'enabledBiometric'; static readonly enableBrowserIntegration: string = 'enableBrowserIntegration'; + static readonly alwaysShowDock: string = 'alwaysShowDock'; + static readonly openAtLogin: string = 'openAtLogin'; } diff --git a/src/electron/tray.main.ts b/src/electron/tray.main.ts index 7e31b632fd..a782d95e12 100644 --- a/src/electron/tray.main.ts +++ b/src/electron/tray.main.ts @@ -1,4 +1,5 @@ import { + app, Menu, MenuItem, MenuItemConstructorOptions, @@ -44,7 +45,7 @@ export class TrayMain { }, { type: 'separator' }, { - label: process.platform === 'darwin' ? this.i18nService.t('close') : this.i18nService.t('exit'), + label: this.i18nService.t('exit'), click: () => this.closeWindow(), }]; @@ -52,30 +53,26 @@ export class TrayMain { menuItemOptions.splice(1, 0, ...additionalMenuItems); } - if (process.platform !== 'darwin') { - this.contextMenu = Menu.buildFromTemplate(menuItemOptions); - } + this.contextMenu = Menu.buildFromTemplate(menuItemOptions); if (await this.storageService.get(ElectronConstants.enableTrayKey)) { this.showTray(); } - if (process.platform === 'win32') { - this.windowMain.win.on('minimize', async (e: Event) => { - if (await this.storageService.get(ElectronConstants.enableMinimizeToTrayKey)) { + this.windowMain.win.on('minimize', async (e: Event) => { + if (await this.storageService.get(ElectronConstants.enableMinimizeToTrayKey)) { + e.preventDefault(); + this.hideToTray(); + } + }); + + this.windowMain.win.on('close', async (e: Event) => { + if (await this.storageService.get(ElectronConstants.enableCloseToTrayKey)) { + if (!this.windowMain.isQuitting) { e.preventDefault(); this.hideToTray(); } - }); - - this.windowMain.win.on('close', async (e: Event) => { - if (await this.storageService.get(ElectronConstants.enableCloseToTrayKey)) { - if (!this.windowMain.isQuitting) { - e.preventDefault(); - this.hideToTray(); - } - } - }); - } + } + }); this.windowMain.win.on('show', async (e: Event) => { const enableTray = await this.storageService.get(ElectronConstants.enableTrayKey); @@ -96,11 +93,20 @@ export class TrayMain { } } - hideToTray() { + async hideToTray() { this.showTray(); if (this.windowMain.win != null) { this.windowMain.win.hide(); } + if (this.isDarwin() && !await this.storageService.get(ElectronConstants.alwaysShowDock)) { + this.hideDock(); + } + } + + restoreFromTray() { + if (this.windowMain.win == null || !this.windowMain.win.isVisible()) { + this.toggleWindow(); + } } showTray() { @@ -111,29 +117,49 @@ export class TrayMain { this.tray = new Tray(this.icon); this.tray.setToolTip(this.appName); this.tray.on('click', () => this.toggleWindow()); + this.tray.on('right-click', () => this.tray.popUpContextMenu(this.contextMenu)); if (this.pressedIcon != null) { this.tray.setPressedImage(this.pressedIcon); } - if (this.contextMenu != null) { + if (this.contextMenu != null && !this.isDarwin()) { this.tray.setContextMenu(this.contextMenu); } } - private toggleWindow() { + private hideDock() { + app.dock.hide(); + } + + private showDock() { + app.dock.show(); + } + + private isDarwin() { + return process.platform === 'darwin'; + } + + private async toggleWindow() { if (this.windowMain.win == null) { - if (process.platform === 'darwin') { + if (this.isDarwin()) { // On MacOS, closing the window via the red button destroys the BrowserWindow instance. this.windowMain.createWindow().then(() => { this.windowMain.win.show(); + this.showDock(); }); } return; } if (this.windowMain.win.isVisible()) { this.windowMain.win.hide(); + if (this.isDarwin() && !await this.storageService.get(ElectronConstants.alwaysShowDock)) { + this.hideDock(); + } } else { this.windowMain.win.show(); + if (this.isDarwin()) { + this.showDock(); + } } } diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index e650ad38d6..e1a1498da8 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -72,7 +72,7 @@ export class WindowMain { app.on('window-all-closed', () => { // On OS X it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q - if (process.platform !== 'darwin' || isMacAppStore()) { + if (process.platform !== 'darwin' || this.isQuitting || isMacAppStore()) { app.quit(); } }); From 6fb06464817c127a07a7ea6aeeec3f37768238ad Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Fri, 4 Dec 2020 12:29:31 -0600 Subject: [PATCH 1111/1626] Fix 1password importer (#217) * Fix import of 1password csv * 1password is using '\' as a quote escape character. * 1password's csv headers are sometimes capitalized. We want to identify them case insensitively * Change cipher type based on csv type header * Translate 1password data to correct fields * Test identity and credit card import * linter fixes * Do not use node 'fs' module Karma is being used for automated tests so node modules are not available Co-authored-by: Matt Gibson --- .../importers/onepasswordCsvImporter.spec.ts | 50 +++++++++++ .../testData/onePasswordCsv/creditCard.csv.ts | 3 + .../testData/onePasswordCsv/identity.csv.ts | 6 ++ src/importers/baseImporter.ts | 14 +-- src/importers/onepasswordWinCsvImporter.ts | 89 ++++++++++++++++--- 5 files changed, 143 insertions(+), 19 deletions(-) create mode 100644 spec/common/importers/onepasswordCsvImporter.spec.ts create mode 100644 spec/common/importers/testData/onePasswordCsv/creditCard.csv.ts create mode 100644 spec/common/importers/testData/onePasswordCsv/identity.csv.ts diff --git a/spec/common/importers/onepasswordCsvImporter.spec.ts b/spec/common/importers/onepasswordCsvImporter.spec.ts new file mode 100644 index 0000000000..299b4901ed --- /dev/null +++ b/spec/common/importers/onepasswordCsvImporter.spec.ts @@ -0,0 +1,50 @@ +import { OnePasswordWinCsvImporter as Importer } from '../../../src/importers/onepasswordWinCsvImporter'; + +import { CipherType } from '../../../src/enums'; + +import { data as creditCardData } from './testData/onePasswordCsv/creditCard.csv' +import { data as identityData } from './testData/onePasswordCsv/identity.csv' + +describe('1Password CSV Importer', () => { + it('should parse identity imports', () => { + const importer = new Importer(); + const result = importer.parse(identityData); + + expect(result).not.toBeNull(); + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(1); + const cipher = result.ciphers[0]; + expect(cipher.type).toBe(CipherType.Identity) + + expect(cipher.identity).toEqual(jasmine.objectContaining({ + firstName: 'first name', + middleName: 'mi', + lastName: 'last name', + username: 'userNam3', + company: 'bitwarden', + phone: '8005555555', + email: 'email@bitwarden.com' + })); + + expect(cipher.notes).toContain('address\ncity state zip\nUnited States'); + }); + + it('should parse credit card imports', () => { + const importer = new Importer(); + const result = importer.parse(creditCardData); + + expect(result).not.toBeNull(); + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(1); + const cipher = result.ciphers[0]; + expect(cipher.type).toBe(CipherType.Card); + + expect(cipher.card).toEqual(jasmine.objectContaining({ + number: '4111111111111111', + code: '111', + cardholderName: 'test', + expMonth: '1', + expYear: '2030', + })); + }); +}); diff --git a/spec/common/importers/testData/onePasswordCsv/creditCard.csv.ts b/spec/common/importers/testData/onePasswordCsv/creditCard.csv.ts new file mode 100644 index 0000000000..47c05222a5 --- /dev/null +++ b/spec/common/importers/testData/onePasswordCsv/creditCard.csv.ts @@ -0,0 +1,3 @@ +export const data = `"account number(accountNo)","address(address)","address(branchAddress)","admin console URL(admin_console_url)","admin console username(admin_console_username)","AirPort ID(airport_id)","alias(alias)","AOL/AIM(aim)","approved wildlife(game)","attached storage password(disk_password)","auth​ method(pop_authentication)","auth​ method(smtp_authentication)","bank name(bankName)","base station name(name)","base station password(password)","birth date(birthdate)","business(busphone)","cardholder name(cardholder)","cash withdrawal limit(cashLimit)","cell(cellphone)","company name(company_name)","company(company)","conditions / restrictions(conditions)","connection options(options)","console password(admin_console_password)","country(country)","Created Date","credit limit(creditLimit)","customer service phone(customer_service_phone)","database(database)","date of birth(birthdate)","default phone(defphone)","department(department)","download page(download_link)","email(email)","expires(expires)","expiry date(expiry_date)","expiry date(expiry)","first name(firstname)","forum signature(forumsig)","full name(fullname)","full name(name)","group(org_name)","height(height)","home(homephone)","IBAN(iban)","ICQ(icq)","initial(initial)","interest rate(interest)","issue number(issuenumber)","issued on(issue_date)","issuing authority(issuing_authority)","issuing bank(bank)","issuing country(issuing_country)","job title(jobtitle)","last name(lastname)","license class(class)","license key(reg_code)","licensed to(reg_name)","maximum quota(quota)","member ID (additional)(additional_no)","member ID(membership_no)","member name(member_name)","member since(member_since)","Modified Date","MSN(msn)","name on account(owner)","name(name)","nationality(nationality)","network name(network_name)","Notes","number(ccnum)","number(number)","occupation(occupation)","order number(order_number)","order total(order_total)","Password","password(password)","password(pop_password)","password(smtp_password)","phone (intl)(phoneIntl)","phone (local)(phone_local)","phone (local)(phoneLocal)","phone (toll free)(phone_tollfree)","phone (toll free)(phoneTollFree)","phone for reserva​tions(reservations_phone)","phone(branchPhone)","PIN(pin)","PIN(telephonePin)","place of birth(birthplace)","port number(pop_port)","port number(smtp_port)","port(port)","provider's website(provider_website)","provider(provider)","publisher(publisher_name)","purchase date(order_date)","registered email(reg_email)","reminder answer(remindera)","reminder question(reminderq)","retail price(retail_price)","routing number(routingNo)","Scope","security(pop_security)","security(smtp_security)","server / IP address(server)","server(hostname)","server(pop_server)","sex(sex)","SID(sid)","skype(skype)","SMTP server(smtp_server)","state(state)","support email(support_email)","support phone(support_contact_phone)","support URL(support_contact_url)","SWIFT(swift)","Tags","telephone(phone)","Title","Type","type(accountType)","type(database_type)","type(pop_type)","type(type)","URL","URL(url)","Username","username(pop_username)","username(smtp_username)","username(username)","valid from(valid_from)","valid from(validFrom)","verification number(cvv)","version(product_version)","website(publisher_website)","website(website)","wireless network password(wireless_password)","wireless security(wireless_security)","Yahoo(yahoo)", +,,,,,,,,,,,,,,,,,"test",,,,,,,,,"1606923869",,,,,,,,,,,"01/2030",,,,,,,,,,,,,,,,,,,,,,,,,,,"1606924056",,,,,,"","4111111111111111",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"{( +)}",,"test card","Credit Card",,,,"laser",,,,,,,,,"111",,,,,,,` diff --git a/spec/common/importers/testData/onePasswordCsv/identity.csv.ts b/spec/common/importers/testData/onePasswordCsv/identity.csv.ts new file mode 100644 index 0000000000..2b74cd8d8a --- /dev/null +++ b/spec/common/importers/testData/onePasswordCsv/identity.csv.ts @@ -0,0 +1,6 @@ +export const data = `"account number(accountNo)","address(address)","address(branchAddress)","admin console URL(admin_console_url)","admin console username(admin_console_username)","AirPort ID(airport_id)","alias(alias)","AOL/AIM(aim)","approved wildlife(game)","attached storage password(disk_password)","auth​ method(pop_authentication)","auth​ method(smtp_authentication)","bank name(bankName)","base station name(name)","base station password(password)","birth date(birthdate)","business(busphone)","cardholder name(cardholder)","cash withdrawal limit(cashLimit)","cell(cellphone)","company name(company_name)","company(company)","conditions / restrictions(conditions)","connection options(options)","console password(admin_console_password)","country(country)","Created Date","credit limit(creditLimit)","customer service phone(customer_service_phone)","database(database)","date of birth(birthdate)","default phone(defphone)","department(department)","download page(download_link)","email(email)","expires(expires)","expiry date(expiry_date)","expiry date(expiry)","first name(firstname)","forum signature(forumsig)","full name(fullname)","full name(name)","group(org_name)","height(height)","home(homephone)","IBAN(iban)","ICQ(icq)","initial(initial)","interest rate(interest)","issue number(issuenumber)","issued on(issue_date)","issuing authority(issuing_authority)","issuing bank(bank)","issuing country(issuing_country)","job title(jobtitle)","last name(lastname)","license class(class)","license key(reg_code)","licensed to(reg_name)","maximum quota(quota)","member ID (additional)(additional_no)","member ID(membership_no)","member name(member_name)","member since(member_since)","Modified Date","MSN(msn)","name on account(owner)","name(name)","nationality(nationality)","network name(network_name)","Notes","number(ccnum)","number(number)","occupation(occupation)","order number(order_number)","order total(order_total)","Password","password(password)","password(pop_password)","password(smtp_password)","phone (intl)(phoneIntl)","phone (local)(phone_local)","phone (local)(phoneLocal)","phone (toll free)(phone_tollfree)","phone (toll free)(phoneTollFree)","phone for reserva​tions(reservations_phone)","phone(branchPhone)","PIN(pin)","PIN(telephonePin)","place of birth(birthplace)","port number(pop_port)","port number(smtp_port)","port(port)","provider's website(provider_website)","provider(provider)","publisher(publisher_name)","purchase date(order_date)","registered email(reg_email)","reminder answer(remindera)","reminder question(reminderq)","retail price(retail_price)","routing number(routingNo)","Scope","security(pop_security)","security(smtp_security)","server / IP address(server)","server(hostname)","server(pop_server)","sex(sex)","SID(sid)","skype(skype)","SMTP server(smtp_server)","state(state)","support email(support_email)","support phone(support_contact_phone)","support URL(support_contact_url)","SWIFT(swift)","Tags","telephone(phone)","Title","Type","type(accountType)","type(database_type)","type(pop_type)","type(type)","URL","URL(url)","Username","username(pop_username)","username(smtp_username)","username(username)","valid from(valid_from)","valid from(validFrom)","verification number(cvv)","version(product_version)","website(publisher_website)","website(website)","wireless network password(wireless_password)","wireless security(wireless_security)","Yahoo(yahoo)", +,"address +city state zip +United States",,,,,,"",,,,,,,,"12/2/20","",,,"",,"bitwarden",,,,,"1606923754",,,,"12/2/20","8005555555","department",,"email@bitwarden.com",,,,"first name","",,,,,"",,"","mi",,,,,,,"job title","last name",,,,,,,,,"1607020883","",,,,,"It’s you! 🖐 Select Edit to fill in more details, like your address and contact information.",,,"occupation",,,,,,,,,,,,,,,,,,,,,,,,,"","",,,,,,,,,"",,"",,,,,,,"{( + \\"Starter Kit\\" +)}",,"Identity Item","Identity",,,,,,,"userNam3",,,"userNam3",,,,,,"",,,"",` diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index 9c0ce1eef0..2999227d3c 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -65,19 +65,21 @@ export abstract class BaseImporter { 'ort', 'adresse', ]; + protected parseCsvOptions = { + encoding: 'UTF-8', + skipEmptyLines: false, + } + protected parseXml(data: string): Document { const parser = new DOMParser(); const doc = parser.parseFromString(data, 'application/xml'); return doc != null && doc.querySelector('parsererror') == null ? doc : null; } - protected parseCsv(data: string, header: boolean): any[] { + protected parseCsv(data: string, header: boolean, options: any = {}): any[] { + const parseOptions = Object.assign({ header: header }, this.parseCsvOptions, options); data = this.splitNewLine(data).join('\n').trim(); - const result = papa.parse(data, { - header: header, - encoding: 'UTF-8', - skipEmptyLines: false, - }); + const result = papa.parse(data, parseOptions); if (result.errors != null && result.errors.length > 0) { result.errors.forEach((e) => { if (e.row != null) { diff --git a/src/importers/onepasswordWinCsvImporter.ts b/src/importers/onepasswordWinCsvImporter.ts index 0ca9a93ab1..110268da92 100644 --- a/src/importers/onepasswordWinCsvImporter.ts +++ b/src/importers/onepasswordWinCsvImporter.ts @@ -4,14 +4,17 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; import { CipherType } from '../enums/cipherType'; -import { CardView } from '../models/view'; +import { CardView, IdentityView } from '../models/view'; -const IgnoredProperties = ['ainfo', 'autosubmit', 'notesplain', 'ps', 'scope', 'tags', 'title', 'uuid']; +const IgnoredProperties = ['ainfo', 'autosubmit', 'notesplain', 'ps', 'scope', 'tags', 'title', 'uuid', 'notes']; export class OnePasswordWinCsvImporter extends BaseImporter implements Importer { parse(data: string): ImportResult { const result = new ImportResult(); - const results = this.parseCsv(data, true); + const results = this.parseCsv(data, true, { + quoteChar: '"', + escapeChar: '\\', + }); if (results == null) { result.success = false; return result; @@ -24,7 +27,29 @@ export class OnePasswordWinCsvImporter extends BaseImporter implements Importer const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(this.getProp(value, 'title'), '--'); - cipher.notes = this.getValueOrDefault(this.getProp(value, 'notesPlain'), '') + '\n'; + + cipher.notes = this.getValueOrDefault(this.getProp(value, 'notesPlain'), '') + '\n' + + this.getValueOrDefault(this.getProp(value, 'notes'), '') + '\n'; + cipher.notes.trim(); + + const onePassType = this.getValueOrDefault(this.getProp(value, 'type'), 'Login') + switch (onePassType) { + case 'Credit Card': + cipher.type = CipherType.Card; + cipher.card = new CardView(); + IgnoredProperties.push('type'); + break; + case 'Identity': + cipher.type = CipherType.Identity; + cipher.identity = new IdentityView(); + IgnoredProperties.push('type'); + break; + case 'Login': + case 'Secure Note': + IgnoredProperties.push('type'); + default: + break; + } if (!this.isNullOrWhitespace(this.getProp(value, 'number')) && !this.isNullOrWhitespace(this.getProp(value, 'expiry date'))) { @@ -50,30 +75,59 @@ export class OnePasswordWinCsvImporter extends BaseImporter implements Importer const urls = value[property].split(this.newLineRegex); cipher.login.uris = this.makeUriArray(urls); continue; + } else if ((lowerProp === 'url')) { + if (cipher.login.uris == null) { + cipher.login.uris = []; + } + cipher.login.uris.concat(this.makeUriArray(value[property])); + continue; } } else if (cipher.type === CipherType.Card) { - if (this.isNullOrWhitespace(cipher.card.number) && lowerProp === 'number') { + if (this.isNullOrWhitespace(cipher.card.number) && lowerProp.includes('number')) { cipher.card.number = value[property]; cipher.card.brand = this.getCardBrand(this.getProp(value, 'number')); continue; - } else if (this.isNullOrWhitespace(cipher.card.code) && lowerProp === 'verification number') { + } else if (this.isNullOrWhitespace(cipher.card.code) && lowerProp.includes('verification number')) { cipher.card.code = value[property]; continue; - } else if (this.isNullOrWhitespace(cipher.card.cardholderName) && lowerProp === 'cardholder name') { + } else if (this.isNullOrWhitespace(cipher.card.cardholderName) && lowerProp.includes('cardholder name')) { cipher.card.cardholderName = value[property]; continue; - } else if (this.isNullOrWhitespace(cipher.card.expiration) && lowerProp === 'expiry date' && - value[property].length === 6) { - cipher.card.expMonth = (value[property] as string).substr(4, 2); + } else if (this.isNullOrWhitespace(cipher.card.expiration) && lowerProp.includes('expiry date') && + value[property].length === 7) { + cipher.card.expMonth = (value[property] as string).substr(0, 2); if (cipher.card.expMonth[0] === '0') { cipher.card.expMonth = cipher.card.expMonth.substr(1, 1); } - cipher.card.expYear = (value[property] as string).substr(0, 4); + cipher.card.expYear = (value[property] as string).substr(3, 4); continue; - } else if (lowerProp === 'type') { + } else if (lowerProp === 'type' || lowerProp === 'type(type)') { // Skip since brand was determined from number above continue; } + } else if (cipher.type === CipherType.Identity) { + if (this.isNullOrWhitespace(cipher.identity.firstName) && lowerProp.includes('first name')) { + cipher.identity.firstName = value[property]; + continue; + } else if (this.isNullOrWhitespace(cipher.identity.middleName) && lowerProp.includes('initial')) { + cipher.identity.middleName = value[property]; + continue; + } else if (this.isNullOrWhitespace(cipher.identity.lastName) && lowerProp.includes('last name')) { + cipher.identity.lastName = value[property]; + continue; + } else if (this.isNullOrWhitespace(cipher.identity.username) && lowerProp.includes('username')) { + cipher.identity.username = value[property]; + continue; + } else if (this.isNullOrWhitespace(cipher.identity.company) && lowerProp.includes('company')) { + cipher.identity.company = value[property]; + continue; + } else if (this.isNullOrWhitespace(cipher.identity.phone) && lowerProp.includes('default phone')) { + cipher.identity.phone = value[property]; + continue; + } else if (this.isNullOrWhitespace(cipher.identity.email) && lowerProp.includes('email')) { + cipher.identity.email = value[property]; + continue; + } } if (IgnoredProperties.indexOf(lowerProp) === -1 && !lowerProp.startsWith('section:') && @@ -81,6 +135,11 @@ export class OnePasswordWinCsvImporter extends BaseImporter implements Importer if (altUsername == null && lowerProp === 'email') { altUsername = value[property]; } + else if (lowerProp === 'created date' || lowerProp === 'modified date') { + const readableDate = new Date(parseInt(value[property], 10) * 1000).toUTCString(); + this.processKvp(cipher, '1Password ' + property, readableDate); + continue; + } this.processKvp(cipher, property, value[property]); } } @@ -100,6 +159,10 @@ export class OnePasswordWinCsvImporter extends BaseImporter implements Importer } private getProp(obj: any, name: string): any { - return obj[name] || obj[name.toUpperCase()]; + const lowerObj = Object.entries(obj).reduce((agg: any, entry: [string, any]) => { + agg[entry[0].toLowerCase()] = entry[1]; + return agg; + }, {}); + return lowerObj[name.toLowerCase()]; } } From 0fed528b6f1fe640acd9c321aa488b425cdf238e Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Fri, 4 Dec 2020 12:38:26 -0600 Subject: [PATCH 1112/1626] Use logService for console messages (#214) * Use logService for console messages * linter autofixes * Use full import path * Implement a base ConsoleLog service Use this class as a default for other services that would like to output to console. This service is overriden in CLI and Desktop to use CLI's consoleLogService and electronLogService, respectively. * linter fixes Co-authored-by: Matt Gibson --- src/abstractions/log.service.ts | 2 + src/cli/baseProgram.ts | 2 +- src/cli/services/consoleLog.service.ts | 50 +++----------- src/electron/services/electronLog.service.ts | 15 +++++ src/importers/baseImporter.ts | 6 +- src/services/auth.service.ts | 11 +++- src/services/consoleLog.service.ts | 68 ++++++++++++++++++++ src/services/crypto.service.ts | 21 +++--- src/services/notifications.service.ts | 12 +++- src/services/search.service.ts | 15 +++-- 10 files changed, 141 insertions(+), 61 deletions(-) create mode 100644 src/services/consoleLog.service.ts diff --git a/src/abstractions/log.service.ts b/src/abstractions/log.service.ts index c4cb55035e..6b93643625 100644 --- a/src/abstractions/log.service.ts +++ b/src/abstractions/log.service.ts @@ -6,4 +6,6 @@ export abstract class LogService { warning: (message: string) => void; error: (message: string) => void; write: (level: LogLevelType, message: string) => void; + time: (label: string) => void; + timeEnd: (label: string) => bigint; } diff --git a/src/cli/baseProgram.ts b/src/cli/baseProgram.ts index cc58ac1844..d8fa45acf3 100644 --- a/src/cli/baseProgram.ts +++ b/src/cli/baseProgram.ts @@ -18,7 +18,7 @@ export abstract class BaseProgram { if (!response.success) { if (process.env.BW_QUIET !== 'true') { if (process.env.BW_RESPONSE === 'true') { - this.writeLn(this.getJson(response), true, true); + this.writeLn(this.getJson(response), true, false); } else { this.writeLn(chalk.redBright(response.message), true, true); } diff --git a/src/cli/services/consoleLog.service.ts b/src/cli/services/consoleLog.service.ts index 5e1904790d..a7f7f39e2b 100644 --- a/src/cli/services/consoleLog.service.ts +++ b/src/cli/services/consoleLog.service.ts @@ -1,27 +1,10 @@ import { LogLevelType } from '../../enums/logLevelType'; -import { LogService as LogServiceAbstraction } from '../../abstractions/log.service'; +import { ConsoleLogService as BaseConsoleLogService } from '../../services/consoleLog.service'; -export class ConsoleLogService implements LogServiceAbstraction { - constructor(private isDev: boolean, private filter: (level: LogLevelType) => boolean = null) { } - - debug(message: string) { - if (!this.isDev) { - return; - } - this.write(LogLevelType.Debug, message); - } - - info(message: string) { - this.write(LogLevelType.Info, message); - } - - warning(message: string) { - this.write(LogLevelType.Warning, message); - } - - error(message: string) { - this.write(LogLevelType.Error, message); +export class ConsoleLogService extends BaseConsoleLogService { + constructor(isDev: boolean, filter: (level: LogLevelType) => boolean = null) { + super(isDev, filter); } write(level: LogLevelType, message: string) { @@ -29,25 +12,12 @@ export class ConsoleLogService implements LogServiceAbstraction { return; } - switch (level) { - case LogLevelType.Debug: - // tslint:disable-next-line - console.log(message); - break; - case LogLevelType.Info: - // tslint:disable-next-line - console.log(message); - break; - case LogLevelType.Warning: - // tslint:disable-next-line - console.warn(message); - break; - case LogLevelType.Error: - // tslint:disable-next-line - console.error(message); - break; - default: - break; + if (process.env.BW_RESPONSE) { + // tslint:disable-next-line + console.error(message); + return; } + + super.write(level, message); } } diff --git a/src/electron/services/electronLog.service.ts b/src/electron/services/electronLog.service.ts index c66a398d67..c01ce10218 100644 --- a/src/electron/services/electronLog.service.ts +++ b/src/electron/services/electronLog.service.ts @@ -8,6 +8,8 @@ import { LogLevelType } from '../../enums/logLevelType'; import { LogService as LogServiceAbstraction } from '../../abstractions/log.service'; export class ElectronLogService implements LogServiceAbstraction { + private timersMap: Map = new Map(); + constructor(private filter: (level: LogLevelType) => boolean = null, logDir: string = null) { if (log.transports == null) { return; @@ -61,4 +63,17 @@ export class ElectronLogService implements LogServiceAbstraction { break; } } + + time(label: string = 'default') { + if (!this.timersMap.has(label)) { + this.timersMap.set(label, process.hrtime.bigint()); + } + } + + timeEnd(label: string = 'default'): bigint { + const elapsed = (process.hrtime.bigint() - this.timersMap.get(label)) / BigInt(1000000); + this.timersMap.delete(label); + this.write(LogLevelType.Info, `${label}: ${elapsed}ms`); + return elapsed; + } } diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index 2999227d3c..a966af84ce 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -13,13 +13,17 @@ import { FolderView } from '../models/view/folderView'; import { LoginView } from '../models/view/loginView'; import { SecureNoteView } from '../models/view/secureNoteView'; +import { LogService } from '../abstractions/log.service'; import { CipherType } from '../enums/cipherType'; import { FieldType } from '../enums/fieldType'; import { SecureNoteType } from '../enums/secureNoteType'; +import { ConsoleLogService } from '../services/consoleLog.service'; export abstract class BaseImporter { organization = false; + protected logService: LogService = new ConsoleLogService(false); + protected newLineRegex = /(?:\r\n|\r|\n)/; protected passwordFieldNames = [ @@ -84,7 +88,7 @@ export abstract class BaseImporter { result.errors.forEach((e) => { if (e.row != null) { // tslint:disable-next-line - console.warn('Error parsing row ' + e.row + ': ' + e.message); + this.logService.warning('Error parsing row ' + e.row + ': ' + e.message); } }); } diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index d8ec84277d..fb024549d5 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -17,11 +17,13 @@ import { AppIdService } from '../abstractions/appId.service'; import { AuthService as AuthServiceAbstraction } from '../abstractions/auth.service'; import { CryptoService } from '../abstractions/crypto.service'; import { I18nService } from '../abstractions/i18n.service'; +import { LogService } from '../abstractions/log.service'; import { MessagingService } from '../abstractions/messaging.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { TokenService } from '../abstractions/token.service'; import { UserService } from '../abstractions/user.service'; import { VaultTimeoutService } from '../abstractions/vaultTimeout.service'; +import { ConsoleLogService } from '../services/consoleLog.service'; export const TwoFactorProviders = { [TwoFactorProviderType.Authenticator]: { @@ -91,7 +93,12 @@ export class AuthService implements AuthServiceAbstraction { private userService: UserService, private tokenService: TokenService, private appIdService: AppIdService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private messagingService: MessagingService, - private vaultTimeoutService: VaultTimeoutService, private setCryptoKeys = true) { } + private vaultTimeoutService: VaultTimeoutService, private logService?: LogService, + private setCryptoKeys = true) { + if (!logService) { + this.logService = new ConsoleLogService(false); + } + } init() { TwoFactorProviders[TwoFactorProviderType.Email].name = this.i18nService.t('emailTitle'); @@ -351,7 +358,7 @@ export class AuthService implements AuthServiceAbstraction { tokenResponse.privateKey = keyPair[1].encryptedString; } catch (e) { // tslint:disable-next-line - console.error(e); + this.logService.error(e); } } diff --git a/src/services/consoleLog.service.ts b/src/services/consoleLog.service.ts new file mode 100644 index 0000000000..7e334c399d --- /dev/null +++ b/src/services/consoleLog.service.ts @@ -0,0 +1,68 @@ +import { LogLevelType } from '../enums/logLevelType'; + +import { LogService as LogServiceAbstraction } from '../abstractions/log.service'; + +export class ConsoleLogService implements LogServiceAbstraction { + private timersMap: Map = new Map(); + + constructor(protected isDev: boolean, protected filter: (level: LogLevelType) => boolean = null) { } + + debug(message: string) { + if (!this.isDev) { + return; + } + this.write(LogLevelType.Debug, message); + } + + info(message: string) { + this.write(LogLevelType.Info, message); + } + + warning(message: string) { + this.write(LogLevelType.Warning, message); + } + + error(message: string) { + this.write(LogLevelType.Error, message); + } + + write(level: LogLevelType, message: string) { + if (this.filter != null && this.filter(level)) { + return; + } + + switch (level) { + case LogLevelType.Debug: + // tslint:disable-next-line + console.log(message); + break; + case LogLevelType.Info: + // tslint:disable-next-line + console.log(message); + break; + case LogLevelType.Warning: + // tslint:disable-next-line + console.warn(message); + break; + case LogLevelType.Error: + // tslint:disable-next-line + console.error(message); + break; + default: + break; + } + } + + time(label: string = 'default') { + if (!this.timersMap.has(label)) { + this.timersMap.set(label, process.hrtime.bigint()); + } + } + + timeEnd(label: string = 'default'): bigint { + const elapsed = (process.hrtime.bigint() - this.timersMap.get(label)) / BigInt(1000000); + this.timersMap.delete(label); + this.write(LogLevelType.Info, `${label}: ${elapsed}ms`); + return elapsed; + } +} diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index edc63862df..250f1fc937 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -10,8 +10,10 @@ import { ProfileOrganizationResponse } from '../models/response/profileOrganizat import { CryptoService as CryptoServiceAbstraction } from '../abstractions/crypto.service'; import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; +import { LogService } from '../abstractions/log.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { StorageService } from '../abstractions/storage.service'; +import { ConsoleLogService } from '../services/consoleLog.service'; import { ConstantsService } from './constants.service'; @@ -37,7 +39,12 @@ export class CryptoService implements CryptoServiceAbstraction { private orgKeys: Map; constructor(private storageService: StorageService, private secureStorageService: StorageService, - private cryptoFunctionService: CryptoFunctionService, private platformUtilService: PlatformUtilsService) { } + private cryptoFunctionService: CryptoFunctionService, private platformUtilService: PlatformUtilsService, + private logService?: LogService) { + if (!logService) { + this.logService = new ConsoleLogService(false); + } + } async setKey(key: SymmetricCryptoKey): Promise { this.key = key; @@ -546,14 +553,12 @@ export class CryptoService implements CryptoServiceAbstraction { const theKey = this.resolveLegacyKey(encType, keyForEnc); if (theKey.macKey != null && mac == null) { - // tslint:disable-next-line - console.error('mac required.'); + this.logService.error('mac required.'); return null; } if (theKey.encType !== encType) { - // tslint:disable-next-line - console.error('encType unavailable.'); + this.logService.error('encType unavailable.'); return null; } @@ -563,8 +568,7 @@ export class CryptoService implements CryptoServiceAbstraction { fastParams.macKey, 'sha256'); const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac); if (!macsEqual) { - // tslint:disable-next-line - console.error('mac failed.'); + this.logService.error('mac failed.'); return null; } } @@ -596,8 +600,7 @@ export class CryptoService implements CryptoServiceAbstraction { const macsMatch = await this.cryptoFunctionService.compare(mac, computedMac); if (!macsMatch) { - // tslint:disable-next-line - console.error('mac failed.'); + this.logService.error('mac failed.'); return null; } } diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index 5f5026ad6e..9c615a8a43 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -6,10 +6,12 @@ import { NotificationType } from '../enums/notificationType'; import { ApiService } from '../abstractions/api.service'; import { AppIdService } from '../abstractions/appId.service'; import { EnvironmentService } from '../abstractions/environment.service'; +import { LogService } from '../abstractions/log.service' import { NotificationsService as NotificationsServiceAbstraction } from '../abstractions/notifications.service'; import { SyncService } from '../abstractions/sync.service'; import { UserService } from '../abstractions/user.service'; import { VaultTimeoutService } from '../abstractions/vaultTimeout.service'; +import { ConsoleLogService } from '../services/consoleLog.service'; import { NotificationResponse, @@ -27,7 +29,12 @@ export class NotificationsService implements NotificationsServiceAbstraction { constructor(private userService: UserService, private syncService: SyncService, private appIdService: AppIdService, private apiService: ApiService, - private vaultTimeoutService: VaultTimeoutService, private logoutCallback: () => Promise) { } + private vaultTimeoutService: VaultTimeoutService, + private logoutCallback: () => Promise, private logService?: LogService) { + if (!logService) { + this.logService = new ConsoleLogService(false); + } + } async init(environmentService: EnvironmentService): Promise { this.inited = false; @@ -87,8 +94,7 @@ export class NotificationsService implements NotificationsServiceAbstraction { await this.signalrConnection.stop(); } } catch (e) { - // tslint:disable-next-line - console.error(e.toString()); + this.logService.error(e.toString()) } } diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 4b992d54f3..2067637a90 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -5,15 +5,20 @@ import { CipherView } from '../models/view/cipherView'; import { CipherService } from '../abstractions/cipher.service'; import { SearchService as SearchServiceAbstraction } from '../abstractions/search.service'; +import { LogService } from '../abstractions/log.service'; import { CipherType } from '../enums/cipherType'; import { FieldType } from '../enums/fieldType'; import { UriMatchType } from '../enums/uriMatchType'; +import { ConsoleLogService } from '../services/consoleLog.service'; export class SearchService implements SearchServiceAbstraction { private indexing = false; private index: lunr.Index = null; - constructor(private cipherService: CipherService) { + constructor(private cipherService: CipherService, private logService?: LogService) { + if (!logService) { + this.logService = new ConsoleLogService(false); + } } clearIndex(): void { @@ -30,8 +35,8 @@ export class SearchService implements SearchServiceAbstraction { if (this.indexing) { return; } - // tslint:disable-next-line - console.time('search indexing'); + + this.logService.time('search indexing'); this.indexing = true; this.index = null; const builder = new lunr.Builder(); @@ -62,8 +67,8 @@ export class SearchService implements SearchServiceAbstraction { ciphers.forEach((c) => builder.add(c)); this.index = builder.build(); this.indexing = false; - // tslint:disable-next-line - console.timeEnd('search indexing'); + + this.logService.timeEnd('search indexing'); } async searchCiphers(query: string, From 2b8c2c2b3ebd94704cd8bb871d535198842bfa07 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Fri, 4 Dec 2020 13:58:32 -0600 Subject: [PATCH 1113/1626] Revert "Use logService for console messages (#214)" (#219) This reverts commit 0fed528b6f1fe640acd9c321aa488b425cdf238e. --- src/abstractions/log.service.ts | 2 - src/cli/baseProgram.ts | 2 +- src/cli/services/consoleLog.service.ts | 50 +++++++++++--- src/electron/services/electronLog.service.ts | 15 ----- src/importers/baseImporter.ts | 6 +- src/services/auth.service.ts | 11 +--- src/services/consoleLog.service.ts | 68 -------------------- src/services/crypto.service.ts | 21 +++--- src/services/notifications.service.ts | 12 +--- src/services/search.service.ts | 15 ++--- 10 files changed, 61 insertions(+), 141 deletions(-) delete mode 100644 src/services/consoleLog.service.ts diff --git a/src/abstractions/log.service.ts b/src/abstractions/log.service.ts index 6b93643625..c4cb55035e 100644 --- a/src/abstractions/log.service.ts +++ b/src/abstractions/log.service.ts @@ -6,6 +6,4 @@ export abstract class LogService { warning: (message: string) => void; error: (message: string) => void; write: (level: LogLevelType, message: string) => void; - time: (label: string) => void; - timeEnd: (label: string) => bigint; } diff --git a/src/cli/baseProgram.ts b/src/cli/baseProgram.ts index d8fa45acf3..cc58ac1844 100644 --- a/src/cli/baseProgram.ts +++ b/src/cli/baseProgram.ts @@ -18,7 +18,7 @@ export abstract class BaseProgram { if (!response.success) { if (process.env.BW_QUIET !== 'true') { if (process.env.BW_RESPONSE === 'true') { - this.writeLn(this.getJson(response), true, false); + this.writeLn(this.getJson(response), true, true); } else { this.writeLn(chalk.redBright(response.message), true, true); } diff --git a/src/cli/services/consoleLog.service.ts b/src/cli/services/consoleLog.service.ts index a7f7f39e2b..5e1904790d 100644 --- a/src/cli/services/consoleLog.service.ts +++ b/src/cli/services/consoleLog.service.ts @@ -1,10 +1,27 @@ import { LogLevelType } from '../../enums/logLevelType'; -import { ConsoleLogService as BaseConsoleLogService } from '../../services/consoleLog.service'; +import { LogService as LogServiceAbstraction } from '../../abstractions/log.service'; -export class ConsoleLogService extends BaseConsoleLogService { - constructor(isDev: boolean, filter: (level: LogLevelType) => boolean = null) { - super(isDev, filter); +export class ConsoleLogService implements LogServiceAbstraction { + constructor(private isDev: boolean, private filter: (level: LogLevelType) => boolean = null) { } + + debug(message: string) { + if (!this.isDev) { + return; + } + this.write(LogLevelType.Debug, message); + } + + info(message: string) { + this.write(LogLevelType.Info, message); + } + + warning(message: string) { + this.write(LogLevelType.Warning, message); + } + + error(message: string) { + this.write(LogLevelType.Error, message); } write(level: LogLevelType, message: string) { @@ -12,12 +29,25 @@ export class ConsoleLogService extends BaseConsoleLogService { return; } - if (process.env.BW_RESPONSE) { - // tslint:disable-next-line - console.error(message); - return; + switch (level) { + case LogLevelType.Debug: + // tslint:disable-next-line + console.log(message); + break; + case LogLevelType.Info: + // tslint:disable-next-line + console.log(message); + break; + case LogLevelType.Warning: + // tslint:disable-next-line + console.warn(message); + break; + case LogLevelType.Error: + // tslint:disable-next-line + console.error(message); + break; + default: + break; } - - super.write(level, message); } } diff --git a/src/electron/services/electronLog.service.ts b/src/electron/services/electronLog.service.ts index c01ce10218..c66a398d67 100644 --- a/src/electron/services/electronLog.service.ts +++ b/src/electron/services/electronLog.service.ts @@ -8,8 +8,6 @@ import { LogLevelType } from '../../enums/logLevelType'; import { LogService as LogServiceAbstraction } from '../../abstractions/log.service'; export class ElectronLogService implements LogServiceAbstraction { - private timersMap: Map = new Map(); - constructor(private filter: (level: LogLevelType) => boolean = null, logDir: string = null) { if (log.transports == null) { return; @@ -63,17 +61,4 @@ export class ElectronLogService implements LogServiceAbstraction { break; } } - - time(label: string = 'default') { - if (!this.timersMap.has(label)) { - this.timersMap.set(label, process.hrtime.bigint()); - } - } - - timeEnd(label: string = 'default'): bigint { - const elapsed = (process.hrtime.bigint() - this.timersMap.get(label)) / BigInt(1000000); - this.timersMap.delete(label); - this.write(LogLevelType.Info, `${label}: ${elapsed}ms`); - return elapsed; - } } diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index a966af84ce..2999227d3c 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -13,17 +13,13 @@ import { FolderView } from '../models/view/folderView'; import { LoginView } from '../models/view/loginView'; import { SecureNoteView } from '../models/view/secureNoteView'; -import { LogService } from '../abstractions/log.service'; import { CipherType } from '../enums/cipherType'; import { FieldType } from '../enums/fieldType'; import { SecureNoteType } from '../enums/secureNoteType'; -import { ConsoleLogService } from '../services/consoleLog.service'; export abstract class BaseImporter { organization = false; - protected logService: LogService = new ConsoleLogService(false); - protected newLineRegex = /(?:\r\n|\r|\n)/; protected passwordFieldNames = [ @@ -88,7 +84,7 @@ export abstract class BaseImporter { result.errors.forEach((e) => { if (e.row != null) { // tslint:disable-next-line - this.logService.warning('Error parsing row ' + e.row + ': ' + e.message); + console.warn('Error parsing row ' + e.row + ': ' + e.message); } }); } diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index fb024549d5..d8ec84277d 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -17,13 +17,11 @@ import { AppIdService } from '../abstractions/appId.service'; import { AuthService as AuthServiceAbstraction } from '../abstractions/auth.service'; import { CryptoService } from '../abstractions/crypto.service'; import { I18nService } from '../abstractions/i18n.service'; -import { LogService } from '../abstractions/log.service'; import { MessagingService } from '../abstractions/messaging.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { TokenService } from '../abstractions/token.service'; import { UserService } from '../abstractions/user.service'; import { VaultTimeoutService } from '../abstractions/vaultTimeout.service'; -import { ConsoleLogService } from '../services/consoleLog.service'; export const TwoFactorProviders = { [TwoFactorProviderType.Authenticator]: { @@ -93,12 +91,7 @@ export class AuthService implements AuthServiceAbstraction { private userService: UserService, private tokenService: TokenService, private appIdService: AppIdService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private messagingService: MessagingService, - private vaultTimeoutService: VaultTimeoutService, private logService?: LogService, - private setCryptoKeys = true) { - if (!logService) { - this.logService = new ConsoleLogService(false); - } - } + private vaultTimeoutService: VaultTimeoutService, private setCryptoKeys = true) { } init() { TwoFactorProviders[TwoFactorProviderType.Email].name = this.i18nService.t('emailTitle'); @@ -358,7 +351,7 @@ export class AuthService implements AuthServiceAbstraction { tokenResponse.privateKey = keyPair[1].encryptedString; } catch (e) { // tslint:disable-next-line - this.logService.error(e); + console.error(e); } } diff --git a/src/services/consoleLog.service.ts b/src/services/consoleLog.service.ts deleted file mode 100644 index 7e334c399d..0000000000 --- a/src/services/consoleLog.service.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { LogLevelType } from '../enums/logLevelType'; - -import { LogService as LogServiceAbstraction } from '../abstractions/log.service'; - -export class ConsoleLogService implements LogServiceAbstraction { - private timersMap: Map = new Map(); - - constructor(protected isDev: boolean, protected filter: (level: LogLevelType) => boolean = null) { } - - debug(message: string) { - if (!this.isDev) { - return; - } - this.write(LogLevelType.Debug, message); - } - - info(message: string) { - this.write(LogLevelType.Info, message); - } - - warning(message: string) { - this.write(LogLevelType.Warning, message); - } - - error(message: string) { - this.write(LogLevelType.Error, message); - } - - write(level: LogLevelType, message: string) { - if (this.filter != null && this.filter(level)) { - return; - } - - switch (level) { - case LogLevelType.Debug: - // tslint:disable-next-line - console.log(message); - break; - case LogLevelType.Info: - // tslint:disable-next-line - console.log(message); - break; - case LogLevelType.Warning: - // tslint:disable-next-line - console.warn(message); - break; - case LogLevelType.Error: - // tslint:disable-next-line - console.error(message); - break; - default: - break; - } - } - - time(label: string = 'default') { - if (!this.timersMap.has(label)) { - this.timersMap.set(label, process.hrtime.bigint()); - } - } - - timeEnd(label: string = 'default'): bigint { - const elapsed = (process.hrtime.bigint() - this.timersMap.get(label)) / BigInt(1000000); - this.timersMap.delete(label); - this.write(LogLevelType.Info, `${label}: ${elapsed}ms`); - return elapsed; - } -} diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 250f1fc937..edc63862df 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -10,10 +10,8 @@ import { ProfileOrganizationResponse } from '../models/response/profileOrganizat import { CryptoService as CryptoServiceAbstraction } from '../abstractions/crypto.service'; import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; -import { LogService } from '../abstractions/log.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { StorageService } from '../abstractions/storage.service'; -import { ConsoleLogService } from '../services/consoleLog.service'; import { ConstantsService } from './constants.service'; @@ -39,12 +37,7 @@ export class CryptoService implements CryptoServiceAbstraction { private orgKeys: Map; constructor(private storageService: StorageService, private secureStorageService: StorageService, - private cryptoFunctionService: CryptoFunctionService, private platformUtilService: PlatformUtilsService, - private logService?: LogService) { - if (!logService) { - this.logService = new ConsoleLogService(false); - } - } + private cryptoFunctionService: CryptoFunctionService, private platformUtilService: PlatformUtilsService) { } async setKey(key: SymmetricCryptoKey): Promise { this.key = key; @@ -553,12 +546,14 @@ export class CryptoService implements CryptoServiceAbstraction { const theKey = this.resolveLegacyKey(encType, keyForEnc); if (theKey.macKey != null && mac == null) { - this.logService.error('mac required.'); + // tslint:disable-next-line + console.error('mac required.'); return null; } if (theKey.encType !== encType) { - this.logService.error('encType unavailable.'); + // tslint:disable-next-line + console.error('encType unavailable.'); return null; } @@ -568,7 +563,8 @@ export class CryptoService implements CryptoServiceAbstraction { fastParams.macKey, 'sha256'); const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac); if (!macsEqual) { - this.logService.error('mac failed.'); + // tslint:disable-next-line + console.error('mac failed.'); return null; } } @@ -600,7 +596,8 @@ export class CryptoService implements CryptoServiceAbstraction { const macsMatch = await this.cryptoFunctionService.compare(mac, computedMac); if (!macsMatch) { - this.logService.error('mac failed.'); + // tslint:disable-next-line + console.error('mac failed.'); return null; } } diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index 9c615a8a43..5f5026ad6e 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -6,12 +6,10 @@ import { NotificationType } from '../enums/notificationType'; import { ApiService } from '../abstractions/api.service'; import { AppIdService } from '../abstractions/appId.service'; import { EnvironmentService } from '../abstractions/environment.service'; -import { LogService } from '../abstractions/log.service' import { NotificationsService as NotificationsServiceAbstraction } from '../abstractions/notifications.service'; import { SyncService } from '../abstractions/sync.service'; import { UserService } from '../abstractions/user.service'; import { VaultTimeoutService } from '../abstractions/vaultTimeout.service'; -import { ConsoleLogService } from '../services/consoleLog.service'; import { NotificationResponse, @@ -29,12 +27,7 @@ export class NotificationsService implements NotificationsServiceAbstraction { constructor(private userService: UserService, private syncService: SyncService, private appIdService: AppIdService, private apiService: ApiService, - private vaultTimeoutService: VaultTimeoutService, - private logoutCallback: () => Promise, private logService?: LogService) { - if (!logService) { - this.logService = new ConsoleLogService(false); - } - } + private vaultTimeoutService: VaultTimeoutService, private logoutCallback: () => Promise) { } async init(environmentService: EnvironmentService): Promise { this.inited = false; @@ -94,7 +87,8 @@ export class NotificationsService implements NotificationsServiceAbstraction { await this.signalrConnection.stop(); } } catch (e) { - this.logService.error(e.toString()) + // tslint:disable-next-line + console.error(e.toString()); } } diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 2067637a90..4b992d54f3 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -5,20 +5,15 @@ import { CipherView } from '../models/view/cipherView'; import { CipherService } from '../abstractions/cipher.service'; import { SearchService as SearchServiceAbstraction } from '../abstractions/search.service'; -import { LogService } from '../abstractions/log.service'; import { CipherType } from '../enums/cipherType'; import { FieldType } from '../enums/fieldType'; import { UriMatchType } from '../enums/uriMatchType'; -import { ConsoleLogService } from '../services/consoleLog.service'; export class SearchService implements SearchServiceAbstraction { private indexing = false; private index: lunr.Index = null; - constructor(private cipherService: CipherService, private logService?: LogService) { - if (!logService) { - this.logService = new ConsoleLogService(false); - } + constructor(private cipherService: CipherService) { } clearIndex(): void { @@ -35,8 +30,8 @@ export class SearchService implements SearchServiceAbstraction { if (this.indexing) { return; } - - this.logService.time('search indexing'); + // tslint:disable-next-line + console.time('search indexing'); this.indexing = true; this.index = null; const builder = new lunr.Builder(); @@ -67,8 +62,8 @@ export class SearchService implements SearchServiceAbstraction { ciphers.forEach((c) => builder.add(c)); this.index = builder.build(); this.indexing = false; - - this.logService.timeEnd('search indexing'); + // tslint:disable-next-line + console.timeEnd('search indexing'); } async searchCiphers(query: string, From dcbd09e736b516b359369f9d9fe5b0f5a6c2a928 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 4 Dec 2020 21:05:11 -0500 Subject: [PATCH 1114/1626] encrypted import for bitwarden json (#220) --- .../importers/keepass2XmlImporter.spec.ts | 2 +- .../importers/lastpassCsvImporter.spec.ts | 2 +- .../importers/onepassword1PifImporter.spec.ts | 10 +- .../importers/onepasswordCsvImporter.spec.ts | 8 +- src/abstractions/import.service.ts | 2 +- src/importers/ascendoCsvImporter.ts | 6 +- src/importers/avastCsvImporter.ts | 6 +- src/importers/avastJsonImporter.ts | 6 +- src/importers/aviraCsvImporter.ts | 6 +- src/importers/baseImporter.ts | 6 +- src/importers/bitwardenCsvImporter.ts | 6 +- src/importers/bitwardenJsonImporter.ts | 108 ++++++++++++++---- src/importers/blackBerryCsvImporter.ts | 6 +- src/importers/blurCsvImporter.ts | 6 +- src/importers/buttercupCsvImporter.ts | 6 +- src/importers/chromeCsvImporter.ts | 6 +- src/importers/clipperzHtmlImporter.ts | 8 +- src/importers/codebookCsvImporter.ts | 6 +- src/importers/dashlaneJsonImporter.ts | 6 +- src/importers/encryptrCsvImporter.ts | 6 +- src/importers/enpassCsvImporter.ts | 6 +- src/importers/enpassJsonImporter.ts | 6 +- src/importers/firefoxCsvImporter.ts | 6 +- src/importers/fsecureFskImporter.ts | 6 +- src/importers/gnomeJsonImporter.ts | 6 +- src/importers/importer.ts | 5 +- src/importers/kasperskyTxtImporter.ts | 4 +- src/importers/keepass2XmlImporter.ts | 8 +- src/importers/keepassxCsvImporter.ts | 6 +- src/importers/keeperCsvImporter.ts | 6 +- src/importers/lastpassCsvImporter.ts | 6 +- src/importers/logMeOnceCsvImporter.ts | 6 +- src/importers/meldiumCsvImporter.ts | 6 +- src/importers/msecureCsvImporter.ts | 6 +- src/importers/mykiCsvImporter.ts | 6 +- src/importers/onepassword1PifImporter.ts | 4 +- src/importers/onepasswordWinCsvImporter.ts | 6 +- src/importers/padlockCsvImporter.ts | 6 +- src/importers/passkeepCsvImporter.ts | 6 +- src/importers/passmanJsonImporter.ts | 6 +- src/importers/passpackCsvImporter.ts | 6 +- src/importers/passwordAgentCsvImporter.ts | 6 +- src/importers/passwordBossJsonImporter.ts | 6 +- src/importers/passwordDragonXmlImporter.ts | 6 +- src/importers/passwordSafeXmlImporter.ts | 8 +- src/importers/passwordWalletTxtImporter.ts | 6 +- src/importers/rememBearCsvImporter.ts | 6 +- src/importers/roboformCsvImporter.ts | 6 +- src/importers/safeInCloudXmlImporter.ts | 8 +- src/importers/saferpassCsvImport.ts | 6 +- src/importers/secureSafeCsvImporter.ts | 6 +- src/importers/splashIdCsvImporter.ts | 6 +- src/importers/stickyPasswordXmlImporter.ts | 6 +- src/importers/truekeyCsvImporter.ts | 6 +- src/importers/upmCsvImporter.ts | 6 +- src/importers/yotiCsvImporter.ts | 6 +- src/importers/zohoVaultCsvImporter.ts | 6 +- src/models/export/card.ts | 11 ++ src/models/export/cipher.ts | 33 ++++++ src/models/export/collection.ts | 10 ++ src/models/export/field.ts | 8 ++ src/models/export/folder.ts | 6 + src/models/export/identity.ts | 23 ++++ src/models/export/login.ts | 11 ++ src/models/export/loginUri.ts | 7 ++ src/models/export/secureNote.ts | 5 + src/services/export.service.ts | 4 + src/services/import.service.ts | 4 +- 68 files changed, 375 insertions(+), 188 deletions(-) diff --git a/spec/common/importers/keepass2XmlImporter.spec.ts b/spec/common/importers/keepass2XmlImporter.spec.ts index 757b53e69c..a744e322e3 100644 --- a/spec/common/importers/keepass2XmlImporter.spec.ts +++ b/spec/common/importers/keepass2XmlImporter.spec.ts @@ -192,7 +192,7 @@ line2 describe('KeePass2 Xml Importer', () => { it('should parse XML data', async () => { const importer = new Importer(); - const result = importer.parse(TestData); + const result = await importer.parse(TestData); expect(result != null).toBe(true); }); }); diff --git a/spec/common/importers/lastpassCsvImporter.spec.ts b/spec/common/importers/lastpassCsvImporter.spec.ts index 6254a816fe..df1dea0fb8 100644 --- a/spec/common/importers/lastpassCsvImporter.spec.ts +++ b/spec/common/importers/lastpassCsvImporter.spec.ts @@ -163,7 +163,7 @@ describe('Lastpass CSV Importer', () => { CipherData.forEach((data) => { it(data.title, async () => { const importer = new Importer(); - const result = importer.parse(data.csv); + const result = await importer.parse(data.csv); expect(result != null).toBe(true); expect(result.ciphers.length).toBeGreaterThan(0); diff --git a/spec/common/importers/onepassword1PifImporter.spec.ts b/spec/common/importers/onepassword1PifImporter.spec.ts index 276f36397a..98158fdc6d 100644 --- a/spec/common/importers/onepassword1PifImporter.spec.ts +++ b/spec/common/importers/onepassword1PifImporter.spec.ts @@ -434,7 +434,7 @@ const IdentityTestData = JSON.stringify({ describe('1Password 1Pif Importer', () => { it('should parse data', async () => { const importer = new Importer(); - const result = importer.parse(TestData); + const result = await importer.parse(TestData); expect(result != null).toBe(true); const cipher = result.ciphers.shift(); @@ -447,7 +447,7 @@ describe('1Password 1Pif Importer', () => { it('should create concealed field as "hidden" type', async () => { const importer = new Importer(); - const result = importer.parse(TestData); + const result = await importer.parse(TestData); expect(result != null).toBe(true); const ciphers = result.ciphers; @@ -465,7 +465,7 @@ describe('1Password 1Pif Importer', () => { it('should create identity records', async () => { const importer = new Importer(); - const result = importer.parse(IdentityTestData); + const result = await importer.parse(IdentityTestData); expect(result != null).toBe(true); const cipher = result.ciphers.shift(); expect(cipher.name).toEqual('Test Identity'); @@ -488,7 +488,7 @@ describe('1Password 1Pif Importer', () => { it('should create password history', async () => { const importer = new Importer(); - const result = importer.parse(TestData); + const result = await importer.parse(TestData); const cipher = result.ciphers.shift(); expect(cipher.passwordHistory.length).toEqual(1); @@ -499,7 +499,7 @@ describe('1Password 1Pif Importer', () => { it('should create password history from windows opvault 1pif format', async () => { const importer = new Importer(); - const result = importer.parse(WindowsOpVaultTestData); + const result = await importer.parse(WindowsOpVaultTestData); const cipher = result.ciphers.shift(); expect(cipher.passwordHistory.length).toEqual(5); diff --git a/spec/common/importers/onepasswordCsvImporter.spec.ts b/spec/common/importers/onepasswordCsvImporter.spec.ts index 299b4901ed..8ca37cef59 100644 --- a/spec/common/importers/onepasswordCsvImporter.spec.ts +++ b/spec/common/importers/onepasswordCsvImporter.spec.ts @@ -6,9 +6,9 @@ import { data as creditCardData } from './testData/onePasswordCsv/creditCard.csv import { data as identityData } from './testData/onePasswordCsv/identity.csv' describe('1Password CSV Importer', () => { - it('should parse identity imports', () => { + it('should parse identity imports', async () => { const importer = new Importer(); - const result = importer.parse(identityData); + const result = await importer.parse(identityData); expect(result).not.toBeNull(); expect(result.success).toBe(true); @@ -29,9 +29,9 @@ describe('1Password CSV Importer', () => { expect(cipher.notes).toContain('address\ncity state zip\nUnited States'); }); - it('should parse credit card imports', () => { + it('should parse credit card imports', async () => { const importer = new Importer(); - const result = importer.parse(creditCardData); + const result = await importer.parse(creditCardData); expect(result).not.toBeNull(); expect(result.success).toBe(true); diff --git a/src/abstractions/import.service.ts b/src/abstractions/import.service.ts index c471ea6ef3..8115c2c0b1 100644 --- a/src/abstractions/import.service.ts +++ b/src/abstractions/import.service.ts @@ -9,5 +9,5 @@ export abstract class ImportService { regularImportOptions: ImportOption[]; getImportOptions: () => ImportOption[]; import: (importer: Importer, fileContents: string, organizationId?: string) => Promise; - getImporter: (format: string, organization?: boolean) => Importer; + getImporter: (format: string, organizationId: string) => Importer; } diff --git a/src/importers/ascendoCsvImporter.ts b/src/importers/ascendoCsvImporter.ts index 52b9f0808b..f729c0f6d9 100644 --- a/src/importers/ascendoCsvImporter.ts +++ b/src/importers/ascendoCsvImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class AscendoCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, false); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -50,6 +50,6 @@ export class AscendoCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/avastCsvImporter.ts b/src/importers/avastCsvImporter.ts index a1eb16574f..8e12a05f9c 100644 --- a/src/importers/avastCsvImporter.ts +++ b/src/importers/avastCsvImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class AvastCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -23,6 +23,6 @@ export class AvastCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/avastJsonImporter.ts b/src/importers/avastJsonImporter.ts index b4981983a6..9f8d775295 100644 --- a/src/importers/avastJsonImporter.ts +++ b/src/importers/avastJsonImporter.ts @@ -7,12 +7,12 @@ import { CipherType } from '../enums/cipherType'; import { SecureNoteType } from '../enums/secureNoteType'; export class AvastJsonImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = JSON.parse(data); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } if (results.logins != null) { @@ -64,6 +64,6 @@ export class AvastJsonImporter extends BaseImporter implements Importer { } result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/aviraCsvImporter.ts b/src/importers/aviraCsvImporter.ts index 41774ba75c..2586aa63be 100644 --- a/src/importers/aviraCsvImporter.ts +++ b/src/importers/aviraCsvImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class AviraCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -31,6 +31,6 @@ export class AviraCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index 2999227d3c..ab02785bd6 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -18,7 +18,7 @@ import { FieldType } from '../enums/fieldType'; import { SecureNoteType } from '../enums/secureNoteType'; export abstract class BaseImporter { - organization = false; + organizationId: string = null; protected newLineRegex = /(?:\r\n|\r|\n)/; @@ -70,6 +70,10 @@ export abstract class BaseImporter { skipEmptyLines: false, } + protected organization() { + return this.organizationId != null; + } + protected parseXml(data: string): Document { const parser = new DOMParser(); const doc = parser.parseFromString(data, 'application/xml'); diff --git a/src/importers/bitwardenCsvImporter.ts b/src/importers/bitwardenCsvImporter.ts index 3da6c338b6..ab2bc0d83d 100644 --- a/src/importers/bitwardenCsvImporter.ts +++ b/src/importers/bitwardenCsvImporter.ts @@ -15,12 +15,12 @@ import { FieldType } from '../enums/fieldType'; import { SecureNoteType } from '../enums/secureNoteType'; export class BitwardenCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -105,6 +105,6 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/bitwardenJsonImporter.ts b/src/importers/bitwardenJsonImporter.ts index 1e94ad94db..25f72abe3b 100644 --- a/src/importers/bitwardenJsonImporter.ts +++ b/src/importers/bitwardenJsonImporter.ts @@ -8,37 +8,106 @@ import { CollectionWithId } from '../models/export/collectionWithId'; import { FolderWithId } from '../models/export/folderWithId'; export class BitwardenJsonImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { - const result = new ImportResult(); - const results = JSON.parse(data); - if (results == null || results.items == null || results.items.length === 0) { - result.success = false; - return result; + private results: any; + private result: ImportResult; + + async parse(data: string): Promise { + this.result = new ImportResult(); + this.results = JSON.parse(data); + if (this.results == null || this.results.items == null || this.results.items.length === 0) { + this.result.success = false; + return this.result; } + if (this.results.encrypted) { + await this.parseEncrypted(); + } else { + this.parseDecrypted(); + } + + this.result.success = true; + return this.result; + } + + private async parseEncrypted() { const groupingsMap = new Map(); - if (this.organization && results.collections != null) { - results.collections.forEach((c: CollectionWithId) => { + + if (this.organization && this.results.collections != null) { + for (const c of this.results.collections as CollectionWithId[]) { + const collection = CollectionWithId.toDomain(c); + if (collection != null) { + collection.id = null; + collection.organizationId = this.organizationId; + const view = await collection.decrypt(); + groupingsMap.set(c.id, this.result.collections.length); + this.result.collections.push(view); + } + } + } else if (!this.organization && this.results.folders != null) { + for (const f of this.results.folders as FolderWithId[]) { + const folder = FolderWithId.toDomain(f); + if (folder != null) { + folder.id = null; + const view = await folder.decrypt(); + groupingsMap.set(f.id, this.result.folders.length); + this.result.folders.push(view); + } + } + } + + for (const c of this.results.items as CipherWithIds[]) { + const cipher = CipherWithIds.toDomain(c); + // reset ids incase they were set for some reason + cipher.id = null; + cipher.folderId = null; + cipher.organizationId = this.organizationId; + cipher.collectionIds = null; + + // make sure password history is limited + if (cipher.passwordHistory != null && cipher.passwordHistory.length > 5) { + cipher.passwordHistory = cipher.passwordHistory.slice(0, 5); + } + + if (!this.organization && c.folderId != null && groupingsMap.has(c.folderId)) { + this.result.folderRelationships.push([this.result.ciphers.length, groupingsMap.get(c.folderId)]); + } else if (this.organization && c.collectionIds != null) { + c.collectionIds.forEach((cId) => { + if (groupingsMap.has(cId)) { + this.result.collectionRelationships.push([this.result.ciphers.length, groupingsMap.get(cId)]); + } + }); + } + + const view = await cipher.decrypt(); + this.cleanupCipher(view); + this.result.ciphers.push(view); + } + } + + private parseDecrypted() { + const groupingsMap = new Map(); + if (this.organization && this.results.collections != null) { + this.results.collections.forEach((c: CollectionWithId) => { const collection = CollectionWithId.toView(c); if (collection != null) { collection.id = null; collection.organizationId = null; - groupingsMap.set(c.id, result.collections.length); - result.collections.push(collection); + groupingsMap.set(c.id, this.result.collections.length); + this.result.collections.push(collection); } }); - } else if (!this.organization && results.folders != null) { - results.folders.forEach((f: FolderWithId) => { + } else if (!this.organization && this.results.folders != null) { + this.results.folders.forEach((f: FolderWithId) => { const folder = FolderWithId.toView(f); if (folder != null) { folder.id = null; - groupingsMap.set(f.id, result.folders.length); - result.folders.push(folder); + groupingsMap.set(f.id, this.result.folders.length); + this.result.folders.push(folder); } }); } - results.items.forEach((c: CipherWithIds) => { + this.results.items.forEach((c: CipherWithIds) => { const cipher = CipherWithIds.toView(c); // reset ids incase they were set for some reason cipher.id = null; @@ -52,20 +121,17 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { } if (!this.organization && c.folderId != null && groupingsMap.has(c.folderId)) { - result.folderRelationships.push([result.ciphers.length, groupingsMap.get(c.folderId)]); + this.result.folderRelationships.push([this.result.ciphers.length, groupingsMap.get(c.folderId)]); } else if (this.organization && c.collectionIds != null) { c.collectionIds.forEach((cId) => { if (groupingsMap.has(cId)) { - result.collectionRelationships.push([result.ciphers.length, groupingsMap.get(cId)]); + this.result.collectionRelationships.push([this.result.ciphers.length, groupingsMap.get(cId)]); } }); } this.cleanupCipher(cipher); - result.ciphers.push(cipher); + this.result.ciphers.push(cipher); }); - - result.success = true; - return result; } } diff --git a/src/importers/blackBerryCsvImporter.ts b/src/importers/blackBerryCsvImporter.ts index 286603f04b..5d26bcc9a6 100644 --- a/src/importers/blackBerryCsvImporter.ts +++ b/src/importers/blackBerryCsvImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class BlackBerryCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -31,6 +31,6 @@ export class BlackBerryCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/blurCsvImporter.ts b/src/importers/blurCsvImporter.ts index 9fd484a51f..6a6001b4f7 100644 --- a/src/importers/blurCsvImporter.ts +++ b/src/importers/blurCsvImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class BlurCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -34,6 +34,6 @@ export class BlurCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/buttercupCsvImporter.ts b/src/importers/buttercupCsvImporter.ts index 9173dc6569..344f7c0895 100644 --- a/src/importers/buttercupCsvImporter.ts +++ b/src/importers/buttercupCsvImporter.ts @@ -8,12 +8,12 @@ const OfficialProps = [ ]; export class ButtercupCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -46,6 +46,6 @@ export class ButtercupCsvImporter extends BaseImporter implements Importer { } result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/chromeCsvImporter.ts b/src/importers/chromeCsvImporter.ts index 5ca97dd20a..8f4a77822b 100644 --- a/src/importers/chromeCsvImporter.ts +++ b/src/importers/chromeCsvImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class ChromeCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -23,6 +23,6 @@ export class ChromeCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/clipperzHtmlImporter.ts b/src/importers/clipperzHtmlImporter.ts index 0443114811..a772ae0084 100644 --- a/src/importers/clipperzHtmlImporter.ts +++ b/src/importers/clipperzHtmlImporter.ts @@ -4,19 +4,19 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class ClipperzHtmlImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const doc = this.parseXml(data); if (doc == null) { result.success = false; - return result; + return Promise.resolve(result); } const textarea = doc.querySelector('textarea'); if (textarea == null || this.isNullOrWhitespace(textarea.textContent)) { result.errorMessage = 'Missing textarea.'; result.success = false; - return result; + return Promise.resolve(result); } const entries = JSON.parse(textarea.textContent); @@ -74,6 +74,6 @@ export class ClipperzHtmlImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/codebookCsvImporter.ts b/src/importers/codebookCsvImporter.ts index 7ee56c205b..96e56ef481 100644 --- a/src/importers/codebookCsvImporter.ts +++ b/src/importers/codebookCsvImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class CodebookCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -42,6 +42,6 @@ export class CodebookCsvImporter extends BaseImporter implements Importer { } result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/dashlaneJsonImporter.ts b/src/importers/dashlaneJsonImporter.ts index fdf2ebf0fc..dd8d7bb6a6 100644 --- a/src/importers/dashlaneJsonImporter.ts +++ b/src/importers/dashlaneJsonImporter.ts @@ -17,12 +17,12 @@ const HandledResults = new Set(['ADDRESS', 'AUTHENTIFIANT', 'BANKSTATEMENT', 'ID export class DashlaneJsonImporter extends BaseImporter implements Importer { private result: ImportResult; - parse(data: string): ImportResult { + parse(data: string): Promise { this.result = new ImportResult(); const results = JSON.parse(data); if (results == null || results.length === 0) { this.result.success = false; - return this.result; + return Promise.resolve(this.result); } if (results.ADDRESS != null) { @@ -51,7 +51,7 @@ export class DashlaneJsonImporter extends BaseImporter implements Importer { } this.result.success = true; - return this.result; + return Promise.resolve(this.result); } private processAuth(results: any[]) { diff --git a/src/importers/encryptrCsvImporter.ts b/src/importers/encryptrCsvImporter.ts index a1e57899e5..3deb155541 100644 --- a/src/importers/encryptrCsvImporter.ts +++ b/src/importers/encryptrCsvImporter.ts @@ -8,12 +8,12 @@ import { CardView } from '../models/view/cardView'; import { CipherType } from '../enums/cipherType'; export class EncryptrCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -57,6 +57,6 @@ export class EncryptrCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/enpassCsvImporter.ts b/src/importers/enpassCsvImporter.ts index 02fa9ab3ca..8543ae2f6e 100644 --- a/src/importers/enpassCsvImporter.ts +++ b/src/importers/enpassCsvImporter.ts @@ -10,12 +10,12 @@ import { CardView } from '../models/view/cardView'; import { SecureNoteView } from '../models/view/secureNoteView'; export class EnpassCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, false); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } let firstRow = true; @@ -99,7 +99,7 @@ export class EnpassCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } private containsField(fields: any[], name: string) { diff --git a/src/importers/enpassJsonImporter.ts b/src/importers/enpassJsonImporter.ts index ca43577239..1b325224bb 100644 --- a/src/importers/enpassJsonImporter.ts +++ b/src/importers/enpassJsonImporter.ts @@ -11,12 +11,12 @@ import { CipherType } from '../enums/cipherType'; import { FieldType } from '../enums/fieldType'; export class EnpassJsonImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = JSON.parse(data); if (results == null || results.items == null || results.items.length === 0) { result.success = false; - return result; + return Promise.resolve(result); } const foldersMap = new Map(); @@ -59,7 +59,7 @@ export class EnpassJsonImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } private processLogin(cipher: CipherView, fields: any[]) { diff --git a/src/importers/firefoxCsvImporter.ts b/src/importers/firefoxCsvImporter.ts index a7d6a5b356..87dd13c35a 100644 --- a/src/importers/firefoxCsvImporter.ts +++ b/src/importers/firefoxCsvImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class FirefoxCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -24,6 +24,6 @@ export class FirefoxCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/fsecureFskImporter.ts b/src/importers/fsecureFskImporter.ts index 81ca6d5f30..1aac8932f0 100644 --- a/src/importers/fsecureFskImporter.ts +++ b/src/importers/fsecureFskImporter.ts @@ -8,12 +8,12 @@ import { CardView } from '../models/view/cardView'; import { CipherType } from '../enums/cipherType'; export class FSecureFskImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = JSON.parse(data); if (results == null || results.data == null) { result.success = false; - return result; + return Promise.resolve(result); } for (const key in results.data) { @@ -55,6 +55,6 @@ export class FSecureFskImporter extends BaseImporter implements Importer { } result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/gnomeJsonImporter.ts b/src/importers/gnomeJsonImporter.ts index be62e8cfbe..23ebfc832b 100644 --- a/src/importers/gnomeJsonImporter.ts +++ b/src/importers/gnomeJsonImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class GnomeJsonImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = JSON.parse(data); if (results == null || Object.keys(results).length === 0) { result.success = false; - return result; + return Promise.resolve(result); } for (const keyRing in results) { @@ -55,6 +55,6 @@ export class GnomeJsonImporter extends BaseImporter implements Importer { } result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/importer.ts b/src/importers/importer.ts index fdd5c0c45d..487dc18468 100644 --- a/src/importers/importer.ts +++ b/src/importers/importer.ts @@ -1,7 +1,6 @@ import { ImportResult } from '../models/domain/importResult'; export interface Importer { - organization: boolean; - - parse(data: string): ImportResult; + organizationId: string; + parse(data: string): Promise; } diff --git a/src/importers/kasperskyTxtImporter.ts b/src/importers/kasperskyTxtImporter.ts index 9c318df611..d01f4ba296 100644 --- a/src/importers/kasperskyTxtImporter.ts +++ b/src/importers/kasperskyTxtImporter.ts @@ -9,7 +9,7 @@ const WebsitesHeader = 'Websites\n\n'; const Delimiter = '\n---\n'; export class KasperskyTxtImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); let notesData: string; @@ -72,7 +72,7 @@ export class KasperskyTxtImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } private parseDataCategory(data: string): Map[] { diff --git a/src/importers/keepass2XmlImporter.ts b/src/importers/keepass2XmlImporter.ts index a485d2265e..604a6f7d26 100644 --- a/src/importers/keepass2XmlImporter.ts +++ b/src/importers/keepass2XmlImporter.ts @@ -10,18 +10,18 @@ import { FolderView } from '../models/view/folderView'; export class KeePass2XmlImporter extends BaseImporter implements Importer { result = new ImportResult(); - parse(data: string): ImportResult { + parse(data: string): Promise { const doc = this.parseXml(data); if (doc == null) { this.result.success = false; - return this.result; + return Promise.resolve(this.result); } const rootGroup = doc.querySelector('KeePassFile > Root > Group'); if (rootGroup == null) { this.result.errorMessage = 'Missing `KeePassFile > Root > Group` node.'; this.result.success = false; - return this.result; + return Promise.resolve(this.result); } this.traverse(rootGroup, true, ''); @@ -31,7 +31,7 @@ export class KeePass2XmlImporter extends BaseImporter implements Importer { } this.result.success = true; - return this.result; + return Promise.resolve(this.result); } traverse(node: Element, isRootNode: boolean, groupPrefixName: string) { diff --git a/src/importers/keepassxCsvImporter.ts b/src/importers/keepassxCsvImporter.ts index 0d07fcc998..69a3d2144e 100644 --- a/src/importers/keepassxCsvImporter.ts +++ b/src/importers/keepassxCsvImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class KeePassXCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -37,6 +37,6 @@ export class KeePassXCsvImporter extends BaseImporter implements Importer { } result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/keeperCsvImporter.ts b/src/importers/keeperCsvImporter.ts index 27a520e52f..68bfd79dbc 100644 --- a/src/importers/keeperCsvImporter.ts +++ b/src/importers/keeperCsvImporter.ts @@ -6,12 +6,12 @@ import { ImportResult } from '../models/domain/importResult'; import { FolderView } from '../models/view/folderView'; export class KeeperCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, false); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -43,6 +43,6 @@ export class KeeperCsvImporter extends BaseImporter implements Importer { } result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/lastpassCsvImporter.ts b/src/importers/lastpassCsvImporter.ts index fdb0da2991..63c76bffce 100644 --- a/src/importers/lastpassCsvImporter.ts +++ b/src/importers/lastpassCsvImporter.ts @@ -14,12 +14,12 @@ import { CipherType } from '../enums/cipherType'; import { SecureNoteType } from '../enums/secureNoteType'; export class LastPassCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value, index) => { @@ -84,7 +84,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { } result.success = true; - return result; + return Promise.resolve(result); } private buildBaseCipher(value: any) { diff --git a/src/importers/logMeOnceCsvImporter.ts b/src/importers/logMeOnceCsvImporter.ts index d7ff04e8a0..8e3542bae5 100644 --- a/src/importers/logMeOnceCsvImporter.ts +++ b/src/importers/logMeOnceCsvImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class LogMeOnceCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, false); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -26,6 +26,6 @@ export class LogMeOnceCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/meldiumCsvImporter.ts b/src/importers/meldiumCsvImporter.ts index f73de0124a..98117758b9 100644 --- a/src/importers/meldiumCsvImporter.ts +++ b/src/importers/meldiumCsvImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class MeldiumCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -24,6 +24,6 @@ export class MeldiumCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/msecureCsvImporter.ts b/src/importers/msecureCsvImporter.ts index d7c14e69cc..e37a76c53e 100644 --- a/src/importers/msecureCsvImporter.ts +++ b/src/importers/msecureCsvImporter.ts @@ -9,12 +9,12 @@ import { SecureNoteType } from '../enums/secureNoteType'; import { SecureNoteView } from '../models/view/secureNoteView'; export class MSecureCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, false); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -57,6 +57,6 @@ export class MSecureCsvImporter extends BaseImporter implements Importer { } result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/mykiCsvImporter.ts b/src/importers/mykiCsvImporter.ts index 47988cc797..365cb2cfef 100644 --- a/src/importers/mykiCsvImporter.ts +++ b/src/importers/mykiCsvImporter.ts @@ -11,12 +11,12 @@ import { SecureNoteView } from '../models/view/secureNoteView'; import { ImportResult } from '../models/domain/importResult'; export class MykiCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -71,6 +71,6 @@ export class MykiCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/onepassword1PifImporter.ts b/src/importers/onepassword1PifImporter.ts index d2aaa5c3db..821b69d45e 100644 --- a/src/importers/onepassword1PifImporter.ts +++ b/src/importers/onepassword1PifImporter.ts @@ -16,7 +16,7 @@ import { SecureNoteType } from '../enums/secureNoteType'; export class OnePassword1PifImporter extends BaseImporter implements Importer { result = new ImportResult(); - parse(data: string): ImportResult { + parse(data: string): Promise { data.split(this.newLineRegex).forEach((line) => { if (this.isNullOrWhitespace(line) || line[0] !== '{') { return; @@ -39,7 +39,7 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { }); this.result.success = true; - return this.result; + return Promise.resolve(this.result); } private processWinOpVaultItem(item: any, cipher: CipherView) { diff --git a/src/importers/onepasswordWinCsvImporter.ts b/src/importers/onepasswordWinCsvImporter.ts index 110268da92..6e0192e697 100644 --- a/src/importers/onepasswordWinCsvImporter.ts +++ b/src/importers/onepasswordWinCsvImporter.ts @@ -9,7 +9,7 @@ import { CardView, IdentityView } from '../models/view'; const IgnoredProperties = ['ainfo', 'autosubmit', 'notesplain', 'ps', 'scope', 'tags', 'title', 'uuid', 'notes']; export class OnePasswordWinCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true, { quoteChar: '"', @@ -17,7 +17,7 @@ export class OnePasswordWinCsvImporter extends BaseImporter implements Importer }); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -155,7 +155,7 @@ export class OnePasswordWinCsvImporter extends BaseImporter implements Importer }); result.success = true; - return result; + return Promise.resolve(result); } private getProp(obj: any, name: string): any { diff --git a/src/importers/padlockCsvImporter.ts b/src/importers/padlockCsvImporter.ts index b85e4113bc..8ec5e8228d 100644 --- a/src/importers/padlockCsvImporter.ts +++ b/src/importers/padlockCsvImporter.ts @@ -7,12 +7,12 @@ import { CollectionView } from '../models/view/collectionView'; import { FolderView } from '../models/view/folderView'; export class PadlockCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, false); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } let headers: string[] = null; @@ -82,6 +82,6 @@ export class PadlockCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/passkeepCsvImporter.ts b/src/importers/passkeepCsvImporter.ts index b51ffdaec5..e1c2873c02 100644 --- a/src/importers/passkeepCsvImporter.ts +++ b/src/importers/passkeepCsvImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class PassKeepCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -30,7 +30,7 @@ export class PassKeepCsvImporter extends BaseImporter implements Importer { } result.success = true; - return result; + return Promise.resolve(result); } private getValue(key: string, value: any) { diff --git a/src/importers/passmanJsonImporter.ts b/src/importers/passmanJsonImporter.ts index 8e2346f0b8..c00eeb1a7f 100644 --- a/src/importers/passmanJsonImporter.ts +++ b/src/importers/passmanJsonImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class PassmanJsonImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = JSON.parse(data); if (results == null || results.length === 0) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((credential: any) => { @@ -56,6 +56,6 @@ export class PassmanJsonImporter extends BaseImporter implements Importer { } result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/passpackCsvImporter.ts b/src/importers/passpackCsvImporter.ts index 9b26d45c2b..3fcdf4120e 100644 --- a/src/importers/passpackCsvImporter.ts +++ b/src/importers/passpackCsvImporter.ts @@ -6,12 +6,12 @@ import { ImportResult } from '../models/domain/importResult'; import { CollectionView } from '../models/view/collectionView'; export class PasspackCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -88,6 +88,6 @@ export class PasspackCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/passwordAgentCsvImporter.ts b/src/importers/passwordAgentCsvImporter.ts index e1d1018368..d14e8036cc 100644 --- a/src/importers/passwordAgentCsvImporter.ts +++ b/src/importers/passwordAgentCsvImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class PasswordAgentCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, false); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } let newVersion = true; @@ -47,6 +47,6 @@ export class PasswordAgentCsvImporter extends BaseImporter implements Importer { } result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/passwordBossJsonImporter.ts b/src/importers/passwordBossJsonImporter.ts index 65c0875570..3edb18edec 100644 --- a/src/importers/passwordBossJsonImporter.ts +++ b/src/importers/passwordBossJsonImporter.ts @@ -9,12 +9,12 @@ import { FolderView } from '../models/view/folderView'; import { CipherType } from '../enums/cipherType'; export class PasswordBossJsonImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = JSON.parse(data); if (results == null || results.items == null) { result.success = false; - return result; + return Promise.resolve(result); } const foldersMap = new Map(); @@ -116,6 +116,6 @@ export class PasswordBossJsonImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/passwordDragonXmlImporter.ts b/src/importers/passwordDragonXmlImporter.ts index 5fc52b524e..9195fca5ba 100644 --- a/src/importers/passwordDragonXmlImporter.ts +++ b/src/importers/passwordDragonXmlImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class PasswordDragonXmlImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const doc = this.parseXml(data); if (doc == null) { result.success = false; - return result; + return Promise.resolve(result); } const records = doc.querySelectorAll('PasswordManager > record'); @@ -52,6 +52,6 @@ export class PasswordDragonXmlImporter extends BaseImporter implements Importer } result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/passwordSafeXmlImporter.ts b/src/importers/passwordSafeXmlImporter.ts index e4bbedb836..e105e186e8 100644 --- a/src/importers/passwordSafeXmlImporter.ts +++ b/src/importers/passwordSafeXmlImporter.ts @@ -4,19 +4,19 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class PasswordSafeXmlImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const doc = this.parseXml(data); if (doc == null) { result.success = false; - return result; + return Promise.resolve(result); } const passwordSafe = doc.querySelector('passwordsafe'); if (passwordSafe == null) { result.errorMessage = 'Missing `passwordsafe` node.'; result.success = false; - return result; + return Promise.resolve(result); } const notesDelimiter = passwordSafe.getAttribute('delimiter'); @@ -57,6 +57,6 @@ export class PasswordSafeXmlImporter extends BaseImporter implements Importer { } result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/passwordWalletTxtImporter.ts b/src/importers/passwordWalletTxtImporter.ts index c98459f354..561085aa04 100644 --- a/src/importers/passwordWalletTxtImporter.ts +++ b/src/importers/passwordWalletTxtImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class PasswordWalletTxtImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, false); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -42,6 +42,6 @@ export class PasswordWalletTxtImporter extends BaseImporter implements Importer } result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/rememBearCsvImporter.ts b/src/importers/rememBearCsvImporter.ts index 9cf559e8d2..bc9aaeb945 100644 --- a/src/importers/rememBearCsvImporter.ts +++ b/src/importers/rememBearCsvImporter.ts @@ -8,12 +8,12 @@ import { ImportResult } from '../models/domain/importResult'; import { CardView } from '../models/view/cardView'; export class RememBearCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -68,6 +68,6 @@ export class RememBearCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/roboformCsvImporter.ts b/src/importers/roboformCsvImporter.ts index 09a97997b3..99c763665c 100644 --- a/src/importers/roboformCsvImporter.ts +++ b/src/importers/roboformCsvImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class RoboFormCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } let i = 1; @@ -58,6 +58,6 @@ export class RoboFormCsvImporter extends BaseImporter implements Importer { } result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/safeInCloudXmlImporter.ts b/src/importers/safeInCloudXmlImporter.ts index 7027ec1bff..ea22fe2aa4 100644 --- a/src/importers/safeInCloudXmlImporter.ts +++ b/src/importers/safeInCloudXmlImporter.ts @@ -10,19 +10,19 @@ import { CipherType } from '../enums/cipherType'; import { SecureNoteType } from '../enums/secureNoteType'; export class SafeInCloudXmlImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const doc = this.parseXml(data); if (doc == null) { result.success = false; - return result; + return Promise.resolve(result); } const db = doc.querySelector('database'); if (db == null) { result.errorMessage = 'Missing `database` node.'; result.success = false; - return result; + return Promise.resolve(result); } const foldersMap = new Map(); @@ -96,6 +96,6 @@ export class SafeInCloudXmlImporter extends BaseImporter implements Importer { } result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/saferpassCsvImport.ts b/src/importers/saferpassCsvImport.ts index e36c3da498..3fdad40b8a 100644 --- a/src/importers/saferpassCsvImport.ts +++ b/src/importers/saferpassCsvImport.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class SaferPassCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -24,6 +24,6 @@ export class SaferPassCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/secureSafeCsvImporter.ts b/src/importers/secureSafeCsvImporter.ts index a0cabb5e21..068c70098e 100644 --- a/src/importers/secureSafeCsvImporter.ts +++ b/src/importers/secureSafeCsvImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class SecureSafeCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -24,6 +24,6 @@ export class SecureSafeCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/splashIdCsvImporter.ts b/src/importers/splashIdCsvImporter.ts index 39dd76aea4..9210824c0a 100644 --- a/src/importers/splashIdCsvImporter.ts +++ b/src/importers/splashIdCsvImporter.ts @@ -5,12 +5,12 @@ import { ImportResult } from '../models/domain/importResult'; import { CipherView } from '../models/view/cipherView'; export class SplashIdCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, false); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -42,7 +42,7 @@ export class SplashIdCsvImporter extends BaseImporter implements Importer { } result.success = true; - return result; + return Promise.resolve(result); } private parseFieldsToNotes(cipher: CipherView, startIndex: number, value: any) { diff --git a/src/importers/stickyPasswordXmlImporter.ts b/src/importers/stickyPasswordXmlImporter.ts index 6bb1dc88ee..3955099255 100644 --- a/src/importers/stickyPasswordXmlImporter.ts +++ b/src/importers/stickyPasswordXmlImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class StickyPasswordXmlImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const doc = this.parseXml(data); if (doc == null) { result.success = false; - return result; + return Promise.resolve(result); } const loginNodes = doc.querySelectorAll('root > Database > Logins > Login'); @@ -62,7 +62,7 @@ export class StickyPasswordXmlImporter extends BaseImporter implements Importer } result.success = true; - return result; + return Promise.resolve(result); } buildGroupText(doc: Document, groupId: string, groupText: string): string { diff --git a/src/importers/truekeyCsvImporter.ts b/src/importers/truekeyCsvImporter.ts index e8d30a88a2..a7d8f8f9c9 100644 --- a/src/importers/truekeyCsvImporter.ts +++ b/src/importers/truekeyCsvImporter.ts @@ -14,12 +14,12 @@ const PropertiesToIgnore = ['kind', 'autologin', 'favorite', 'hexcolor', 'protec ]; export class TrueKeyCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -69,6 +69,6 @@ export class TrueKeyCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/upmCsvImporter.ts b/src/importers/upmCsvImporter.ts index 8b5002cad7..40cb814b79 100644 --- a/src/importers/upmCsvImporter.ts +++ b/src/importers/upmCsvImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class UpmCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, false); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -27,6 +27,6 @@ export class UpmCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/yotiCsvImporter.ts b/src/importers/yotiCsvImporter.ts index 2651e6fa70..abfe0d6c63 100644 --- a/src/importers/yotiCsvImporter.ts +++ b/src/importers/yotiCsvImporter.ts @@ -4,12 +4,12 @@ import { Importer } from './importer'; import { ImportResult } from '../models/domain/importResult'; export class YotiCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -23,6 +23,6 @@ export class YotiCsvImporter extends BaseImporter implements Importer { }); result.success = true; - return result; + return Promise.resolve(result); } } diff --git a/src/importers/zohoVaultCsvImporter.ts b/src/importers/zohoVaultCsvImporter.ts index cf2403e552..4b91e4f5a7 100644 --- a/src/importers/zohoVaultCsvImporter.ts +++ b/src/importers/zohoVaultCsvImporter.ts @@ -5,12 +5,12 @@ import { ImportResult } from '../models/domain/importResult'; import { CipherView } from '../models/view'; export class ZohoVaultCsvImporter extends BaseImporter implements Importer { - parse(data: string): ImportResult { + parse(data: string): Promise { const result = new ImportResult(); const results = this.parseCsv(data, true); if (results == null) { result.success = false; - return result; + return Promise.resolve(result); } results.forEach((value) => { @@ -37,7 +37,7 @@ export class ZohoVaultCsvImporter extends BaseImporter implements Importer { } result.success = true; - return result; + return Promise.resolve(result); } private parseData(cipher: CipherView, data: string) { diff --git a/src/models/export/card.ts b/src/models/export/card.ts index 380f112612..168b3b3ab3 100644 --- a/src/models/export/card.ts +++ b/src/models/export/card.ts @@ -1,6 +1,7 @@ import { CardView } from '../view/cardView'; import { Card as CardDomain } from '../domain/card'; +import { CipherString } from '../domain/cipherString'; export class Card { static template(): Card { @@ -24,6 +25,16 @@ export class Card { return view; } + static toDomain(req: Card, domain = new CardDomain()) { + domain.cardholderName = req.cardholderName != null ? new CipherString(req.cardholderName) : null; + domain.brand = req.brand != null ? new CipherString(req.brand) : null; + domain.number = req.number != null ? new CipherString(req.number) : null; + domain.expMonth = req.expMonth != null ? new CipherString(req.expMonth) : null; + domain.expYear = req.expYear != null ? new CipherString(req.expYear) : null; + domain.code = req.code != null ? new CipherString(req.code) : null; + return domain; + } + cardholderName: string; brand: string; number: string; diff --git a/src/models/export/cipher.ts b/src/models/export/cipher.ts index 5fb2462f25..d88db8f7a4 100644 --- a/src/models/export/cipher.ts +++ b/src/models/export/cipher.ts @@ -3,6 +3,7 @@ import { CipherType } from '../../enums/cipherType'; import { CipherView } from '../view/cipherView'; import { Cipher as CipherDomain } from '../domain/cipher'; +import { CipherString } from '../domain/cipherString'; import { Card } from './card'; import { Field } from './field'; @@ -59,6 +60,38 @@ export class Cipher { return view; } + static toDomain(req: Cipher, domain = new CipherDomain()) { + domain.type = req.type; + domain.folderId = req.folderId; + if (domain.organizationId == null) { + domain.organizationId = req.organizationId; + } + domain.name = req.name != null ? new CipherString(req.name) : null; + domain.notes = req.notes != null ? new CipherString(req.notes) : null; + domain.favorite = req.favorite; + + if (req.fields != null) { + domain.fields = req.fields.map((f) => Field.toDomain(f)); + } + + switch (req.type) { + case CipherType.Login: + domain.login = Login.toDomain(req.login); + break; + case CipherType.SecureNote: + domain.secureNote = SecureNote.toDomain(req.secureNote); + break; + case CipherType.Card: + domain.card = Card.toDomain(req.card); + break; + case CipherType.Identity: + domain.identity = Identity.toDomain(req.identity); + break; + } + + return domain; + } + type: CipherType; folderId: string; organizationId: string; diff --git a/src/models/export/collection.ts b/src/models/export/collection.ts index 03b2a42599..f0efd41fd9 100644 --- a/src/models/export/collection.ts +++ b/src/models/export/collection.ts @@ -1,5 +1,6 @@ import { CollectionView } from '../view/collectionView'; +import { CipherString } from '../domain/cipherString'; import { Collection as CollectionDomain } from '../domain/collection'; export class Collection { @@ -20,6 +21,15 @@ export class Collection { return view; } + static toDomain(req: Collection, domain = new CollectionDomain()) { + domain.name = req.name != null ? new CipherString(req.name) : null; + domain.externalId = req.externalId; + if (domain.organizationId == null) { + domain.organizationId = req.organizationId; + } + return domain; + } + organizationId: string; name: string; externalId: string; diff --git a/src/models/export/field.ts b/src/models/export/field.ts index 3029b2338e..da451e4c68 100644 --- a/src/models/export/field.ts +++ b/src/models/export/field.ts @@ -2,6 +2,7 @@ import { FieldType } from '../../enums/fieldType'; import { FieldView } from '../view/fieldView'; +import { CipherString } from '../domain/cipherString'; import { Field as FieldDomain } from '../domain/field'; export class Field { @@ -20,6 +21,13 @@ export class Field { return view; } + static toDomain(req: Field, domain = new FieldDomain()) { + domain.type = req.type; + domain.value = req.value != null ? new CipherString(req.value) : null; + domain.name = req.name != null ? new CipherString(req.name) : null; + return domain; + } + name: string; value: string; type: FieldType; diff --git a/src/models/export/folder.ts b/src/models/export/folder.ts index b8d6f43977..cc6299300f 100644 --- a/src/models/export/folder.ts +++ b/src/models/export/folder.ts @@ -1,5 +1,6 @@ import { FolderView } from '../view/folderView'; +import { CipherString } from '../domain/cipherString'; import { Folder as FolderDomain } from '../domain/folder'; export class Folder { @@ -14,6 +15,11 @@ export class Folder { return view; } + static toDomain(req: Folder, domain = new FolderDomain()) { + domain.name = req.name != null ? new CipherString(req.name) : null; + return domain; + } + name: string; // Use build method instead of ctor so that we can control order of JSON stringify for pretty print diff --git a/src/models/export/identity.ts b/src/models/export/identity.ts index 20546ac7f0..6bae582cda 100644 --- a/src/models/export/identity.ts +++ b/src/models/export/identity.ts @@ -1,5 +1,6 @@ import { IdentityView } from '../view/identityView'; +import { CipherString } from '../domain/cipherString'; import { Identity as IdentityDomain } from '../domain/identity'; export class Identity { @@ -48,6 +49,28 @@ export class Identity { return view; } + static toDomain(req: Identity, domain = new IdentityDomain()) { + domain.title = req.title != null ? new CipherString(req.title) : null; + domain.firstName = req.firstName != null ? new CipherString(req.firstName) : null; + domain.middleName = req.middleName != null ? new CipherString(req.middleName) : null; + domain.lastName = req.lastName != null ? new CipherString(req.lastName) : null; + domain.address1 = req.address1 != null ? new CipherString(req.address1) : null; + domain.address2 = req.address2 != null ? new CipherString(req.address2) : null; + domain.address3 = req.address3 != null ? new CipherString(req.address3) : null; + domain.city = req.city != null ? new CipherString(req.city) : null; + domain.state = req.state != null ? new CipherString(req.state) : null; + domain.postalCode = req.postalCode != null ? new CipherString(req.postalCode) : null; + domain.country = req.country != null ? new CipherString(req.country) : null; + domain.company = req.company != null ? new CipherString(req.company) : null; + domain.email = req.email != null ? new CipherString(req.email) : null; + domain.phone = req.phone != null ? new CipherString(req.phone) : null; + domain.ssn = req.ssn != null ? new CipherString(req.ssn) : null; + domain.username = req.username != null ? new CipherString(req.username) : null; + domain.passportNumber = req.passportNumber != null ? new CipherString(req.passportNumber) : null; + domain.licenseNumber = req.licenseNumber != null ? new CipherString(req.licenseNumber) : null; + return domain; + } + title: string; firstName: string; middleName: string; diff --git a/src/models/export/login.ts b/src/models/export/login.ts index d8ad6918ab..4bf487b6f1 100644 --- a/src/models/export/login.ts +++ b/src/models/export/login.ts @@ -2,6 +2,7 @@ import { LoginUri } from './loginUri'; import { LoginView } from '../view/loginView'; +import { CipherString } from '../domain/cipherString'; import { Login as LoginDomain } from '../domain/login'; export class Login { @@ -24,6 +25,16 @@ export class Login { return view; } + static toDomain(req: Login, domain = new LoginDomain()) { + if (req.uris != null) { + domain.uris = req.uris.map((u) => LoginUri.toDomain(u)); + } + domain.username = req.username != null ? new CipherString(req.username) : null; + domain.password = req.password != null ? new CipherString(req.password) : null; + domain.totp = req.totp != null ? new CipherString(req.totp) : null; + return domain; + } + uris: LoginUri[]; username: string; password: string; diff --git a/src/models/export/loginUri.ts b/src/models/export/loginUri.ts index 3b416d9fdd..c567b670a3 100644 --- a/src/models/export/loginUri.ts +++ b/src/models/export/loginUri.ts @@ -2,6 +2,7 @@ import { UriMatchType } from '../../enums/uriMatchType'; import { LoginUriView } from '../view/loginUriView'; +import { CipherString } from '../domain/cipherString'; import { LoginUri as LoginUriDomain } from '../domain/loginUri'; export class LoginUri { @@ -18,6 +19,12 @@ export class LoginUri { return view; } + static toDomain(req: LoginUri, domain = new LoginUriDomain()) { + domain.uri = req.uri != null ? new CipherString(req.uri) : null; + domain.match = req.match; + return domain; + } + uri: string; match: UriMatchType = null; diff --git a/src/models/export/secureNote.ts b/src/models/export/secureNote.ts index 31a80c04df..078f40331e 100644 --- a/src/models/export/secureNote.ts +++ b/src/models/export/secureNote.ts @@ -16,6 +16,11 @@ export class SecureNote { return view; } + static toDomain(req: SecureNote, view = new SecureNoteDomain()) { + view.type = req.type; + return view; + } + type: SecureNoteType; constructor(o?: SecureNoteView | SecureNoteDomain) { diff --git a/src/services/export.service.ts b/src/services/export.service.ts index 03c499d9b1..e0c4188e87 100644 --- a/src/services/export.service.ts +++ b/src/services/export.service.ts @@ -98,6 +98,7 @@ export class ExportService implements ExportServiceAbstraction { return papa.unparse(exportCiphers); } else { const jsonDoc: any = { + encrypted: false, folders: [], items: [], }; @@ -130,6 +131,7 @@ export class ExportService implements ExportServiceAbstraction { const ciphers = await this.cipherService.getAll(); const jsonDoc: any = { + encrypted: true, folders: [], items: [], }; @@ -215,6 +217,7 @@ export class ExportService implements ExportServiceAbstraction { return papa.unparse(exportCiphers); } else { const jsonDoc: any = { + encrypted: false, collections: [], items: [], }; @@ -264,6 +267,7 @@ export class ExportService implements ExportServiceAbstraction { await Promise.all(promises); const jsonDoc: any = { + encrypted: true, collections: [], items: [], }; diff --git a/src/services/import.service.ts b/src/services/import.service.ts index a74fc2c62d..3b2d4ed444 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -165,12 +165,12 @@ export class ImportService implements ImportServiceAbstraction { } } - getImporter(format: string, organization = false): Importer { + getImporter(format: string, organizationId: string = null): Importer { const importer = this.getImporterInstance(format); if (importer == null) { return null; } - importer.organization = organization; + importer.organizationId = organizationId; return importer; } From 2d62e10d988633b7cd1c58af7e450cfa2150070b Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Tue, 8 Dec 2020 10:10:15 -0600 Subject: [PATCH 1115/1626] [Policy] Personal Ownership (#213) * Initial commit of personal ownership enforcement * Updated policy type enum * Sync'd eventType for Policy_Updated with server enum value * Added policyId to eventResponse model * Removed explicit typing --- src/angular/components/add-edit.component.ts | 26 +++++++++++++++++++- src/enums/eventType.ts | 4 +-- src/enums/policyType.ts | 1 + src/models/response/eventResponse.ts | 2 ++ 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 16c0f87540..09cdf1343b 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -14,6 +14,7 @@ import { CipherType } from '../../enums/cipherType'; import { EventType } from '../../enums/eventType'; import { FieldType } from '../../enums/fieldType'; import { OrganizationUserStatusType } from '../../enums/organizationUserStatusType'; +import { PolicyType } from '../../enums/policyType'; import { SecureNoteType } from '../../enums/secureNoteType'; import { UriMatchType } from '../../enums/uriMatchType'; @@ -25,6 +26,7 @@ import { FolderService } from '../../abstractions/folder.service'; import { I18nService } from '../../abstractions/i18n.service'; import { MessagingService } from '../../abstractions/messaging.service'; import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { PolicyService } from '../../abstractions/policy.service'; import { StateService } from '../../abstractions/state.service'; import { UserService } from '../../abstractions/user.service'; @@ -81,6 +83,7 @@ export class AddEditComponent implements OnInit { uriMatchOptions: any[]; ownershipOptions: any[] = []; currentDate = new Date(); + allowPersonal = true; protected writeableCollections: CollectionView[]; private previousCipherId: string; @@ -89,7 +92,8 @@ export class AddEditComponent implements OnInit { protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected auditService: AuditService, protected stateService: StateService, protected userService: UserService, protected collectionService: CollectionService, - protected messagingService: MessagingService, protected eventService: EventService) { + protected messagingService: MessagingService, protected eventService: EventService, + protected policyService: PolicyService) { this.typeOptions = [ { name: i18nService.t('typeLogin'), value: CipherType.Login }, { name: i18nService.t('typeCard'), value: CipherType.Card }, @@ -151,12 +155,26 @@ export class AddEditComponent implements OnInit { } async init() { + const policies = await this.policyService.getAll(PolicyType.PersonalOwnership); const myEmail = await this.userService.getEmail(); this.ownershipOptions.push({ name: myEmail, value: null }); const orgs = await this.userService.getAllOrganizations(); orgs.sort(Utils.getSortFunction(this.i18nService, 'name')).forEach((o) => { if (o.enabled && o.status === OrganizationUserStatusType.Confirmed) { this.ownershipOptions.push({ name: o.name, value: o.id }); + if (policies != null && o.usePolicies && !o.isAdmin && this.allowPersonal) { + for (const policy of policies) { + if (policy.organizationId === o.id && policy.enabled) { + this.allowPersonal = false; + this.ownershipOptions.splice(0, 1); + // Default to the organization who owns this policy for now (if necessary) + if (this.organizationId == null) { + this.organizationId = o.id; + } + break; + } + } + } } }); this.writeableCollections = await this.loadCollections(); @@ -236,6 +254,12 @@ export class AddEditComponent implements OnInit { return false; } + if ((!this.editMode || this.cloneMode) && !this.allowPersonal && this.cipher.organizationId == null) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('personalOwnershipSubmitError')); + return false; + } + if ((!this.editMode || this.cloneMode) && this.cipher.type === CipherType.Login && this.cipher.login.uris != null && this.cipher.login.uris.length === 1 && (this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === '')) { diff --git a/src/enums/eventType.ts b/src/enums/eventType.ts index 40f626e751..154a363bcb 100644 --- a/src/enums/eventType.ts +++ b/src/enums/eventType.ts @@ -44,7 +44,5 @@ export enum EventType { Organization_PurgedVault = 1601, // Organization_ClientExportedVault = 1602, - Policy_Created = 1700, - Policy_Updated = 1701, - Policy_Deleted = 1702, + Policy_Updated = 1700, } diff --git a/src/enums/policyType.ts b/src/enums/policyType.ts index 31285a5da2..a01fa7df47 100644 --- a/src/enums/policyType.ts +++ b/src/enums/policyType.ts @@ -4,4 +4,5 @@ export enum PolicyType { PasswordGenerator = 2, // Sets minimum requirements/default type for generated passwords/passphrases SingleOrg = 3, // Allows users to only be apart of one organization RequireSso = 4, // Requires users to authenticate with SSO + PersonalOwnership = 5, // Disables personal vault ownership for adding/cloning items } diff --git a/src/models/response/eventResponse.ts b/src/models/response/eventResponse.ts index e3cfa93323..770133720e 100644 --- a/src/models/response/eventResponse.ts +++ b/src/models/response/eventResponse.ts @@ -10,6 +10,7 @@ export class EventResponse extends BaseResponse { cipherId: string; collectionId: string; groupId: string; + policyId: string; organizationUserId: string; actingUserId: string; date: string; @@ -24,6 +25,7 @@ export class EventResponse extends BaseResponse { this.cipherId = this.getResponseProperty('CipherId'); this.collectionId = this.getResponseProperty('CollectionId'); this.groupId = this.getResponseProperty('GroupId'); + this.policyId = this.getResponseProperty('PolicyId'); this.organizationUserId = this.getResponseProperty('OrganizationUserId'); this.actingUserId = this.getResponseProperty('ActingUserId'); this.date = this.getResponseProperty('Date'); From 72bf18f369068d36767794bdc0ca377f734cf373 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 8 Dec 2020 11:29:57 -0600 Subject: [PATCH 1116/1626] Fix 1password importer (#222) * Change cipher type based on csv type header * Test identity and credit card import * Do not use node 'fs' module Karma is being used for automated tests so node modules are not available * WIP: mac and windows 1password importer split Need to improve windows field identification to limit secret data exposure and improve user experience * Hide fields with likely secret values Co-authored-by: Matt Gibson --- .../importers/onepassword1PifImporter.spec.ts | 2 +- .../importers/onepasswordCsvImporter.spec.ts | 50 --- .../onepasswordMacCsvImporter.spec.ts | 71 +++++ .../onepasswordWinCsvImporter.spec.ts | 81 +++++ ...reditCard.csv.ts => creditCard.mac.csv.ts} | 2 +- .../onePasswordCsv/creditCard.windows.csv.ts | 2 + .../{identity.csv.ts => identity.mac.csv.ts} | 2 +- .../onePasswordCsv/identity.windows.csv.ts | 2 + .../onePasswordCsv/multipleItems.mac.csv.ts | 14 + .../multipleItems.windows.csv.ts | 5 + .../nodeCryptoFunction.service.spec.ts | 2 +- .../webCryptoFunction.service.spec.ts | 2 +- src/abstractions/cryptoFunction.service.ts | 2 +- .../components/two-factor.component.ts | 2 +- src/importers/baseImporter.ts | 2 +- .../cipherImportContext.ts | 8 + .../onepassword1PifImporter.ts | 22 +- .../onepasswordCsvImporter.ts | 288 ++++++++++++++++++ .../onepasswordMacCsvImporter.ts | 28 ++ .../onepasswordWinCsvImporter.ts | 53 ++++ src/importers/onepasswordWinCsvImporter.ts | 168 ---------- src/misc/utils.ts | 2 +- src/models/request/sendRequest.ts | 2 +- src/models/request/tokenRequest.ts | 4 +- src/services/auth.service.ts | 2 +- src/services/import.service.ts | 8 +- tslint.json | 6 +- 27 files changed, 587 insertions(+), 245 deletions(-) delete mode 100644 spec/common/importers/onepasswordCsvImporter.spec.ts create mode 100644 spec/common/importers/onepasswordMacCsvImporter.spec.ts create mode 100644 spec/common/importers/onepasswordWinCsvImporter.spec.ts rename spec/common/importers/testData/onePasswordCsv/{creditCard.csv.ts => creditCard.mac.csv.ts} (98%) create mode 100644 spec/common/importers/testData/onePasswordCsv/creditCard.windows.csv.ts rename spec/common/importers/testData/onePasswordCsv/{identity.csv.ts => identity.mac.csv.ts} (99%) create mode 100644 spec/common/importers/testData/onePasswordCsv/identity.windows.csv.ts create mode 100644 spec/common/importers/testData/onePasswordCsv/multipleItems.mac.csv.ts create mode 100644 spec/common/importers/testData/onePasswordCsv/multipleItems.windows.csv.ts create mode 100644 src/importers/onepasswordImporters/cipherImportContext.ts rename src/importers/{ => onepasswordImporters}/onepassword1PifImporter.ts (94%) create mode 100644 src/importers/onepasswordImporters/onepasswordCsvImporter.ts create mode 100644 src/importers/onepasswordImporters/onepasswordMacCsvImporter.ts create mode 100644 src/importers/onepasswordImporters/onepasswordWinCsvImporter.ts delete mode 100644 src/importers/onepasswordWinCsvImporter.ts diff --git a/spec/common/importers/onepassword1PifImporter.spec.ts b/spec/common/importers/onepassword1PifImporter.spec.ts index 98158fdc6d..b06f32b0a4 100644 --- a/spec/common/importers/onepassword1PifImporter.spec.ts +++ b/spec/common/importers/onepassword1PifImporter.spec.ts @@ -1,5 +1,5 @@ import { FieldType } from '../../../src/enums/fieldType'; -import { OnePassword1PifImporter as Importer } from '../../../src/importers/onepassword1PifImporter'; +import { OnePassword1PifImporter as Importer } from '../../../src/importers/onepasswordImporters/onepassword1PifImporter'; import { Utils } from '../../../src/misc/utils'; diff --git a/spec/common/importers/onepasswordCsvImporter.spec.ts b/spec/common/importers/onepasswordCsvImporter.spec.ts deleted file mode 100644 index 8ca37cef59..0000000000 --- a/spec/common/importers/onepasswordCsvImporter.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { OnePasswordWinCsvImporter as Importer } from '../../../src/importers/onepasswordWinCsvImporter'; - -import { CipherType } from '../../../src/enums'; - -import { data as creditCardData } from './testData/onePasswordCsv/creditCard.csv' -import { data as identityData } from './testData/onePasswordCsv/identity.csv' - -describe('1Password CSV Importer', () => { - it('should parse identity imports', async () => { - const importer = new Importer(); - const result = await importer.parse(identityData); - - expect(result).not.toBeNull(); - expect(result.success).toBe(true); - expect(result.ciphers.length).toBe(1); - const cipher = result.ciphers[0]; - expect(cipher.type).toBe(CipherType.Identity) - - expect(cipher.identity).toEqual(jasmine.objectContaining({ - firstName: 'first name', - middleName: 'mi', - lastName: 'last name', - username: 'userNam3', - company: 'bitwarden', - phone: '8005555555', - email: 'email@bitwarden.com' - })); - - expect(cipher.notes).toContain('address\ncity state zip\nUnited States'); - }); - - it('should parse credit card imports', async () => { - const importer = new Importer(); - const result = await importer.parse(creditCardData); - - expect(result).not.toBeNull(); - expect(result.success).toBe(true); - expect(result.ciphers.length).toBe(1); - const cipher = result.ciphers[0]; - expect(cipher.type).toBe(CipherType.Card); - - expect(cipher.card).toEqual(jasmine.objectContaining({ - number: '4111111111111111', - code: '111', - cardholderName: 'test', - expMonth: '1', - expYear: '2030', - })); - }); -}); diff --git a/spec/common/importers/onepasswordMacCsvImporter.spec.ts b/spec/common/importers/onepasswordMacCsvImporter.spec.ts new file mode 100644 index 0000000000..4cc270f492 --- /dev/null +++ b/spec/common/importers/onepasswordMacCsvImporter.spec.ts @@ -0,0 +1,71 @@ +import { OnePasswordMacCsvImporter as Importer } from '../../../src/importers/onepasswordImporters/onepasswordMacCsvImporter'; + +import { CipherType } from '../../../src/enums'; +import { CipherView } from '../../../src/models/view/cipherView'; + +import { data as creditCardData } from './testData/onePasswordCsv/creditCard.mac.csv'; +import { data as identityData } from './testData/onePasswordCsv/identity.mac.csv'; +import { data as multiTypeData } from './testData/onePasswordCsv/multipleItems.mac.csv'; + +function expectIdentity(cipher: CipherView) { + expect(cipher.type).toBe(CipherType.Identity); + + expect(cipher.identity).toEqual(jasmine.objectContaining({ + firstName: 'first name', + middleName: 'mi', + lastName: 'last name', + username: 'userNam3', + company: 'bitwarden', + phone: '8005555555', + email: 'email@bitwarden.com' + })); + + expect(cipher.notes).toContain('address\ncity state zip\nUnited States'); +} + +function expectCreditCard(cipher: CipherView) { + expect(cipher.type).toBe(CipherType.Card); + + expect(cipher.card).toEqual(jasmine.objectContaining({ + number: '4111111111111111', + code: '111', + cardholderName: 'test', + expMonth: '1', + expYear: '2030', + })); +} + +describe('1Password mac CSV Importer', () => { + it('should parse identity records', async () => { + const importer = new Importer(); + const result = await importer.parse(identityData); + + expect(result).not.toBeNull(); + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(1); + const cipher = result.ciphers[0]; + expectIdentity(cipher); + }); + + it('should parse credit card records', async () => { + const importer = new Importer(); + const result = await importer.parse(creditCardData); + + expect(result).not.toBeNull(); + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(1); + const cipher = result.ciphers[0]; + expectCreditCard(cipher); + }); + + it('should parse csv\'s with multiple record type', async () => { + const importer = new Importer(); + const result = await importer.parse(multiTypeData); + + expect(result).not.toBeNull(); + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(4); + expectIdentity(result.ciphers[1]); + expectCreditCard(result.ciphers[2]); + }); +}); diff --git a/spec/common/importers/onepasswordWinCsvImporter.spec.ts b/spec/common/importers/onepasswordWinCsvImporter.spec.ts new file mode 100644 index 0000000000..a456d0bbd1 --- /dev/null +++ b/spec/common/importers/onepasswordWinCsvImporter.spec.ts @@ -0,0 +1,81 @@ +import { OnePasswordWinCsvImporter as Importer } from '../../../src/importers/onepasswordImporters/onepasswordWinCsvImporter'; + +import { CipherType, FieldType } from '../../../src/enums'; +import { CipherView } from '../../../src/models/view/cipherView'; +import { FieldView } from '../../../src/models/view/fieldView'; + +import { data as creditCardData } from './testData/onePasswordCsv/creditCard.windows.csv'; +import { data as identityData } from './testData/onePasswordCsv/identity.windows.csv'; +import { data as multiTypeData } from './testData/onePasswordCsv/multipleItems.windows.csv'; + +function expectIdentity(cipher: CipherView) { + expect(cipher.type).toBe(CipherType.Identity); + + expect(cipher.identity).toEqual(jasmine.objectContaining({ + firstName: 'first name', + middleName: 'mi', + lastName: 'last name', + username: 'userNam3', + company: 'bitwarden', + phone: '8005555555', + email: 'email@bitwarden.com' + })); + + expect(cipher.fields).toEqual(jasmine.arrayContaining([ + Object.assign(new FieldView(), { + type: FieldType.Text, + name: 'address', + value: 'address city state zip us' + }) + ])); +} + +function expectCreditCard(cipher: CipherView) { + expect(cipher.type).toBe(CipherType.Card); + + expect(cipher.card).toEqual(jasmine.objectContaining({ + number: '4111111111111111', + code: '111', + cardholderName: 'test', + expMonth: '1', + expYear: '1970', + })); +} + +describe('1Password windows CSV Importer', () => { + let importer: Importer; + beforeEach(() => { + importer = new Importer(); + }); + + it('should parse identity records', async () => { + const result = await importer.parse(identityData); + + expect(result).not.toBeNull(); + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(1); + const cipher = result.ciphers[0]; + expectIdentity(cipher); + }); + + it('should parse credit card records', async () => { + const result = await importer.parse(creditCardData); + + expect(result).not.toBeNull(); + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(1); + const cipher = result.ciphers[0]; + expectCreditCard(cipher); + }); + + it('should parse csv\'s with multiple record types', async () => { + const result = await importer.parse(multiTypeData); + + expect(result).not.toBeNull(); + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(4); + + expectIdentity(result.ciphers[1]); + expectCreditCard(result.ciphers[2]); + }); +}); diff --git a/spec/common/importers/testData/onePasswordCsv/creditCard.csv.ts b/spec/common/importers/testData/onePasswordCsv/creditCard.mac.csv.ts similarity index 98% rename from spec/common/importers/testData/onePasswordCsv/creditCard.csv.ts rename to spec/common/importers/testData/onePasswordCsv/creditCard.mac.csv.ts index 47c05222a5..3062c06752 100644 --- a/spec/common/importers/testData/onePasswordCsv/creditCard.csv.ts +++ b/spec/common/importers/testData/onePasswordCsv/creditCard.mac.csv.ts @@ -1,3 +1,3 @@ export const data = `"account number(accountNo)","address(address)","address(branchAddress)","admin console URL(admin_console_url)","admin console username(admin_console_username)","AirPort ID(airport_id)","alias(alias)","AOL/AIM(aim)","approved wildlife(game)","attached storage password(disk_password)","auth​ method(pop_authentication)","auth​ method(smtp_authentication)","bank name(bankName)","base station name(name)","base station password(password)","birth date(birthdate)","business(busphone)","cardholder name(cardholder)","cash withdrawal limit(cashLimit)","cell(cellphone)","company name(company_name)","company(company)","conditions / restrictions(conditions)","connection options(options)","console password(admin_console_password)","country(country)","Created Date","credit limit(creditLimit)","customer service phone(customer_service_phone)","database(database)","date of birth(birthdate)","default phone(defphone)","department(department)","download page(download_link)","email(email)","expires(expires)","expiry date(expiry_date)","expiry date(expiry)","first name(firstname)","forum signature(forumsig)","full name(fullname)","full name(name)","group(org_name)","height(height)","home(homephone)","IBAN(iban)","ICQ(icq)","initial(initial)","interest rate(interest)","issue number(issuenumber)","issued on(issue_date)","issuing authority(issuing_authority)","issuing bank(bank)","issuing country(issuing_country)","job title(jobtitle)","last name(lastname)","license class(class)","license key(reg_code)","licensed to(reg_name)","maximum quota(quota)","member ID (additional)(additional_no)","member ID(membership_no)","member name(member_name)","member since(member_since)","Modified Date","MSN(msn)","name on account(owner)","name(name)","nationality(nationality)","network name(network_name)","Notes","number(ccnum)","number(number)","occupation(occupation)","order number(order_number)","order total(order_total)","Password","password(password)","password(pop_password)","password(smtp_password)","phone (intl)(phoneIntl)","phone (local)(phone_local)","phone (local)(phoneLocal)","phone (toll free)(phone_tollfree)","phone (toll free)(phoneTollFree)","phone for reserva​tions(reservations_phone)","phone(branchPhone)","PIN(pin)","PIN(telephonePin)","place of birth(birthplace)","port number(pop_port)","port number(smtp_port)","port(port)","provider's website(provider_website)","provider(provider)","publisher(publisher_name)","purchase date(order_date)","registered email(reg_email)","reminder answer(remindera)","reminder question(reminderq)","retail price(retail_price)","routing number(routingNo)","Scope","security(pop_security)","security(smtp_security)","server / IP address(server)","server(hostname)","server(pop_server)","sex(sex)","SID(sid)","skype(skype)","SMTP server(smtp_server)","state(state)","support email(support_email)","support phone(support_contact_phone)","support URL(support_contact_url)","SWIFT(swift)","Tags","telephone(phone)","Title","Type","type(accountType)","type(database_type)","type(pop_type)","type(type)","URL","URL(url)","Username","username(pop_username)","username(smtp_username)","username(username)","valid from(valid_from)","valid from(validFrom)","verification number(cvv)","version(product_version)","website(publisher_website)","website(website)","wireless network password(wireless_password)","wireless security(wireless_security)","Yahoo(yahoo)", ,,,,,,,,,,,,,,,,,"test",,,,,,,,,"1606923869",,,,,,,,,,,"01/2030",,,,,,,,,,,,,,,,,,,,,,,,,,,"1606924056",,,,,,"","4111111111111111",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"{( -)}",,"test card","Credit Card",,,,"laser",,,,,,,,,"111",,,,,,,` +)}",,"test card","Credit Card",,,,"laser",,,,,,,,,"111",,,,,,,`; diff --git a/spec/common/importers/testData/onePasswordCsv/creditCard.windows.csv.ts b/spec/common/importers/testData/onePasswordCsv/creditCard.windows.csv.ts new file mode 100644 index 0000000000..f9dd98081b --- /dev/null +++ b/spec/common/importers/testData/onePasswordCsv/creditCard.windows.csv.ts @@ -0,0 +1,2 @@ +export const data = `"UUID","TITLE","SCOPE","AUTOSUBMIT","1: CARDHOLDER NAME","2: NUMBER","3: VERIFICATION NUMBER","4: EXPIRY DATE","SECTION 2: SECTION_PZET7LEKRQXZUINIEGH5ABA2UY","SECTION_PZET7LEKRQXZUINIEGH5ABA2UY 1: LABEL" +"sd26pt226etnsijbl3kqzi5bmm","test card","Default","Default","test","4111111111111111","111","1/3/1970 12:23 AM","section","field (phone)"`; diff --git a/spec/common/importers/testData/onePasswordCsv/identity.csv.ts b/spec/common/importers/testData/onePasswordCsv/identity.mac.csv.ts similarity index 99% rename from spec/common/importers/testData/onePasswordCsv/identity.csv.ts rename to spec/common/importers/testData/onePasswordCsv/identity.mac.csv.ts index 2b74cd8d8a..e02aed520f 100644 --- a/spec/common/importers/testData/onePasswordCsv/identity.csv.ts +++ b/spec/common/importers/testData/onePasswordCsv/identity.mac.csv.ts @@ -3,4 +3,4 @@ export const data = `"account number(accountNo)","address(address)","address(bra city state zip United States",,,,,,"",,,,,,,,"12/2/20","",,,"",,"bitwarden",,,,,"1606923754",,,,"12/2/20","8005555555","department",,"email@bitwarden.com",,,,"first name","",,,,,"",,"","mi",,,,,,,"job title","last name",,,,,,,,,"1607020883","",,,,,"It’s you! 🖐 Select Edit to fill in more details, like your address and contact information.",,,"occupation",,,,,,,,,,,,,,,,,,,,,,,,,"","",,,,,,,,,"",,"",,,,,,,"{( \\"Starter Kit\\" -)}",,"Identity Item","Identity",,,,,,,"userNam3",,,"userNam3",,,,,,"",,,"",` +)}",,"Identity Item","Identity",,,,,,,"userNam3",,,"userNam3",,,,,,"",,,"",`; diff --git a/spec/common/importers/testData/onePasswordCsv/identity.windows.csv.ts b/spec/common/importers/testData/onePasswordCsv/identity.windows.csv.ts new file mode 100644 index 0000000000..3e198e17db --- /dev/null +++ b/spec/common/importers/testData/onePasswordCsv/identity.windows.csv.ts @@ -0,0 +1,2 @@ +export const data = `"UUID","TITLE","SCOPE","AUTOSUBMIT","TAGS","NOTES","SECTION 1: NAME","NAME 1: FIRST NAME","NAME 2: INITIAL","NAME 3: LAST NAME","NAME 4: BIRTH DATE","NAME 5: OCCUPATION","NAME 6: COMPANY","NAME 7: DEPARTMENT","NAME 8: JOB TITLE","SECTION 2: ADDRESS","ADDRESS 1: ADDRESS","ADDRESS 2: DEFAULT PHONE","SECTION 3: INTERNET","INTERNET 1: USERNAME","INTERNET 2: EMAIL","SECTION 4: MFJQKMWEOYDZDFH4YMR7WLJKIY","MFJQKMWEOYDZDFH4YMR7WLJKIY 1: SECTION FIELD","MFJQKMWEOYDZDFH4YMR7WLJKIY 2: SECTION FIELD" +"6v56y5z4tejwg37jsettta7d7m","Identity Item","Default","Default","Starter Kit","It’s you! 🖐 Select Edit to fill in more details, like your address and contact information.","Identification","first name","mi","last name","12/2/2020 4:01 AM","occupation","bitwarden","department","job title","Address","address city state zip us","8005555555","Internet Details","userNam3","email@bitwarden.com","💡 Did you know?","1Password can fill names and addresses into webpages:","https://support.1password.com/credit-card-address-filling/"`; diff --git a/spec/common/importers/testData/onePasswordCsv/multipleItems.mac.csv.ts b/spec/common/importers/testData/onePasswordCsv/multipleItems.mac.csv.ts new file mode 100644 index 0000000000..e994b1afda --- /dev/null +++ b/spec/common/importers/testData/onePasswordCsv/multipleItems.mac.csv.ts @@ -0,0 +1,14 @@ +export const data = `"account number(accountNo)","address(address)","address(branchAddress)","admin console URL(admin_console_url)","admin console username(admin_console_username)","AirPort ID(airport_id)","alias(alias)","AOL/AIM(aim)","approved wildlife(game)","attached storage password(disk_password)","auth​ method(pop_authentication)","auth​ method(smtp_authentication)","bank name(bankName)","base station name(name)","base station password(password)","birth date(birthdate)","business(busphone)","cardholder name(cardholder)","cash withdrawal limit(cashLimit)","cell(cellphone)","company name(company_name)","company(company)","conditions / restrictions(conditions)","connection options(options)","console password(admin_console_password)","country(country)","Created Date","credit limit(creditLimit)","customer service phone(customer_service_phone)","database(database)","date of birth(birthdate)","default phone(defphone)","department(department)","download page(download_link)","email(email)","expires(expires)","expiry date(expiry_date)","expiry date(expiry)","first name(firstname)","forum signature(forumsig)","full name(fullname)","full name(name)","group(org_name)","height(height)","home(homephone)","IBAN(iban)","ICQ(icq)","initial(initial)","interest rate(interest)","issue number(issuenumber)","issued on(issue_date)","issuing authority(issuing_authority)","issuing bank(bank)","issuing country(issuing_country)","job title(jobtitle)","last name(lastname)","license class(class)","license key(reg_code)","licensed to(reg_name)","maximum quota(quota)","member ID (additional)(additional_no)","member ID(membership_no)","member name(member_name)","member since(member_since)","Modified Date","MSN(msn)","name on account(owner)","name(name)","nationality(nationality)","network name(network_name)","Notes","number(ccnum)","number(number)","occupation(occupation)","order number(order_number)","order total(order_total)","Password","password(password)","password(pop_password)","password(smtp_password)","phone (intl)(phoneIntl)","phone (local)(phone_local)","phone (local)(phoneLocal)","phone (toll free)(phone_tollfree)","phone (toll free)(phoneTollFree)","phone for reserva​tions(reservations_phone)","phone(branchPhone)","PIN(pin)","PIN(telephonePin)","place of birth(birthplace)","port number(pop_port)","port number(smtp_port)","port(port)","provider's website(provider_website)","provider(provider)","publisher(publisher_name)","purchase date(order_date)","registered email(reg_email)","reminder answer(remindera)","reminder question(reminderq)","retail price(retail_price)","routing number(routingNo)","Scope","security(pop_security)","security(smtp_security)","server / IP address(server)","server(hostname)","server(pop_server)","sex(sex)","SID(sid)","skype(skype)","SMTP server(smtp_server)","state(state)","support email(support_email)","support phone(support_contact_phone)","support URL(support_contact_url)","SWIFT(swift)","Tags","telephone(phone)","Title","Type","type(accountType)","type(database_type)","type(pop_type)","type(type)","URL","URL(url)","Username","username(pop_username)","username(smtp_username)","username(username)","valid from(valid_from)","valid from(validFrom)","verification number(cvv)","version(product_version)","website(publisher_website)","website(website)","wireless network password(wireless_password)","wireless security(wireless_security)","Yahoo(yahoo)", +,,,,,,,,,,,,,,,,,,,,,,,,,,"1606923754",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"1606923754",,,,,,"Follow these steps to get started.",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"{( + \\"Starter Kit\\" +)}",,"🎉 Welcome to 1Password!","Secure Note",,,,,,,,,,,,,,,,,,,, +,"address +city state zip +United States",,,,,,,,,,,,,,"12/2/20",,,,,,"bitwarden",,,,,"1606923754",,,,"12/2/20","8005555555","department",,"email@bitwarden.com",,,,"first name",,,,,,,,,"mi",,,,,,,"job title","last name",,,,,,,,,"1607390191",,,,,,"It’s you! 🖐 Select Edit to fill in more details, like your address and contact information.",,,"occupation",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"{( + \\"Starter Kit\\" +)}",,"Identity Item","Identity",,,,,,,"userNam3",,,"userNam3",,,,,,,,,, +,,,,,,,,,,,,,,,,,"test",,,,,,,,,"1606923869",,,,,,,,,,,"01/2030",,,,,,,,,,,,,,,,,,,,,,,,,,,"1607355631",,,,,,"","4111111111111111",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"{( +)}",,"test card","Credit Card",,,,"laser",,,,,,,,,"111",,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,"1606923754",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"1607020972",,,,,,"You can use this login to sign in to your account on 1password.com.",,,,,,"the account's password",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"{( + \\"Starter Kit\\" +)}",,"1Password Account","Login",,,,,"https://my.1password.com",,"email@bitwarden.com",,,,,,,,,,,,,`; diff --git a/spec/common/importers/testData/onePasswordCsv/multipleItems.windows.csv.ts b/spec/common/importers/testData/onePasswordCsv/multipleItems.windows.csv.ts new file mode 100644 index 0000000000..918b7a52e9 --- /dev/null +++ b/spec/common/importers/testData/onePasswordCsv/multipleItems.windows.csv.ts @@ -0,0 +1,5 @@ +export const data = `"UUID","TITLE","USERNAME","PASSWORD","URL","URLS","EMAIL","MASTER-PASSWORD","ACCOUNT-KEY","SCOPE","AUTOSUBMIT","TAGS","NOTES","SECTION 1: WXHDKEQREE3TH6QRFCPFPSD3AE","WXHDKEQREE3TH6QRFCPFPSD3AE 1: SECRET KEY","SECTION 1: NAME","NAME 1: FIRST NAME","NAME 2: INITIAL","NAME 3: LAST NAME","NAME 4: BIRTH DATE","NAME 5: OCCUPATION","NAME 6: COMPANY","NAME 7: DEPARTMENT","NAME 8: JOB TITLE","SECTION 2: ADDRESS","ADDRESS 1: ADDRESS","ADDRESS 2: DEFAULT PHONE","SECTION 3: INTERNET","INTERNET 1: USERNAME","INTERNET 2: EMAIL","SECTION 4: MFJQKMWEOYDZDFH4YMR7WLJKIY","MFJQKMWEOYDZDFH4YMR7WLJKIY 1: SECTION FIELD","MFJQKMWEOYDZDFH4YMR7WLJKIY 2: SECTION FIELD","1: CARDHOLDER NAME","2: NUMBER","3: VERIFICATION NUMBER","4: EXPIRY DATE","SECTION 2: SECTION_PZET7LEKRQXZUINIEGH5ABA2UY","SECTION_PZET7LEKRQXZUINIEGH5ABA2UY 1: LABEL","SECTION 1: 4PQVXPR4BMOPGC3DBMTP5U4OFY","4PQVXPR4BMOPGC3DBMTP5U4OFY 1: SECTION FIELD","4PQVXPR4BMOPGC3DBMTP5U4OFY 2: SECTION FIELD","SECTION 2: M2NTUZZBFOFTPAYXVXE6EMZ5JU","M2NTUZZBFOFTPAYXVXE6EMZ5JU 1: SECTION FIELD","M2NTUZZBFOFTPAYXVXE6EMZ5JU 2: SECTION FIELD","SECTION 3: WC3KPAWH6ZAEQB2ARJB6WYZ3DQ","WC3KPAWH6ZAEQB2ARJB6WYZ3DQ 1: SECTION FIELD","WC3KPAWH6ZAEQB2ARJB6WYZ3DQ 2: SECTION FIELD","WC3KPAWH6ZAEQB2ARJB6WYZ3DQ 3: SECTION FIELD","SECTION 4: TOHUYJEJEMGMI6GEQAZ2LJODFE","TOHUYJEJEMGMI6GEQAZ2LJODFE 1: SECTION FIELD","TOHUYJEJEMGMI6GEQAZ2LJODFE 2: SECTION FIELD","SECTION 5: O26UWJJTXRAANG3ONYYOUUJHDM","O26UWJJTXRAANG3ONYYOUUJHDM 1: SECTION FIELD","O26UWJJTXRAANG3ONYYOUUJHDM 2: WATCH VIDEOS","O26UWJJTXRAANG3ONYYOUUJHDM 3: GET SUPPORT","O26UWJJTXRAANG3ONYYOUUJHDM 4: READ THE BLOG","O26UWJJTXRAANG3ONYYOUUJHDM 5: CONTACT US" +"xjq32axcswefpcxu2mtxxqnufa","1Password Account","email@bitwarden.com","the account's password","https://my.1password.com","https://my.1password.com","email@bitwarden.com","the account's password","A3-76TR2N-NJG3TZ-9NXFX-WT8GF-6YQC9-R2659","Default","Default","Starter Kit","You can use this login to sign in to your account on 1password.com.","🔑 Secret Key","the account's secret key","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","" +"6v56y5z4tejwg37jsettta7d7m","Identity Item","","","","","","","","Default","Default","Starter Kit","It’s you! 🖐 Select Edit to fill in more details, like your address and contact information.","","","Identification","first name","mi","last name","12/2/2020 4:01 AM","occupation","bitwarden","department","job title","Address","address city state zip us","8005555555","Internet Details","userNam3","email@bitwarden.com","💡 Did you know?","1Password can fill names and addresses into webpages:","https://support.1password.com/credit-card-address-filling/","","","","","","","","","","","","","","","","","","","","","","","","","" +"sd26pt226etnsijbl3kqzi5bmm","test card","","","","","","","","Default","Default","","","","","","","","","","","","","","","","","","","","","","","test","4111111111111111","111","1/3/1970 12:23 AM","section","field (phone)","","","","","","","","","","","","","","","","","","","" +"oml2sgit3yk7737kxdis65o4xq","🎉 Welcome to 1Password!","","","","","","","","Default","Default","Starter Kit","Follow these steps to get started.","","","","","","","","","","","","","","","","","","","","","","","","","","","1️⃣ Get the apps","https://1password.com/downloads","Install 1Password everywhere you need your passwords.","2️⃣ Get 1Password in your browser","https://1password.com/downloads/#browsers","Install 1Password in your browser to save and fill passwords.","3️⃣ Save your first password","1. Sign in to your favorite website.","2. 1Password will ask to save your username and password.","3. Click Save Login.","4️⃣ Fill passwords and more","https://support.1password.com/explore/extension/","Save and fill passwords, credit cards, and addresses.","📚 Learn 1Password","Check out our videos and articles:","https://youtube.com/1PasswordVideos","https://support.1password.com/","https://blog.1password.com/","https://support.1password.com/contact-us/"`; diff --git a/spec/node/services/nodeCryptoFunction.service.spec.ts b/spec/node/services/nodeCryptoFunction.service.spec.ts index 392c0a61bc..a7fbc8022f 100644 --- a/spec/node/services/nodeCryptoFunction.service.spec.ts +++ b/spec/node/services/nodeCryptoFunction.service.spec.ts @@ -66,7 +66,7 @@ describe('NodeCrypto Function Service', () => { const prk16Byte = 'criAmKtfzxanbgea5/kelQ=='; const prk32Byte = 'F5h4KdYQnIVH4rKH0P9CZb1GrR4n16/sJrS0PsQEn0Y='; const prk64Byte = 'ssBK0mRG17VHdtsgt8yo4v25CRNpauH+0r2fwY/E9rLyaFBAOMbIeTry+' + - 'gUJ28p8y+hFh3EI9pcrEWaNvFYonQ==' + 'gUJ28p8y+hFh3EI9pcrEWaNvFYonQ=='; testHkdfExpand('sha256', prk32Byte, 32, 'BnIqJlfnHm0e/2iB/15cbHyR19ARPIcWRp4oNS22CD8='); testHkdfExpand('sha256', prk32Byte, 64, 'BnIqJlfnHm0e/2iB/15cbHyR19ARPIcWRp4oNS22CD9BV+' + diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index e71cb3a493..ee81c14cdc 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -70,7 +70,7 @@ describe('WebCrypto Function Service', () => { const prk16Byte = 'criAmKtfzxanbgea5/kelQ=='; const prk32Byte = 'F5h4KdYQnIVH4rKH0P9CZb1GrR4n16/sJrS0PsQEn0Y='; const prk64Byte = 'ssBK0mRG17VHdtsgt8yo4v25CRNpauH+0r2fwY/E9rLyaFBAOMbIeTry+' + - 'gUJ28p8y+hFh3EI9pcrEWaNvFYonQ==' + 'gUJ28p8y+hFh3EI9pcrEWaNvFYonQ=='; testHkdfExpand('sha256', prk32Byte, 32, 'BnIqJlfnHm0e/2iB/15cbHyR19ARPIcWRp4oNS22CD8='); testHkdfExpand('sha256', prk32Byte, 64, 'BnIqJlfnHm0e/2iB/15cbHyR19ARPIcWRp4oNS22CD9BV+' + diff --git a/src/abstractions/cryptoFunction.service.ts b/src/abstractions/cryptoFunction.service.ts index ddb38d1fe7..7573050ce0 100644 --- a/src/abstractions/cryptoFunction.service.ts +++ b/src/abstractions/cryptoFunction.service.ts @@ -5,7 +5,7 @@ export abstract class CryptoFunctionService { pbkdf2: (password: string | ArrayBuffer, salt: string | ArrayBuffer, algorithm: 'sha256' | 'sha512', iterations: number) => Promise; hkdf: (ikm: ArrayBuffer, salt: string | ArrayBuffer, info: string | ArrayBuffer, - outputByteSize: number, algorithm: 'sha256' | 'sha512') => Promise + outputByteSize: number, algorithm: 'sha256' | 'sha512') => Promise; hkdfExpand: (prk: ArrayBuffer, info: string | ArrayBuffer, outputByteSize: number, algorithm: 'sha256' | 'sha512') => Promise; hash: (value: string | ArrayBuffer, algorithm: 'sha1' | 'sha256' | 'sha512' | 'md5') => Promise; diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 0221bceede..3459b96934 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -246,7 +246,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { } get authing(): boolean { - return this.authService.authingWithPassword() || this.authService.authingWithSso() || this.authService.authingWithApiKey() + return this.authService.authingWithPassword() || this.authService.authingWithSso() || this.authService.authingWithApiKey(); } get needsLock(): boolean { diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index ab02785bd6..61c42bfe9b 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -68,7 +68,7 @@ export abstract class BaseImporter { protected parseCsvOptions = { encoding: 'UTF-8', skipEmptyLines: false, - } + }; protected organization() { return this.organizationId != null; diff --git a/src/importers/onepasswordImporters/cipherImportContext.ts b/src/importers/onepasswordImporters/cipherImportContext.ts new file mode 100644 index 0000000000..e3a6ff8d6c --- /dev/null +++ b/src/importers/onepasswordImporters/cipherImportContext.ts @@ -0,0 +1,8 @@ +import { CipherView } from '../../models/view'; + +export class CipherImportContext { + lowerProperty: string; + constructor(public importRecord: any, public property: string, public cipher: CipherView) { + this.lowerProperty = property.toLowerCase(); + } +} diff --git a/src/importers/onepassword1PifImporter.ts b/src/importers/onepasswordImporters/onepassword1PifImporter.ts similarity index 94% rename from src/importers/onepassword1PifImporter.ts rename to src/importers/onepasswordImporters/onepassword1PifImporter.ts index 821b69d45e..66f5e77d9b 100644 --- a/src/importers/onepassword1PifImporter.ts +++ b/src/importers/onepasswordImporters/onepassword1PifImporter.ts @@ -1,17 +1,17 @@ -import { BaseImporter } from './baseImporter'; -import { Importer } from './importer'; +import { BaseImporter } from '../baseImporter'; +import { Importer } from '../importer'; -import { ImportResult } from '../models/domain/importResult'; +import { ImportResult } from '../../models/domain/importResult'; -import { CardView } from '../models/view/cardView'; -import { CipherView } from '../models/view/cipherView'; -import { IdentityView } from '../models/view/identityView'; -import { PasswordHistoryView } from '../models/view/passwordHistoryView'; -import { SecureNoteView } from '../models/view/secureNoteView'; +import { CardView } from '../../models/view/cardView'; +import { CipherView } from '../../models/view/cipherView'; +import { IdentityView } from '../../models/view/identityView'; +import { PasswordHistoryView } from '../../models/view/passwordHistoryView'; +import { SecureNoteView } from '../../models/view/secureNoteView'; -import { CipherType } from '../enums/cipherType'; -import { FieldType } from '../enums/fieldType'; -import { SecureNoteType } from '../enums/secureNoteType'; +import { CipherType } from '../../enums/cipherType'; +import { FieldType } from '../../enums/fieldType'; +import { SecureNoteType } from '../../enums/secureNoteType'; export class OnePassword1PifImporter extends BaseImporter implements Importer { result = new ImportResult(); diff --git a/src/importers/onepasswordImporters/onepasswordCsvImporter.ts b/src/importers/onepasswordImporters/onepasswordCsvImporter.ts new file mode 100644 index 0000000000..87ee36e54b --- /dev/null +++ b/src/importers/onepasswordImporters/onepasswordCsvImporter.ts @@ -0,0 +1,288 @@ +import { ImportResult } from '../../models/domain/importResult'; +import { BaseImporter } from '../baseImporter'; +import { Importer } from '../importer'; + +import { CipherType } from '../../enums/cipherType'; +import { FieldType } from '../../enums/fieldType'; +import { CipherView } from '../../models/view'; +import { CipherImportContext } from './cipherImportContext'; + +export const IgnoredProperties = ['ainfo', 'autosubmit', 'notesplain', 'ps', 'scope', 'tags', 'title', 'uuid', 'notes']; + +export abstract class OnePasswordCsvImporter extends BaseImporter implements Importer { + protected loginPropertyParsers = [this.setLoginUsername, this.setLoginPassword, this.setLoginUris]; + protected creditCardPropertyParsers = [this.setCreditCardNumber, this.setCreditCardVerification, this.setCreditCardCardholderName, this.setCreditCardExpiry]; + protected identityPropertyParsers = [this.setIdentityFirstName, this.setIdentityInitial, this.setIdentityLastName, this.setIdentityUserName, this.setIdentityEmail, this.setIdentityPhone, this.setIdentityCompany]; + + abstract setCipherType(value: any, cipher: CipherView): void; + + parse(data: string): Promise { + const result = new ImportResult(); + const results = this.parseCsv(data, true, { + quoteChar: '"', + escapeChar: '\\', + }); + if (results == null) { + result.success = false; + return Promise.resolve(result); + } + + results.forEach((value) => { + if (this.isNullOrWhitespace(this.getProp(value, 'title'))) { + return; + } + + const cipher = this.initLoginCipher(); + cipher.name = this.getValueOrDefault(this.getProp(value, 'title'), '--'); + + this.setNotes(value, cipher); + + this.setCipherType(value, cipher); + + let altUsername: string = null; + for (const property in value) { + if (!value.hasOwnProperty(property) || this.isNullOrWhitespace(value[property])) { + continue; + } + + const context = new CipherImportContext(value, property, cipher); + if (cipher.type === CipherType.Login && this.setKnownLoginValue(context)) { + continue; + } else if (cipher.type === CipherType.Card && this.setKnownCreditCardValue(context)) { + continue; + } else if (cipher.type === CipherType.Identity && this.setKnownIdentityValue(context)) { + continue; + } + + altUsername = this.setUnknownValue(context, altUsername); + } + + if (cipher.type === CipherType.Login && !this.isNullOrWhitespace(altUsername) && + this.isNullOrWhitespace(cipher.login.username) && altUsername.indexOf('://') === -1) { + cipher.login.username = altUsername; + } + + this.convertToNoteIfNeeded(cipher); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return Promise.resolve(result); + } + + protected getProp(obj: any, name: string): any { + const lowerObj = Object.entries(obj).reduce((agg: any, entry: [string, any]) => { + agg[entry[0].toLowerCase()] = entry[1]; + return agg; + }, {}); + return lowerObj[name.toLowerCase()]; + } + + protected getPropByRegexp(obj: any, regexp: RegExp): any { + const matchingKeys = Object.keys(obj).reduce((agg: string[], key: string) => { + if (key.match(regexp)) { + agg.push(key); + } + return agg; + }, []); + if (matchingKeys.length === 0) { + return null; + } else { + return obj[matchingKeys[0]]; + } + } + + protected getPropIncluding(obj: any, name: string): any { + const includesMap = Object.keys(obj).reduce((agg: string[], entry: string) => { + if (entry.toLowerCase().includes(name.toLowerCase())) { + agg.push(entry); + } + return agg; + }, []); + if (includesMap.length === 0) { + return null; + } else { + return obj[includesMap[0]]; + } + } + + protected setNotes(importRecord: any, cipher: CipherView) { + cipher.notes = this.getValueOrDefault(this.getProp(importRecord, 'notesPlain'), '') + '\n' + + this.getValueOrDefault(this.getProp(importRecord, 'notes'), '') + '\n'; + cipher.notes.trim(); + + } + + protected setKnownLoginValue(context: CipherImportContext): boolean { + return this.loginPropertyParsers.reduce((agg: boolean, func) => { + if (!agg) { + agg = func.bind(this)(context); + } + return agg; + }, false); + } + + protected setKnownCreditCardValue(context: CipherImportContext): boolean { + return this.creditCardPropertyParsers.reduce((agg: boolean, func) => { + if (!agg) { + agg = func.bind(this)(context); + } + return agg; + }, false); + } + + protected setKnownIdentityValue(context: CipherImportContext): boolean { + return this.identityPropertyParsers.reduce((agg: boolean, func) => { + if (!agg) { + agg = func.bind(this)(context); + } + return agg; + }, false); + } + + protected setUnknownValue(context: CipherImportContext, altUsername: string): string { + if (IgnoredProperties.indexOf(context.lowerProperty) === -1 && !context.lowerProperty.startsWith('section:') && + !context.lowerProperty.startsWith('section ')) { + if (altUsername == null && context.lowerProperty === 'email') { + return context.importRecord[context.property]; + } + else if (context.lowerProperty === 'created date' || context.lowerProperty === 'modified date') { + const readableDate = new Date(parseInt(context.importRecord[context.property], 10) * 1000).toUTCString(); + this.processKvp(context.cipher, '1Password ' + context.property, readableDate); + return null; + } + if (context.lowerProperty.includes('password') || context.lowerProperty.includes('key') || context.lowerProperty.includes('secret')) { + this.processKvp(context.cipher, context.property, context.importRecord[context.property], FieldType.Hidden); + } else { + this.processKvp(context.cipher, context.property, context.importRecord[context.property]); + } + } + return null; + } + + protected setIdentityFirstName(context: CipherImportContext) { + if (this.isNullOrWhitespace(context.cipher.identity.firstName) && context.lowerProperty.includes('first name')) { + context.cipher.identity.firstName = context.importRecord[context.property]; + return true; + } + return false; + } + + protected setIdentityInitial(context: CipherImportContext) { + if (this.isNullOrWhitespace(context.cipher.identity.middleName) && context.lowerProperty.includes('initial')) { + context.cipher.identity.middleName = context.importRecord[context.property]; + return true; + } + return false; + } + + protected setIdentityLastName(context: CipherImportContext) { + if (this.isNullOrWhitespace(context.cipher.identity.lastName) && context.lowerProperty.includes('last name')) { + context.cipher.identity.lastName = context.importRecord[context.property]; + return true; + } + return false; + } + + protected setIdentityUserName(context: CipherImportContext) { + if (this.isNullOrWhitespace(context.cipher.identity.username) && context.lowerProperty.includes('username')) { + context.cipher.identity.username = context.importRecord[context.property]; + return true; + } + return false; + } + + protected setIdentityCompany(context: CipherImportContext) { + if (this.isNullOrWhitespace(context.cipher.identity.company) && context.lowerProperty.includes('company')) { + context.cipher.identity.company = context.importRecord[context.property]; + return true; + } + return false; + } + + protected setIdentityPhone(context: CipherImportContext) { + if (this.isNullOrWhitespace(context.cipher.identity.phone) && context.lowerProperty.includes('default phone')) { + context.cipher.identity.phone = context.importRecord[context.property]; + return true; + } + return false; + } + + protected setIdentityEmail(context: CipherImportContext) { + if (this.isNullOrWhitespace(context.cipher.identity.email) && context.lowerProperty.includes('email')) { + context.cipher.identity.email = context.importRecord[context.property]; + return true; + } + return false; + } + + protected setCreditCardNumber(context: CipherImportContext): boolean { + if (this.isNullOrWhitespace(context.cipher.card.number) && context.lowerProperty.includes('number')) { + context.cipher.card.number = context.importRecord[context.property]; + context.cipher.card.brand = this.getCardBrand(context.cipher.card.number); + return true; + } + return false; + } + + protected setCreditCardVerification(context: CipherImportContext) { + if (this.isNullOrWhitespace(context.cipher.card.code) && context.lowerProperty.includes('verification number')) { + context.cipher.card.code = context.importRecord[context.property]; + return true; + } + return false; + } + + protected setCreditCardCardholderName(context: CipherImportContext) { + if (this.isNullOrWhitespace(context.cipher.card.cardholderName) && context.lowerProperty.includes('cardholder name')) { + context.cipher.card.cardholderName = context.importRecord[context.property]; + return true; + } + return false; + } + + protected setCreditCardExpiry(context: CipherImportContext) { + if (this.isNullOrWhitespace(context.cipher.card.expiration) && context.lowerProperty.includes('expiry date') && + context.importRecord[context.property].length === 7) { + context.cipher.card.expMonth = (context.importRecord[context.property] as string).substr(0, 2); + if (context.cipher.card.expMonth[0] === '0') { + context.cipher.card.expMonth = context.cipher.card.expMonth.substr(1, 1); + } + context.cipher.card.expYear = (context.importRecord[context.property] as string).substr(3, 4); + return true; + } + return false; + } + + protected setLoginPassword(context: CipherImportContext) { + if (this.isNullOrWhitespace(context.cipher.login.password) && context.lowerProperty === 'password') { + context.cipher.login.password = context.importRecord[context.property]; + return true; + } + return false; + } + + protected setLoginUsername(context: CipherImportContext) { + if (this.isNullOrWhitespace(context.cipher.login.username) && context.lowerProperty === 'username') { + context.cipher.login.username = context.importRecord[context.property]; + return true; + } + return false; + } + + protected setLoginUris(context: CipherImportContext) { + if ((context.cipher.login.uris == null || context.cipher.login.uris.length === 0) && context.lowerProperty === 'urls') { + const urls = context.importRecord[context.property].split(this.newLineRegex); + context.cipher.login.uris = this.makeUriArray(urls); + return true; + } else if ((context.lowerProperty === 'url')) { + if (context.cipher.login.uris == null) { + context.cipher.login.uris = []; + } + context.cipher.login.uris.concat(this.makeUriArray(context.importRecord[context.property])); + return true; + } + return false; + } +} diff --git a/src/importers/onepasswordImporters/onepasswordMacCsvImporter.ts b/src/importers/onepasswordImporters/onepasswordMacCsvImporter.ts new file mode 100644 index 0000000000..443647c82d --- /dev/null +++ b/src/importers/onepasswordImporters/onepasswordMacCsvImporter.ts @@ -0,0 +1,28 @@ +import { Importer } from '../importer'; +import { IgnoredProperties, OnePasswordCsvImporter } from './onepasswordCsvImporter'; + +import { CipherType } from '../../enums/cipherType'; +import { CardView, CipherView, IdentityView } from '../../models/view'; + +export class OnePasswordMacCsvImporter extends OnePasswordCsvImporter implements Importer { + setCipherType(value: any, cipher: CipherView) { + const onePassType = this.getValueOrDefault(this.getProp(value, 'type'), 'Login'); + switch (onePassType) { + case 'Credit Card': + cipher.type = CipherType.Card; + cipher.card = new CardView(); + IgnoredProperties.push('type'); + break; + case 'Identity': + cipher.type = CipherType.Identity; + cipher.identity = new IdentityView(); + IgnoredProperties.push('type'); + break; + case 'Login': + case 'Secure Note': + IgnoredProperties.push('type'); + default: + break; + } + } +} diff --git a/src/importers/onepasswordImporters/onepasswordWinCsvImporter.ts b/src/importers/onepasswordImporters/onepasswordWinCsvImporter.ts new file mode 100644 index 0000000000..9b4fae89c6 --- /dev/null +++ b/src/importers/onepasswordImporters/onepasswordWinCsvImporter.ts @@ -0,0 +1,53 @@ +import { Importer } from '../importer'; +import { CipherImportContext } from './cipherImportContext'; +import { OnePasswordCsvImporter } from './onepasswordCsvImporter'; + +import { CipherType } from '../../enums/cipherType'; +import { CardView, CipherView, IdentityView, LoginView } from '../../models/view'; + +export class OnePasswordWinCsvImporter extends OnePasswordCsvImporter implements Importer { + constructor() { + super(); + this.identityPropertyParsers.push(this.setIdentityAddress); + } + + setCipherType(value: any, cipher: CipherView) { + cipher.type = CipherType.Login; + cipher.login = new LoginView(); + + if (!this.isNullOrWhitespace(this.getPropByRegexp(value, /\d+: number/i)) && + !this.isNullOrWhitespace(this.getPropByRegexp(value, /\d+: expiry date/i))) { + cipher.type = CipherType.Card; + cipher.card = new CardView(); + } + + if (!this.isNullOrWhitespace(this.getPropByRegexp(value, /name \d+: first name/i)) || + !this.isNullOrWhitespace(this.getPropByRegexp(value, /name \d+: initial/i)) || + !this.isNullOrWhitespace(this.getPropByRegexp(value, /name \d+: last name/i)) || + !this.isNullOrWhitespace(this.getPropByRegexp(value, /internet \d+: email/i))) { + cipher.type = CipherType.Identity; + cipher.identity = new IdentityView(); + } + } + + setIdentityAddress(context: CipherImportContext) { + if (context.lowerProperty.match(/address \d+: address/i)) { + this.processKvp(context.cipher, 'address', context.importRecord[context.property]); + return true; + } + return false; + } + + setCreditCardExpiry(context: CipherImportContext) { + if (this.isNullOrWhitespace(context.cipher.card.expiration) && context.lowerProperty.includes('expiry date')) { + const expSplit = (context.importRecord[context.property] as string).split('/'); + context.cipher.card.expMonth = expSplit[0]; + if (context.cipher.card.expMonth[0] === '0' && context.cipher.card.expMonth.length === 2) { + context.cipher.card.expMonth = context.cipher.card.expMonth.substr(1, 1); + } + context.cipher.card.expYear = expSplit[2].length > 4 ? expSplit[2].substr(0, 4) : expSplit[2]; + return true; + } + return false; + } +} diff --git a/src/importers/onepasswordWinCsvImporter.ts b/src/importers/onepasswordWinCsvImporter.ts deleted file mode 100644 index 6e0192e697..0000000000 --- a/src/importers/onepasswordWinCsvImporter.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { BaseImporter } from './baseImporter'; -import { Importer } from './importer'; - -import { ImportResult } from '../models/domain/importResult'; - -import { CipherType } from '../enums/cipherType'; -import { CardView, IdentityView } from '../models/view'; - -const IgnoredProperties = ['ainfo', 'autosubmit', 'notesplain', 'ps', 'scope', 'tags', 'title', 'uuid', 'notes']; - -export class OnePasswordWinCsvImporter extends BaseImporter implements Importer { - parse(data: string): Promise { - const result = new ImportResult(); - const results = this.parseCsv(data, true, { - quoteChar: '"', - escapeChar: '\\', - }); - if (results == null) { - result.success = false; - return Promise.resolve(result); - } - - results.forEach((value) => { - if (this.isNullOrWhitespace(this.getProp(value, 'title'))) { - return; - } - - const cipher = this.initLoginCipher(); - cipher.name = this.getValueOrDefault(this.getProp(value, 'title'), '--'); - - cipher.notes = this.getValueOrDefault(this.getProp(value, 'notesPlain'), '') + '\n' + - this.getValueOrDefault(this.getProp(value, 'notes'), '') + '\n'; - cipher.notes.trim(); - - const onePassType = this.getValueOrDefault(this.getProp(value, 'type'), 'Login') - switch (onePassType) { - case 'Credit Card': - cipher.type = CipherType.Card; - cipher.card = new CardView(); - IgnoredProperties.push('type'); - break; - case 'Identity': - cipher.type = CipherType.Identity; - cipher.identity = new IdentityView(); - IgnoredProperties.push('type'); - break; - case 'Login': - case 'Secure Note': - IgnoredProperties.push('type'); - default: - break; - } - - if (!this.isNullOrWhitespace(this.getProp(value, 'number')) && - !this.isNullOrWhitespace(this.getProp(value, 'expiry date'))) { - cipher.type = CipherType.Card; - cipher.card = new CardView(); - } - - let altUsername: string = null; - for (const property in value) { - if (!value.hasOwnProperty(property) || this.isNullOrWhitespace(value[property])) { - continue; - } - - const lowerProp = property.toLowerCase(); - if (cipher.type === CipherType.Login) { - if (this.isNullOrWhitespace(cipher.login.password) && lowerProp === 'password') { - cipher.login.password = value[property]; - continue; - } else if (this.isNullOrWhitespace(cipher.login.username) && lowerProp === 'username') { - cipher.login.username = value[property]; - continue; - } else if ((cipher.login.uris == null || cipher.login.uri.length === 0) && lowerProp === 'urls') { - const urls = value[property].split(this.newLineRegex); - cipher.login.uris = this.makeUriArray(urls); - continue; - } else if ((lowerProp === 'url')) { - if (cipher.login.uris == null) { - cipher.login.uris = []; - } - cipher.login.uris.concat(this.makeUriArray(value[property])); - continue; - } - } else if (cipher.type === CipherType.Card) { - if (this.isNullOrWhitespace(cipher.card.number) && lowerProp.includes('number')) { - cipher.card.number = value[property]; - cipher.card.brand = this.getCardBrand(this.getProp(value, 'number')); - continue; - } else if (this.isNullOrWhitespace(cipher.card.code) && lowerProp.includes('verification number')) { - cipher.card.code = value[property]; - continue; - } else if (this.isNullOrWhitespace(cipher.card.cardholderName) && lowerProp.includes('cardholder name')) { - cipher.card.cardholderName = value[property]; - continue; - } else if (this.isNullOrWhitespace(cipher.card.expiration) && lowerProp.includes('expiry date') && - value[property].length === 7) { - cipher.card.expMonth = (value[property] as string).substr(0, 2); - if (cipher.card.expMonth[0] === '0') { - cipher.card.expMonth = cipher.card.expMonth.substr(1, 1); - } - cipher.card.expYear = (value[property] as string).substr(3, 4); - continue; - } else if (lowerProp === 'type' || lowerProp === 'type(type)') { - // Skip since brand was determined from number above - continue; - } - } else if (cipher.type === CipherType.Identity) { - if (this.isNullOrWhitespace(cipher.identity.firstName) && lowerProp.includes('first name')) { - cipher.identity.firstName = value[property]; - continue; - } else if (this.isNullOrWhitespace(cipher.identity.middleName) && lowerProp.includes('initial')) { - cipher.identity.middleName = value[property]; - continue; - } else if (this.isNullOrWhitespace(cipher.identity.lastName) && lowerProp.includes('last name')) { - cipher.identity.lastName = value[property]; - continue; - } else if (this.isNullOrWhitespace(cipher.identity.username) && lowerProp.includes('username')) { - cipher.identity.username = value[property]; - continue; - } else if (this.isNullOrWhitespace(cipher.identity.company) && lowerProp.includes('company')) { - cipher.identity.company = value[property]; - continue; - } else if (this.isNullOrWhitespace(cipher.identity.phone) && lowerProp.includes('default phone')) { - cipher.identity.phone = value[property]; - continue; - } else if (this.isNullOrWhitespace(cipher.identity.email) && lowerProp.includes('email')) { - cipher.identity.email = value[property]; - continue; - } - } - - if (IgnoredProperties.indexOf(lowerProp) === -1 && !lowerProp.startsWith('section:') && - !lowerProp.startsWith('section ')) { - if (altUsername == null && lowerProp === 'email') { - altUsername = value[property]; - } - else if (lowerProp === 'created date' || lowerProp === 'modified date') { - const readableDate = new Date(parseInt(value[property], 10) * 1000).toUTCString(); - this.processKvp(cipher, '1Password ' + property, readableDate); - continue; - } - this.processKvp(cipher, property, value[property]); - } - } - - if (cipher.type === CipherType.Login && !this.isNullOrWhitespace(altUsername) && - this.isNullOrWhitespace(cipher.login.username) && altUsername.indexOf('://') === -1) { - cipher.login.username = altUsername; - } - - this.convertToNoteIfNeeded(cipher); - this.cleanupCipher(cipher); - result.ciphers.push(cipher); - }); - - result.success = true; - return Promise.resolve(result); - } - - private getProp(obj: any, name: string): any { - const lowerObj = Object.entries(obj).reduce((agg: any, entry: [string, any]) => { - agg[entry[0].toLowerCase()] = entry[1]; - return agg; - }, {}); - return lowerObj[name.toLowerCase()]; - } -} diff --git a/src/misc/utils.ts b/src/misc/utils.ts index cacda81e3e..619270212f 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -94,7 +94,7 @@ export class Utils { } static fromBufferToUrlB64(buffer: ArrayBuffer): string { - return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer)) + return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer)); } static fromB64toUrlB64(b64Str: string) { diff --git a/src/models/request/sendRequest.ts b/src/models/request/sendRequest.ts index a2c64a2ca0..035fe8e03f 100644 --- a/src/models/request/sendRequest.ts +++ b/src/models/request/sendRequest.ts @@ -1,6 +1,6 @@ import { SendType } from '../../enums/sendType'; -import { SendFileApi } from '../api/sendFileApi' +import { SendFileApi } from '../api/sendFileApi'; import { SendTextApi } from '../api/sendTextApi'; import { Send } from '../domain/send'; diff --git a/src/models/request/tokenRequest.ts b/src/models/request/tokenRequest.ts index a3db628670..1180b08120 100644 --- a/src/models/request/tokenRequest.ts +++ b/src/models/request/tokenRequest.ts @@ -25,8 +25,8 @@ export class TokenRequest { this.codeVerifier = codes[1]; this.redirectUri = codes[2]; } else if (clientIdClientSecret != null && clientIdClientSecret.length > 1) { - this.clientId = clientIdClientSecret[0] - this.clientSecret = clientIdClientSecret[1] + this.clientId = clientIdClientSecret[0]; + this.clientSecret = clientIdClientSecret[1]; } this.token = token; this.provider = provider; diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index d8ec84277d..de52de67e1 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -283,7 +283,7 @@ export class AuthService implements AuthServiceAbstraction { codeCodeVerifier = null; } if (clientId != null && clientSecret != null) { - clientIdClientSecret = [clientId, clientSecret] + clientIdClientSecret = [clientId, clientSecret]; } else { clientIdClientSecret = null; } diff --git a/src/services/import.service.ts b/src/services/import.service.ts index 3b2d4ed444..d41cee06bd 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -52,8 +52,9 @@ import { LogMeOnceCsvImporter } from '../importers/logMeOnceCsvImporter'; import { MeldiumCsvImporter } from '../importers/meldiumCsvImporter'; import { MSecureCsvImporter } from '../importers/msecureCsvImporter'; import { MykiCsvImporter } from '../importers/mykiCsvImporter'; -import { OnePassword1PifImporter } from '../importers/onepassword1PifImporter'; -import { OnePasswordWinCsvImporter } from '../importers/onepasswordWinCsvImporter'; +import { OnePassword1PifImporter } from '../importers/onepasswordImporters/onepassword1PifImporter'; +import { OnePasswordMacCsvImporter } from '../importers/onepasswordImporters/onepasswordMacCsvImporter'; +import { OnePasswordWinCsvImporter } from '../importers/onepasswordImporters/onepasswordWinCsvImporter'; import { PadlockCsvImporter } from '../importers/padlockCsvImporter'; import { PassKeepCsvImporter } from '../importers/passkeepCsvImporter'; import { PassmanJsonImporter } from '../importers/passmanJsonImporter'; @@ -90,6 +91,7 @@ export class ImportService implements ImportServiceAbstraction { regularImportOptions: ImportOption[] = [ { id: 'keepassxcsv', name: 'KeePassX (csv)' }, { id: '1passwordwincsv', name: '1Password 6 and 7 Windows (csv)' }, + { id: '1passwordmaccsv', name: '1Password 6 and 7 Mac (csv)' }, { id: 'roboformcsv', name: 'RoboForm (csv)' }, { id: 'keepercsv', name: 'Keeper (csv)' }, { id: 'enpasscsv', name: 'Enpass (csv)' }, @@ -215,6 +217,8 @@ export class ImportService implements ImportServiceAbstraction { return new OnePassword1PifImporter(); case '1passwordwincsv': return new OnePasswordWinCsvImporter(); + case '1passwordmaccsv': + return new OnePasswordMacCsvImporter(); case 'keepercsv': return new KeeperCsvImporter(); case 'passworddragonxml': diff --git a/tslint.json b/tslint.json index 41090a98e1..3fe8b5b5ba 100644 --- a/tslint.json +++ b/tslint.json @@ -51,6 +51,10 @@ "check-separator", "check-type" ], - "max-classes-per-file": false + "max-classes-per-file": false, + "semicolon": [ + true, + "always" + ] } } From ecf1edfb3eff0b2c38ad64ecc86e5b47ac03fad0 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 9 Dec 2020 15:30:29 -0600 Subject: [PATCH 1117/1626] Specify to 2fa we're coming from sso (#218) This will allow for closing of 2fa window to optionally alter behavior for 2fa logins Co-authored-by: Matt Gibson --- src/angular/components/sso.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/angular/components/sso.component.ts b/src/angular/components/sso.component.ts index f0b2f1d41e..655be7db7f 100644 --- a/src/angular/components/sso.component.ts +++ b/src/angular/components/sso.component.ts @@ -145,6 +145,7 @@ export class SsoComponent { this.router.navigate([this.twoFactorRoute], { queryParams: { identifier: orgIdFromState, + sso: 'true' }, }); } From 63fe38b3f4dddd701b1a8da8a89e70c9d7fbc706 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Fri, 11 Dec 2020 15:46:20 +0100 Subject: [PATCH 1118/1626] Fix dock icon not working when minimized to menu bar, fix window listeners not working after closing the main window (#223) --- src/electron/tray.main.ts | 9 ++++++--- src/electron/window.main.ts | 9 ++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/electron/tray.main.ts b/src/electron/tray.main.ts index a782d95e12..bed712f5aa 100644 --- a/src/electron/tray.main.ts +++ b/src/electron/tray.main.ts @@ -1,5 +1,6 @@ import { app, + BrowserWindow, Menu, MenuItem, MenuItemConstructorOptions, @@ -57,15 +58,17 @@ export class TrayMain { if (await this.storageService.get(ElectronConstants.enableTrayKey)) { this.showTray(); } + } - this.windowMain.win.on('minimize', async (e: Event) => { + setupWindowListeners(win: BrowserWindow) { + win.on('minimize', async (e: Event) => { if (await this.storageService.get(ElectronConstants.enableMinimizeToTrayKey)) { e.preventDefault(); this.hideToTray(); } }); - this.windowMain.win.on('close', async (e: Event) => { + win.on('close', async (e: Event) => { if (await this.storageService.get(ElectronConstants.enableCloseToTrayKey)) { if (!this.windowMain.isQuitting) { e.preventDefault(); @@ -74,7 +77,7 @@ export class TrayMain { } }); - this.windowMain.win.on('show', async (e: Event) => { + win.on('show', async (e: Event) => { const enableTray = await this.storageService.get(ElectronConstants.enableTrayKey); if (!enableTray) { setTimeout(() => this.removeTray(false), 100); diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index e1a1498da8..6d7381c4b9 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -23,7 +23,8 @@ export class WindowMain { constructor(private storageService: StorageService, private hideTitleBar = false, private defaultWidth = 950, private defaultHeight = 600, - private argvCallback: (argv: string[]) => void = null) { } + private argvCallback: (argv: string[]) => void = null, + private createWindowCallback: (win: BrowserWindow) => void) { } init(): Promise { return new Promise((resolve, reject) => { @@ -82,6 +83,9 @@ export class WindowMain { // dock icon is clicked and there are no other windows open. if (this.win === null) { await this.createWindow(); + } else { + // Show the window when clicking on Dock icon + this.win.show(); } }); @@ -169,6 +173,9 @@ export class WindowMain { this.windowStateChangeHandler(Keys.mainWindowSize, this.win); }); + if (this.createWindowCallback) { + this.createWindowCallback(this.win); + } } async toggleAlwaysOnTop() { From 2c414ce27a5c14f6cd7f86cfd07096a192d058ca Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Fri, 11 Dec 2020 10:44:57 -0600 Subject: [PATCH 1119/1626] Use log service for console messages (#221) * Use logService for console messages * Implement a base ConsoleLog service Use this class as a default for other services that would like to output to console. This service is overriden in CLI and Desktop to use CLI's consoleLogService and electronLogService, respectively. * Use browser-process-hrtime for timing * test LogService implementations * Ignore default import of hrtime * Clean up imports. Require ConsoleLog injection Co-authored-by: Matt Gibson --- package.json | 2 + .../services/consoleLog.service.spec.ts | 95 +++++++++++++++++++ .../services/electronLog.service.spec.ts | 9 ++ spec/node/cli/consoleLog.service.spec.ts | 41 ++++++++ spec/support/jasmine.json | 3 +- src/abstractions/log.service.ts | 2 + src/cli/baseProgram.ts | 2 +- src/cli/services/consoleLog.service.ts | 50 ++-------- src/electron/services/electronLog.service.ts | 28 +----- src/importers/baseImporter.ts | 8 +- src/services/auth.service.ts | 7 +- src/services/consoleLog.service.ts | 71 ++++++++++++++ src/services/crypto.service.ts | 17 ++-- src/services/notifications.service.ts | 8 +- src/services/search.service.ts | 11 ++- 15 files changed, 269 insertions(+), 85 deletions(-) create mode 100644 spec/common/services/consoleLog.service.spec.ts create mode 100644 spec/electron/services/electronLog.service.spec.ts create mode 100644 spec/node/cli/consoleLog.service.spec.ts create mode 100644 src/services/consoleLog.service.ts diff --git a/package.json b/package.json index c7d2532704..196274d87c 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "karma-typescript": "^4.0.0", "nodemon": "^1.17.3", "rimraf": "^2.6.2", + "ts-node": "^9.1.0", "tslint": "^6.1.3", "typemoq": "^2.1.0", "typescript": "3.8.3" @@ -75,6 +76,7 @@ "@microsoft/signalr-protocol-msgpack": "3.1.0", "@nodert-win10-rs4/windows.security.credentials.ui": "^0.4.4", "big-integer": "1.6.36", + "browser-process-hrtime": "1.0.0", "chalk": "2.4.1", "commander": "2.18.0", "core-js": "2.6.2", diff --git a/spec/common/services/consoleLog.service.spec.ts b/spec/common/services/consoleLog.service.spec.ts new file mode 100644 index 0000000000..b4f7d9d612 --- /dev/null +++ b/spec/common/services/consoleLog.service.spec.ts @@ -0,0 +1,95 @@ +import { ConsoleLogService } from '../../../src/services/consoleLog.service'; + +const originalConsole = console; +let caughtMessage: any; + +declare var console: any; + +export function interceptConsole(interceptions: any): object { + console = { + // tslint:disable-next-line + log: function () { + interceptions.log = arguments; + }, + // tslint:disable-next-line + warn: function () { + interceptions.warn = arguments; + }, + // tslint:disable-next-line + error: function () { + interceptions.error = arguments; + } + }; + return interceptions; +} + +export function restoreConsole() { + console = originalConsole; +} + +describe('ConsoleLogService', () => { + let logService: ConsoleLogService; + beforeEach(() => { + caughtMessage = {}; + interceptConsole(caughtMessage); + logService = new ConsoleLogService(true); + }); + + afterAll(() => { + restoreConsole(); + }); + + it('filters messages below the set threshold', () => { + logService = new ConsoleLogService(true, (level) => true); + logService.debug('debug'); + logService.info('info'); + logService.warning('warning'); + logService.error('error'); + + expect(caughtMessage).toEqual({}); + }); + it('only writes debug messages in dev mode', () => { + logService = new ConsoleLogService(false); + + logService.debug('debug message'); + expect(caughtMessage.log).toBeUndefined(); + }); + + + it('writes debug/info messages to console.log', () => { + logService.debug('this is a debug message'); + expect(caughtMessage).toEqual({ log: jasmine.arrayWithExactContents(['this is a debug message']) }); + + logService.info('this is an info message'); + expect(caughtMessage).toEqual({ log: jasmine.arrayWithExactContents(['this is an info message']) }); + }); + it('writes warning messages to console.warn', () => { + logService.warning('this is a warning message'); + expect(caughtMessage).toEqual({ warn: jasmine.arrayWithExactContents(['this is a warning message']) }); + }); + it('writes error messages to console.error', () => { + logService.error('this is an error message'); + expect(caughtMessage).toEqual({ error: jasmine.arrayWithExactContents(['this is an error message']) }); + }); + + it('times with output to info', async () => { + logService.time(); + await new Promise(r => setTimeout(r, 250)); + const duration = logService.timeEnd(); + expect(duration[0]).toBe(0); + expect(duration[1]).toBeGreaterThan(0); + expect(duration[1]).toBeLessThan(500 * 10e6); + + expect(caughtMessage).toEqual(jasmine.arrayContaining([])); + expect(caughtMessage.log.length).toBe(1); + expect(caughtMessage.log[0]).toEqual(jasmine.stringMatching(/^default: \d+\.?\d*ms$/)); + }); + + it('filters time output', async () => { + logService = new ConsoleLogService(true, (level) => true); + logService.time(); + logService.timeEnd(); + + expect(caughtMessage).toEqual({}); + }); +}); diff --git a/spec/electron/services/electronLog.service.spec.ts b/spec/electron/services/electronLog.service.spec.ts new file mode 100644 index 0000000000..dc608c2b8a --- /dev/null +++ b/spec/electron/services/electronLog.service.spec.ts @@ -0,0 +1,9 @@ +import { ElectronLogService } from '../../../src/electron/services/electronLog.service'; + +describe('ElectronLogService', () => { + it('sets dev based on electron method', () => { + process.env.ELECTRON_IS_DEV = '1'; + const logService = new ElectronLogService(); + expect(logService).toEqual(jasmine.objectContaining({ isDev: true }) as any); + }); +}); diff --git a/spec/node/cli/consoleLog.service.spec.ts b/spec/node/cli/consoleLog.service.spec.ts new file mode 100644 index 0000000000..9a8a8d0f6c --- /dev/null +++ b/spec/node/cli/consoleLog.service.spec.ts @@ -0,0 +1,41 @@ +import { ConsoleLogService } from '../../../src/cli/services/consoleLog.service'; +import { interceptConsole, restoreConsole } from '../../common/services/consoleLog.service.spec'; + +const originalConsole = console; +let caughtMessage: any = {}; + +describe('CLI Console log service', () => { + let logService: ConsoleLogService; + beforeEach(() => { + caughtMessage = {}; + interceptConsole(caughtMessage); + logService = new ConsoleLogService(true); + }); + + afterAll(() => { + restoreConsole(); + }); + + it('should redirect all console to error if BW_RESPONSE env is true', () => { + process.env.BW_RESPONSE = 'true'; + + logService.debug('this is a debug message'); + expect(caughtMessage).toEqual({ error: jasmine.arrayWithExactContents(['this is a debug message']) }); + }); + + it('should not redirect console to error if BW_RESPONSE != true', () => { + process.env.BW_RESPONSE = 'false'; + + logService.debug('debug'); + logService.info('info'); + logService.warning('warning'); + logService.error('error'); + + expect(caughtMessage).toEqual({ + log: jasmine.arrayWithExactContents(['info']), + warn: jasmine.arrayWithExactContents(['warning']), + error: jasmine.arrayWithExactContents(['error']), + }); + + }); +}); diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json index 19ce1e1633..96a0fe21c6 100644 --- a/spec/support/jasmine.json +++ b/spec/support/jasmine.json @@ -2,7 +2,8 @@ "spec_dir": "dist/spec", "spec_files": [ "common/**/*[sS]pec.js", - "node/**/*[sS]pec.js" + "node/**/*[sS]pec.js", + "electron/**/*[sS]pec.js" ], "helpers": [ "helpers.js" diff --git a/src/abstractions/log.service.ts b/src/abstractions/log.service.ts index c4cb55035e..a46284dd02 100644 --- a/src/abstractions/log.service.ts +++ b/src/abstractions/log.service.ts @@ -6,4 +6,6 @@ export abstract class LogService { warning: (message: string) => void; error: (message: string) => void; write: (level: LogLevelType, message: string) => void; + time: (label: string) => void; + timeEnd: (label: string) => [number, number]; } diff --git a/src/cli/baseProgram.ts b/src/cli/baseProgram.ts index cc58ac1844..d8fa45acf3 100644 --- a/src/cli/baseProgram.ts +++ b/src/cli/baseProgram.ts @@ -18,7 +18,7 @@ export abstract class BaseProgram { if (!response.success) { if (process.env.BW_QUIET !== 'true') { if (process.env.BW_RESPONSE === 'true') { - this.writeLn(this.getJson(response), true, true); + this.writeLn(this.getJson(response), true, false); } else { this.writeLn(chalk.redBright(response.message), true, true); } diff --git a/src/cli/services/consoleLog.service.ts b/src/cli/services/consoleLog.service.ts index 5e1904790d..9edd1217ee 100644 --- a/src/cli/services/consoleLog.service.ts +++ b/src/cli/services/consoleLog.service.ts @@ -1,27 +1,10 @@ import { LogLevelType } from '../../enums/logLevelType'; -import { LogService as LogServiceAbstraction } from '../../abstractions/log.service'; +import { ConsoleLogService as BaseConsoleLogService } from '../../services/consoleLog.service'; -export class ConsoleLogService implements LogServiceAbstraction { - constructor(private isDev: boolean, private filter: (level: LogLevelType) => boolean = null) { } - - debug(message: string) { - if (!this.isDev) { - return; - } - this.write(LogLevelType.Debug, message); - } - - info(message: string) { - this.write(LogLevelType.Info, message); - } - - warning(message: string) { - this.write(LogLevelType.Warning, message); - } - - error(message: string) { - this.write(LogLevelType.Error, message); +export class ConsoleLogService extends BaseConsoleLogService { + constructor(isDev: boolean, filter: (level: LogLevelType) => boolean = null) { + super(isDev, filter); } write(level: LogLevelType, message: string) { @@ -29,25 +12,12 @@ export class ConsoleLogService implements LogServiceAbstraction { return; } - switch (level) { - case LogLevelType.Debug: - // tslint:disable-next-line - console.log(message); - break; - case LogLevelType.Info: - // tslint:disable-next-line - console.log(message); - break; - case LogLevelType.Warning: - // tslint:disable-next-line - console.warn(message); - break; - case LogLevelType.Error: - // tslint:disable-next-line - console.error(message); - break; - default: - break; + if (process.env.BW_RESPONSE === 'true') { + // tslint:disable-next-line + console.error(message); + return; } + + super.write(level, message); } } diff --git a/src/electron/services/electronLog.service.ts b/src/electron/services/electronLog.service.ts index c66a398d67..d7ead0c565 100644 --- a/src/electron/services/electronLog.service.ts +++ b/src/electron/services/electronLog.service.ts @@ -5,10 +5,12 @@ import { isDev } from '../utils'; import { LogLevelType } from '../../enums/logLevelType'; -import { LogService as LogServiceAbstraction } from '../../abstractions/log.service'; +import { ConsoleLogService as BaseLogService } from '../../services/consoleLog.service'; -export class ElectronLogService implements LogServiceAbstraction { - constructor(private filter: (level: LogLevelType) => boolean = null, logDir: string = null) { +export class ElectronLogService extends BaseLogService { + + constructor(protected filter: (level: LogLevelType) => boolean = null, logDir: string = null) { + super(isDev(), filter); if (log.transports == null) { return; } @@ -19,26 +21,6 @@ export class ElectronLogService implements LogServiceAbstraction { } } - debug(message: string) { - if (!isDev()) { - return; - } - - this.write(LogLevelType.Debug, message); - } - - info(message: string) { - this.write(LogLevelType.Info, message); - } - - warning(message: string) { - this.write(LogLevelType.Warning, message); - } - - error(message: string) { - this.write(LogLevelType.Error, message); - } - write(level: LogLevelType, message: string) { if (this.filter != null && this.filter(level)) { return; diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index 61c42bfe9b..1b49d16246 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -1,5 +1,7 @@ import * as papa from 'papaparse'; +import { LogService } from '../abstractions/log.service'; + import { ImportResult } from '../models/domain/importResult'; import { CipherView } from '../models/view/cipherView'; @@ -17,9 +19,13 @@ import { CipherType } from '../enums/cipherType'; import { FieldType } from '../enums/fieldType'; import { SecureNoteType } from '../enums/secureNoteType'; +import { ConsoleLogService } from '../services/consoleLog.service'; + export abstract class BaseImporter { organizationId: string = null; + protected logService: LogService = new ConsoleLogService(false); + protected newLineRegex = /(?:\r\n|\r|\n)/; protected passwordFieldNames = [ @@ -88,7 +94,7 @@ export abstract class BaseImporter { result.errors.forEach((e) => { if (e.row != null) { // tslint:disable-next-line - console.warn('Error parsing row ' + e.row + ': ' + e.message); + this.logService.warning('Error parsing row ' + e.row + ': ' + e.message); } }); } diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index de52de67e1..15013b1453 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -17,6 +17,7 @@ import { AppIdService } from '../abstractions/appId.service'; import { AuthService as AuthServiceAbstraction } from '../abstractions/auth.service'; import { CryptoService } from '../abstractions/crypto.service'; import { I18nService } from '../abstractions/i18n.service'; +import { LogService } from '../abstractions/log.service'; import { MessagingService } from '../abstractions/messaging.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { TokenService } from '../abstractions/token.service'; @@ -91,7 +92,9 @@ export class AuthService implements AuthServiceAbstraction { private userService: UserService, private tokenService: TokenService, private appIdService: AppIdService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private messagingService: MessagingService, - private vaultTimeoutService: VaultTimeoutService, private setCryptoKeys = true) { } + private vaultTimeoutService: VaultTimeoutService, private logService: LogService, + private setCryptoKeys = true) { + } init() { TwoFactorProviders[TwoFactorProviderType.Email].name = this.i18nService.t('emailTitle'); @@ -351,7 +354,7 @@ export class AuthService implements AuthServiceAbstraction { tokenResponse.privateKey = keyPair[1].encryptedString; } catch (e) { // tslint:disable-next-line - console.error(e); + this.logService.error(e); } } diff --git a/src/services/consoleLog.service.ts b/src/services/consoleLog.service.ts new file mode 100644 index 0000000000..f6d119b2ce --- /dev/null +++ b/src/services/consoleLog.service.ts @@ -0,0 +1,71 @@ +import { LogLevelType } from '../enums/logLevelType'; + +import { LogService as LogServiceAbstraction } from '../abstractions/log.service'; + +// @ts-ignore: import * as ns from "mod" error, need to do it this way +import hrtime = require('browser-process-hrtime'); + +export class ConsoleLogService implements LogServiceAbstraction { + protected timersMap: Map = new Map(); + + constructor(protected isDev: boolean, protected filter: (level: LogLevelType) => boolean = null) { } + + debug(message: string) { + if (!this.isDev) { + return; + } + this.write(LogLevelType.Debug, message); + } + + info(message: string) { + this.write(LogLevelType.Info, message); + } + + warning(message: string) { + this.write(LogLevelType.Warning, message); + } + + error(message: string) { + this.write(LogLevelType.Error, message); + } + + write(level: LogLevelType, message: string) { + if (this.filter != null && this.filter(level)) { + return; + } + + switch (level) { + case LogLevelType.Debug: + // tslint:disable-next-line + console.log(message); + break; + case LogLevelType.Info: + // tslint:disable-next-line + console.log(message); + break; + case LogLevelType.Warning: + // tslint:disable-next-line + console.warn(message); + break; + case LogLevelType.Error: + // tslint:disable-next-line + console.error(message); + break; + default: + break; + } + } + + time(label: string = 'default') { + if (!this.timersMap.has(label)) { + this.timersMap.set(label, hrtime()); + } + } + + timeEnd(label: string = 'default'): [number, number] { + const elapsed = hrtime(this.timersMap.get(label)); + this.timersMap.delete(label); + this.write(LogLevelType.Info, `${label}: ${elapsed[0] * 1000 + elapsed[1] / 10e6}ms`); + return elapsed; + } +} diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index edc63862df..0b1061a0cf 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -10,6 +10,7 @@ import { ProfileOrganizationResponse } from '../models/response/profileOrganizat import { CryptoService as CryptoServiceAbstraction } from '../abstractions/crypto.service'; import { CryptoFunctionService } from '../abstractions/cryptoFunction.service'; +import { LogService } from '../abstractions/log.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { StorageService } from '../abstractions/storage.service'; @@ -37,7 +38,9 @@ export class CryptoService implements CryptoServiceAbstraction { private orgKeys: Map; constructor(private storageService: StorageService, private secureStorageService: StorageService, - private cryptoFunctionService: CryptoFunctionService, private platformUtilService: PlatformUtilsService) { } + private cryptoFunctionService: CryptoFunctionService, private platformUtilService: PlatformUtilsService, + private logService: LogService) { + } async setKey(key: SymmetricCryptoKey): Promise { this.key = key; @@ -546,14 +549,12 @@ export class CryptoService implements CryptoServiceAbstraction { const theKey = this.resolveLegacyKey(encType, keyForEnc); if (theKey.macKey != null && mac == null) { - // tslint:disable-next-line - console.error('mac required.'); + this.logService.error('mac required.'); return null; } if (theKey.encType !== encType) { - // tslint:disable-next-line - console.error('encType unavailable.'); + this.logService.error('encType unavailable.'); return null; } @@ -563,8 +564,7 @@ export class CryptoService implements CryptoServiceAbstraction { fastParams.macKey, 'sha256'); const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac); if (!macsEqual) { - // tslint:disable-next-line - console.error('mac failed.'); + this.logService.error('mac failed.'); return null; } } @@ -596,8 +596,7 @@ export class CryptoService implements CryptoServiceAbstraction { const macsMatch = await this.cryptoFunctionService.compare(mac, computedMac); if (!macsMatch) { - // tslint:disable-next-line - console.error('mac failed.'); + this.logService.error('mac failed.'); return null; } } diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index 5f5026ad6e..a4df8299f5 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -6,6 +6,7 @@ import { NotificationType } from '../enums/notificationType'; import { ApiService } from '../abstractions/api.service'; import { AppIdService } from '../abstractions/appId.service'; import { EnvironmentService } from '../abstractions/environment.service'; +import { LogService } from '../abstractions/log.service'; import { NotificationsService as NotificationsServiceAbstraction } from '../abstractions/notifications.service'; import { SyncService } from '../abstractions/sync.service'; import { UserService } from '../abstractions/user.service'; @@ -27,7 +28,9 @@ export class NotificationsService implements NotificationsServiceAbstraction { constructor(private userService: UserService, private syncService: SyncService, private appIdService: AppIdService, private apiService: ApiService, - private vaultTimeoutService: VaultTimeoutService, private logoutCallback: () => Promise) { } + private vaultTimeoutService: VaultTimeoutService, + private logoutCallback: () => Promise, private logService: LogService) { + } async init(environmentService: EnvironmentService): Promise { this.inited = false; @@ -87,8 +90,7 @@ export class NotificationsService implements NotificationsServiceAbstraction { await this.signalrConnection.stop(); } } catch (e) { - // tslint:disable-next-line - console.error(e.toString()); + this.logService.error(e.toString()); } } diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 4b992d54f3..4a7207f93b 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -3,6 +3,7 @@ import * as lunr from 'lunr'; import { CipherView } from '../models/view/cipherView'; import { CipherService } from '../abstractions/cipher.service'; +import { LogService } from '../abstractions/log.service'; import { SearchService as SearchServiceAbstraction } from '../abstractions/search.service'; import { CipherType } from '../enums/cipherType'; @@ -13,7 +14,7 @@ export class SearchService implements SearchServiceAbstraction { private indexing = false; private index: lunr.Index = null; - constructor(private cipherService: CipherService) { + constructor(private cipherService: CipherService, private logService: LogService) { } clearIndex(): void { @@ -30,8 +31,8 @@ export class SearchService implements SearchServiceAbstraction { if (this.indexing) { return; } - // tslint:disable-next-line - console.time('search indexing'); + + this.logService.time('search indexing'); this.indexing = true; this.index = null; const builder = new lunr.Builder(); @@ -62,8 +63,8 @@ export class SearchService implements SearchServiceAbstraction { ciphers.forEach((c) => builder.add(c)); this.index = builder.build(); this.indexing = false; - // tslint:disable-next-line - console.timeEnd('search indexing'); + + this.logService.timeEnd('search indexing'); } async searchCiphers(query: string, From cc801ce0d7e200a365bed02c35b8d97666dbeab4 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 15 Dec 2020 10:02:54 -0600 Subject: [PATCH 1120/1626] Has Totp method will be used to enable quick copy (#225) --- src/models/view/loginView.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/models/view/loginView.ts b/src/models/view/loginView.ts index 7c13a33faa..a891f9d231 100644 --- a/src/models/view/loginView.ts +++ b/src/models/view/loginView.ts @@ -1,6 +1,7 @@ import { LoginUriView } from './loginUriView'; import { View } from './view'; +import { Utils } from '../../misc/utils'; import { Login } from '../domain/login'; export class LoginView implements View { @@ -34,6 +35,10 @@ export class LoginView implements View { return this.hasUris && this.uris.some((u) => u.canLaunch); } + get hasTotp(): boolean { + return !Utils.isNullOrWhitespace(this.totp); + } + get launchUri(): string { if (this.hasUris) { const uri = this.uris.find((u) => u.canLaunch); From d7b5f0a26b15472b37aef2248dea51a2aa6d4916 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 15 Dec 2020 21:26:51 +0100 Subject: [PATCH 1121/1626] Avoid destroying the tray icon in linux (#227) --- src/electron/tray.main.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/electron/tray.main.ts b/src/electron/tray.main.ts index bed712f5aa..e34c54b7da 100644 --- a/src/electron/tray.main.ts +++ b/src/electron/tray.main.ts @@ -86,7 +86,9 @@ export class TrayMain { } removeTray(showWindow = true) { - if (this.tray != null) { + // Due to https://github.com/electron/electron/issues/17622 + // we cannot destroy the tray icon on linux. + if (this.tray != null && process.platform !== 'linux') { this.tray.destroy(); this.tray = null; } From f7d8887304daa8c7e19890083999dbf7e719abd5 Mon Sep 17 00:00:00 2001 From: Cedric Wille Date: Tue, 15 Dec 2020 16:04:48 -0500 Subject: [PATCH 1122/1626] Add verification for password hint - the hint should not equal the password for security reasons (#194) --- src/angular/components/register.component.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts index a0f49cf5f1..6149f83f16 100644 --- a/src/angular/components/register.component.ts +++ b/src/angular/components/register.component.ts @@ -112,6 +112,11 @@ export class RegisterComponent { } } + if (this.hint === this.masterPassword) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('hintEqualsPassword')); + return; + } + this.name = this.name === '' ? null : this.name; this.email = this.email.trim().toLowerCase(); const kdf = KdfType.PBKDF2_SHA256; From 58f5369adc1514e7de355e47c3bb28b008d6563c Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Tue, 15 Dec 2020 23:22:24 +0100 Subject: [PATCH 1123/1626] feat(platform-utils): Get and react to changes to the system theme (#161) These changes will allow the WebExtension (and later the desktop application) to respect the system theme. I've added the Electron implementation until I realized that the required API [has been implemented but not released yet](https://www.electronjs.org/docs/api/native-theme/history). Let me know if you I should remove the code. Part of https://github.com/bitwarden/browser/issues/1256. https://www.electronjs.org/docs/api/native-theme Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com> --- src/abstractions/platformUtils.service.ts | 2 ++ src/cli/services/cliPlatformUtils.service.ts | 7 +++++++ .../services/electronPlatformUtils.service.ts | 12 ++++++++++++ 3 files changed, 21 insertions(+) diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index c717b8962b..918528eaf2 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -34,5 +34,7 @@ export abstract class PlatformUtilsService { readFromClipboard: (options?: any) => Promise; supportsBiometric: () => Promise; authenticateBiometric: () => Promise; + getDefaultSystemTheme: () => 'light' | 'dark'; + onDefaultSystemThemeChange: (callback: ((theme: 'light' | 'dark') => unknown)) => unknown; supportsSecureStorage: () => boolean; } diff --git a/src/cli/services/cliPlatformUtils.service.ts b/src/cli/services/cliPlatformUtils.service.ts index 934ee8a7b0..a182854ddf 100644 --- a/src/cli/services/cliPlatformUtils.service.ts +++ b/src/cli/services/cliPlatformUtils.service.ts @@ -146,6 +146,13 @@ export class CliPlatformUtilsService implements PlatformUtilsService { return Promise.resolve(false); } + getDefaultSystemTheme() { + return 'light' as 'light' | 'dark'; + } + + onDefaultSystemThemeChange() { + } + supportsSecureStorage(): boolean { return false; } diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index f4dfacf980..0ea3f2188a 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -3,6 +3,7 @@ import { ipcRenderer, remote, shell, + // nativeTheme, } from 'electron'; import * as fs from 'fs'; @@ -220,6 +221,17 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { }); } + getDefaultSystemTheme() { + return 'light' as 'light' | 'dark'; + // return nativeTheme.shouldUseDarkColors ? 'dark' : 'light'; + } + + onDefaultSystemThemeChange(callback: ((theme: 'light' | 'dark') => unknown)) { + // nativeTheme.on('updated', () => { + // callback(this.getDefaultSystemTheme()); + // }); + } + supportsSecureStorage(): boolean { return true; } From 75ca9f9c13c54b7e348a97105a106b69d3224dce Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 15 Dec 2020 17:49:51 -0600 Subject: [PATCH 1124/1626] Move share button to view page (#229) It is not possible to edit and share at the same time. Browser extension currently utilizes this layout and it is confusing. This change is in conjunction with altering that UI. --- src/angular/components/view.component.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts index cd8c0c4d95..76734978ea 100644 --- a/src/angular/components/view.component.ts +++ b/src/angular/components/view.component.ts @@ -36,6 +36,7 @@ export class ViewComponent implements OnDestroy, OnInit { @Input() cipherId: string; @Output() onEditCipher = new EventEmitter(); @Output() onCloneCipher = new EventEmitter(); + @Output() onShareCipher = new EventEmitter(); @Output() onDeletedCipher = new EventEmitter(); @Output() onRestoredCipher = new EventEmitter(); @@ -114,6 +115,10 @@ export class ViewComponent implements OnDestroy, OnInit { this.onCloneCipher.emit(this.cipher); } + share() { + this.onShareCipher.emit(this.cipher); + } + async delete(): Promise { const confirmed = await this.platformUtilsService.showDialog( this.i18nService.t(this.cipher.isDeleted ? 'permanentlyDeleteItemConfirmation' : 'deleteItemConfirmation'), From ceb78d054ccbd362459bca60bb4b05c16f164ffa Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Tue, 15 Dec 2020 19:16:22 -0500 Subject: [PATCH 1125/1626] Linter build fix (#228) Added comments to empty function block for CLI (doesn't have a "system theme" like other clients) to make linter happy. --- src/cli/services/cliPlatformUtils.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cli/services/cliPlatformUtils.service.ts b/src/cli/services/cliPlatformUtils.service.ts index a182854ddf..333b5c574a 100644 --- a/src/cli/services/cliPlatformUtils.service.ts +++ b/src/cli/services/cliPlatformUtils.service.ts @@ -151,6 +151,7 @@ export class CliPlatformUtilsService implements PlatformUtilsService { } onDefaultSystemThemeChange() { + /* noop */ } supportsSecureStorage(): boolean { From 697e755c0f43119e0811e2c9452c0d9d925970eb Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 16 Dec 2020 15:04:05 -0600 Subject: [PATCH 1126/1626] Add missing event type from server (#231) --- src/enums/eventType.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/enums/eventType.ts b/src/enums/eventType.ts index 154a363bcb..5f4cf268e6 100644 --- a/src/enums/eventType.ts +++ b/src/enums/eventType.ts @@ -39,6 +39,7 @@ export enum EventType { OrganizationUser_Updated = 1502, OrganizationUser_Removed = 1503, OrganizationUser_UpdatedGroups = 1504, + OrganizationUser_UnlinkedSso = 1505, Organization_Updated = 1600, Organization_PurgedVault = 1601, From f9042408f44b299a7cdd1286490b70f5fd2db999 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Wed, 16 Dec 2020 22:07:15 +0100 Subject: [PATCH 1127/1626] Prevent reloading browser extension when using biometric (#230) --- src/services/system.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/system.service.ts b/src/services/system.service.ts index 1ec3a138d6..4edf0e3a2e 100644 --- a/src/services/system.service.ts +++ b/src/services/system.service.ts @@ -19,7 +19,7 @@ export class SystemService implements SystemServiceAbstraction { } startProcessReload(): void { - if (this.vaultTimeoutService.pinProtectedKey != null || this.reloadInterval != null) { + if (this.vaultTimeoutService.pinProtectedKey != null || this.vaultTimeoutService.biometricLocked || this.reloadInterval != null) { return; } this.cancelProcessReload(); From acdbc229533a4f5f57b83b882ce48037a8a17299 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Fri, 18 Dec 2020 09:20:59 -0600 Subject: [PATCH 1128/1626] Force update of tray menu if on linux (#233) --- src/electron/tray.main.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/electron/tray.main.ts b/src/electron/tray.main.ts index e34c54b7da..46b8abc94c 100644 --- a/src/electron/tray.main.ts +++ b/src/electron/tray.main.ts @@ -132,6 +132,12 @@ export class TrayMain { } } + updateContextMenu() { + if (this.contextMenu != null && this.isLinux()) { + this.tray.setContextMenu(this.contextMenu); + } + } + private hideDock() { app.dock.hide(); } @@ -144,6 +150,10 @@ export class TrayMain { return process.platform === 'darwin'; } + private isLinux() { + return process.platform === 'linux'; + } + private async toggleWindow() { if (this.windowMain.win == null) { if (this.isDarwin()) { From 1742228715bdd1161268e9252b7bdfb6f64012d7 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Mon, 21 Dec 2020 19:26:16 +0100 Subject: [PATCH 1129/1626] Make fingerprint optional for browser integration (#234) * Make fingerprint optional for browser integration * Force focus on biometrics * Add dependency --- package-lock.json | 83 +++++++++++++++++++++++++- package.json | 1 + src/electron/biometric.windows.main.ts | 16 +++-- src/electron/electronConstants.ts | 1 + src/globals.d.ts | 1 + 5 files changed, 94 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 32d7e58c5d..09edad89f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -667,6 +667,12 @@ "readable-stream": "^2.0.6" } }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -975,6 +981,14 @@ "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bl": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", @@ -1155,9 +1169,9 @@ "dev": true }, "browser-process-hrtime": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", - "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" }, "browser-resolve": { "version": "1.11.3", @@ -2437,6 +2451,12 @@ "sha.js": "^2.4.8" } }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", @@ -3344,6 +3364,11 @@ "escape-string-regexp": "^1.0.5" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "fileset": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", @@ -3446,6 +3471,15 @@ "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", "dev": true }, + "forcefocus": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/forcefocus/-/forcefocus-1.1.0.tgz", + "integrity": "sha512-bnY7rul5kBLyNoCn0FHNiFAF+GGUZx6TvxWhurUS4PlmOzF+FMixGIigHH5UcyM3w1gp2TxAtP6MOUSXA15Sgw==", + "requires": { + "bindings": "^1.3.0", + "prebuild-install": "^5.0.0" + } + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -5049,6 +5083,12 @@ "pify": "^3.0.0" } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -7545,6 +7585,16 @@ "urix": "^0.1.0" } }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "source-map-url": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", @@ -8121,6 +8171,20 @@ "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "dev": true }, + "ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + } + }, "tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", @@ -8561,6 +8625,13 @@ "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", "requires": { "browser-process-hrtime": "^0.1.2" + }, + "dependencies": { + "browser-process-hrtime": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", + "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==" + } } }, "w3c-xmlserializer": { @@ -8764,6 +8835,12 @@ "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", "dev": true }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, "zone.js": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.9.1.tgz", diff --git a/package.json b/package.json index 196274d87c..b42d22374a 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "electron-log": "2.2.17", "electron-store": "1.3.0", "electron-updater": "4.3.5", + "forcefocus": "^1.1.0", "form-data": "2.3.2", "https-proxy-agent": "4.0.0", "inquirer": "6.2.0", diff --git a/src/electron/biometric.windows.main.ts b/src/electron/biometric.windows.main.ts index 316110b5ec..a51b2e68db 100644 --- a/src/electron/biometric.windows.main.ts +++ b/src/electron/biometric.windows.main.ts @@ -1,16 +1,20 @@ -import { I18nService, StorageService } from '../abstractions'; - import { ipcMain } from 'electron'; -import { BiometricMain } from '../abstractions/biometric.main'; -import { ConstantsService } from '../services'; +import forceFocus from 'forcefocus'; + import { ElectronConstants } from './electronConstants'; +import { WindowMain } from './window.main'; + +import { BiometricMain } from '../abstractions/biometric.main'; +import { I18nService } from '../abstractions/i18n.service'; +import { StorageService } from '../abstractions/storage.service'; +import { ConstantsService } from '../services/constants.service'; export default class BiometricWindowsMain implements BiometricMain { isError: boolean = false; private windowsSecurityCredentialsUiModule: any; - constructor(private storageService: StorageService, private i18nservice: I18nService) { } + constructor(private storageService: StorageService, private i18nservice: I18nService, private windowMain: WindowMain) { } async init() { this.windowsSecurityCredentialsUiModule = this.getWindowsSecurityCredentialsUiModule(); @@ -89,6 +93,8 @@ export default class BiometricWindowsMain implements BiometricMain { } return resolve(result); }); + + forceFocus.focusWindow(this.windowMain.win); } catch (error) { this.isError = true; return reject(error); diff --git a/src/electron/electronConstants.ts b/src/electron/electronConstants.ts index 47510bb68b..1e3f95627c 100644 --- a/src/electron/electronConstants.ts +++ b/src/electron/electronConstants.ts @@ -7,6 +7,7 @@ export class ElectronConstants { static readonly minimizeOnCopyToClipboardKey: string = 'minimizeOnCopyToClipboardKey'; static readonly enableBiometric: string = 'enabledBiometric'; static readonly enableBrowserIntegration: string = 'enableBrowserIntegration'; + static readonly enableBrowserIntegrationFingerprint: string = 'enableBrowserIntegrationFingerprint' static readonly alwaysShowDock: string = 'alwaysShowDock'; static readonly openAtLogin: string = 'openAtLogin'; } diff --git a/src/globals.d.ts b/src/globals.d.ts index 8116ab173f..3d11d40e2c 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -1,3 +1,4 @@ declare function escape(s: string): string; declare function unescape(s: string): string; declare module 'duo_web_sdk'; +declare module 'forcefocus'; From 12321e53b9199e1b4dd4653948b924f1a2b31efc Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Mon, 21 Dec 2020 14:24:04 -0500 Subject: [PATCH 1130/1626] fix lint error, missing semicolon; (#236) --- src/electron/electronConstants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/electron/electronConstants.ts b/src/electron/electronConstants.ts index 1e3f95627c..bf1d1b9c38 100644 --- a/src/electron/electronConstants.ts +++ b/src/electron/electronConstants.ts @@ -7,7 +7,7 @@ export class ElectronConstants { static readonly minimizeOnCopyToClipboardKey: string = 'minimizeOnCopyToClipboardKey'; static readonly enableBiometric: string = 'enabledBiometric'; static readonly enableBrowserIntegration: string = 'enableBrowserIntegration'; - static readonly enableBrowserIntegrationFingerprint: string = 'enableBrowserIntegrationFingerprint' + static readonly enableBrowserIntegrationFingerprint: string = 'enableBrowserIntegrationFingerprint'; static readonly alwaysShowDock: string = 'alwaysShowDock'; static readonly openAtLogin: string = 'openAtLogin'; } From 573eea66eeff1a763e7ed0477fbba51e02c2c5b1 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 22 Dec 2020 16:53:48 +0100 Subject: [PATCH 1131/1626] Add support for Emergency Access (#204) * Add support for Emergency Access * Resolve review comments --- src/abstractions/api.service.ts | 27 +++++ src/abstractions/crypto.service.ts | 3 +- .../components/change-password.component.ts | 80 +++++++------ src/enums/emergencyAccessStatusType.ts | 7 ++ src/enums/emergencyAccessType.ts | 5 + src/models/domain/attachment.ts | 6 +- src/models/domain/card.ts | 5 +- src/models/domain/cipher.ts | 19 +-- src/models/domain/field.ts | 5 +- src/models/domain/identity.ts | 5 +- src/models/domain/login.ts | 7 +- src/models/domain/loginUri.ts | 5 +- src/models/domain/password.ts | 5 +- src/models/domain/secureNote.ts | 3 +- .../request/emergencyAccessAcceptRequest.ts | 3 + .../request/emergencyAccessConfirmRequest.ts | 3 + .../request/emergencyAccessInviteRequest.ts | 7 ++ .../request/emergencyAccessPasswordRequest.ts | 4 + .../request/emergencyAccessUpdateRequest.ts | 7 ++ .../response/emergencyAccessResponse.ts | 81 +++++++++++++ src/services/api.service.ts | 78 +++++++++++++ src/services/crypto.service.ts | 110 +++++++++--------- 22 files changed, 358 insertions(+), 117 deletions(-) create mode 100644 src/enums/emergencyAccessStatusType.ts create mode 100644 src/enums/emergencyAccessType.ts create mode 100644 src/models/request/emergencyAccessAcceptRequest.ts create mode 100644 src/models/request/emergencyAccessConfirmRequest.ts create mode 100644 src/models/request/emergencyAccessInviteRequest.ts create mode 100644 src/models/request/emergencyAccessPasswordRequest.ts create mode 100644 src/models/request/emergencyAccessUpdateRequest.ts create mode 100644 src/models/response/emergencyAccessResponse.ts diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 65b8356074..955041f281 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -15,6 +15,11 @@ import { CollectionRequest } from '../models/request/collectionRequest'; import { DeleteRecoverRequest } from '../models/request/deleteRecoverRequest'; import { EmailRequest } from '../models/request/emailRequest'; import { EmailTokenRequest } from '../models/request/emailTokenRequest'; +import { EmergencyAccessAcceptRequest } from '../models/request/emergencyAccessAcceptRequest'; +import { EmergencyAccessConfirmRequest } from '../models/request/emergencyAccessConfirmRequest'; +import { EmergencyAccessInviteRequest } from '../models/request/emergencyAccessInviteRequest'; +import { EmergencyAccessPasswordRequest } from '../models/request/emergencyAccessPasswordRequest'; +import { EmergencyAccessUpdateRequest } from '../models/request/emergencyAccessUpdateRequest'; import { EventRequest } from '../models/request/eventRequest'; import { FolderRequest } from '../models/request/folderRequest'; import { GroupRequest } from '../models/request/groupRequest'; @@ -73,6 +78,12 @@ import { CollectionResponse, } from '../models/response/collectionResponse'; import { DomainsResponse } from '../models/response/domainsResponse'; +import { + EmergencyAccessGranteeDetailsResponse, + EmergencyAccessGrantorDetailsResponse, + EmergencyAccessTakeoverResponse, + EmergencyAccessViewResponse +} from '../models/response/emergencyAccessResponse'; import { EventResponse } from '../models/response/eventResponse'; import { FolderResponse } from '../models/response/folderResponse'; import { @@ -278,6 +289,22 @@ export abstract class ApiService { postTwoFactorEmailSetup: (request: TwoFactorEmailRequest) => Promise; postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise; + getEmergencyAccessTrusted: () => Promise>; + getEmergencyAccessGranted: () => Promise>; + getEmergencyAccess: (id: string) => Promise; + putEmergencyAccess: (id: string, request: EmergencyAccessUpdateRequest) => Promise; + deleteEmergencyAccess: (id: string) => Promise; + postEmergencyAccessInvite: (request: EmergencyAccessInviteRequest) => Promise; + postEmergencyAccessReinvite: (id: string) => Promise; + postEmergencyAccessAccept: (id: string, request: EmergencyAccessAcceptRequest) => Promise; + postEmergencyAccessConfirm: (id: string, request: EmergencyAccessConfirmRequest) => Promise; + postEmergencyAccessInitiate: (id: string) => Promise; + postEmergencyAccessApprove: (id: string) => Promise; + postEmergencyAccessReject: (id: string) => Promise; + postEmergencyAccessTakeover: (id: string) => Promise; + postEmergencyAccessPassword: (id: string, request: EmergencyAccessPasswordRequest) => Promise; + postEmergencyAccessView: (id: string) => Promise; + getOrganization: (id: string) => Promise; getOrganizationBilling: (id: string) => Promise; getOrganizationSubscription: (id: string) => Promise; diff --git a/src/abstractions/crypto.service.ts b/src/abstractions/crypto.service.ts index 53376e19e9..6fa312fc51 100644 --- a/src/abstractions/crypto.service.ts +++ b/src/abstractions/crypto.service.ts @@ -38,10 +38,11 @@ export abstract class CryptoService { makeSendKey: (keyMaterial: ArrayBuffer) => Promise; hashPassword: (password: string, key: SymmetricCryptoKey) => Promise; makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; - remakeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; + remakeEncKey: (key: SymmetricCryptoKey, encKey?: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, CipherString]>; encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise; encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise; rsaEncrypt: (data: ArrayBuffer, publicKey?: ArrayBuffer) => Promise; + rsaDecrypt: (encValue: string) => Promise; decryptToBytes: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise; decryptToUtf8: (cipherString: CipherString, key?: SymmetricCryptoKey) => Promise; decryptFromBytes: (encBuf: ArrayBuffer, key: SymmetricCryptoKey) => Promise; diff --git a/src/angular/components/change-password.component.ts b/src/angular/components/change-password.component.ts index f4ec66e31e..eaab13f7ac 100644 --- a/src/angular/components/change-password.component.ts +++ b/src/angular/components/change-password.component.ts @@ -21,11 +21,11 @@ export class ChangePasswordComponent implements OnInit { masterPasswordScore: number; enforcedPolicyOptions: MasterPasswordPolicyOptions; + protected email: string; protected kdf: KdfType; protected kdfIterations: number; private masterPasswordStrengthTimeout: any; - private email: string; constructor(protected i18nService: I18nService, protected cryptoService: CryptoService, protected messagingService: MessagingService, protected userService: UserService, @@ -58,43 +58,9 @@ export class ChangePasswordComponent implements OnInit { } async submit() { - if (this.masterPassword == null || this.masterPassword === '') { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPassRequired')); + if (!await this.strongPassword()) { return; } - if (this.masterPassword.length < 8) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPassLength')); - return; - } - if (this.masterPassword !== this.masterPasswordRetype) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPassDoesntMatch')); - return; - } - - const strengthResult = this.passwordGenerationService.passwordStrength(this.masterPassword, - this.getPasswordStrengthUserInput()); - - if (this.enforcedPolicyOptions != null && - !this.policyService.evaluateMasterPassword( - strengthResult.score, - this.masterPassword, - this.enforcedPolicyOptions)) { - this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPasswordPolicyRequirementsNotMet')); - return; - } - - if (strengthResult != null && strengthResult.score < 3) { - const result = await this.platformUtilsService.showDialog(this.i18nService.t('weakMasterPasswordDesc'), - this.i18nService.t('weakMasterPassword'), this.i18nService.t('yes'), this.i18nService.t('no'), - 'warning'); - if (!result) { - return; - } - } if (!await this.setupSubmitActions()) { return; @@ -133,6 +99,48 @@ export class ChangePasswordComponent implements OnInit { // Override in sub-class } + async strongPassword(): Promise { + if (this.masterPassword == null || this.masterPassword === '') { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassRequired')); + return false; + } + if (this.masterPassword.length < 8) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassLength')); + return false; + } + if (this.masterPassword !== this.masterPasswordRetype) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassDoesntMatch')); + return false; + } + + const strengthResult = this.passwordGenerationService.passwordStrength(this.masterPassword, + this.getPasswordStrengthUserInput()); + + if (this.enforcedPolicyOptions != null && + !this.policyService.evaluateMasterPassword( + strengthResult.score, + this.masterPassword, + this.enforcedPolicyOptions)) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPasswordPolicyRequirementsNotMet')); + return false; + } + + if (strengthResult != null && strengthResult.score < 3) { + const result = await this.platformUtilsService.showDialog(this.i18nService.t('weakMasterPasswordDesc'), + this.i18nService.t('weakMasterPassword'), this.i18nService.t('yes'), this.i18nService.t('no'), + 'warning'); + if (!result) { + return false; + } + } + + return true; + } + updatePasswordStrength() { if (this.masterPasswordStrengthTimeout != null) { clearTimeout(this.masterPasswordStrengthTimeout); diff --git a/src/enums/emergencyAccessStatusType.ts b/src/enums/emergencyAccessStatusType.ts new file mode 100644 index 0000000000..eb362edfcc --- /dev/null +++ b/src/enums/emergencyAccessStatusType.ts @@ -0,0 +1,7 @@ +export enum EmergencyAccessStatusType { + Invited = 0, + Accepted = 1, + Confirmed = 2, + RecoveryInitiated = 3, + RecoveryApproved = 4, +} diff --git a/src/enums/emergencyAccessType.ts b/src/enums/emergencyAccessType.ts new file mode 100644 index 0000000000..803634f4e7 --- /dev/null +++ b/src/enums/emergencyAccessType.ts @@ -0,0 +1,5 @@ +export enum EmergencyAccessType +{ + View = 0, + Takeover = 1, +} diff --git a/src/models/domain/attachment.ts b/src/models/domain/attachment.ts index 838b83b9e6..096753c1ec 100644 --- a/src/models/domain/attachment.ts +++ b/src/models/domain/attachment.ts @@ -34,10 +34,10 @@ export class Attachment extends Domain { }, alreadyEncrypted, ['id', 'url', 'sizeName']); } - async decrypt(orgId: string): Promise { + async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { const view = await this.decryptObj(new AttachmentView(this), { fileName: null, - }, orgId); + }, orgId, encKey); if (this.key != null) { let cryptoService: CryptoService; @@ -50,7 +50,7 @@ export class Attachment extends Domain { try { const orgKey = await cryptoService.getOrgKey(orgId); - const decValue = await cryptoService.decryptToBytes(this.key, orgKey); + const decValue = await cryptoService.decryptToBytes(this.key, orgKey ?? encKey); view.key = new SymmetricCryptoKey(decValue); } catch (e) { // TODO: error? diff --git a/src/models/domain/card.ts b/src/models/domain/card.ts index c1ef7a22d3..7f294eb243 100644 --- a/src/models/domain/card.ts +++ b/src/models/domain/card.ts @@ -4,6 +4,7 @@ import { CipherString } from './cipherString'; import Domain from './domainBase'; import { CardView } from '../view/cardView'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; export class Card extends Domain { cardholderName: CipherString; @@ -29,7 +30,7 @@ export class Card extends Domain { }, alreadyEncrypted, []); } - decrypt(orgId: string): Promise { + decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { return this.decryptObj(new CardView(this), { cardholderName: null, brand: null, @@ -37,7 +38,7 @@ export class Card extends Domain { expMonth: null, expYear: null, code: null, - }, orgId); + }, orgId, encKey); } toCardData(): CardData { diff --git a/src/models/domain/cipher.ts b/src/models/domain/cipher.ts index 9b31235a77..ddf78557e1 100644 --- a/src/models/domain/cipher.ts +++ b/src/models/domain/cipher.ts @@ -13,6 +13,7 @@ import { Identity } from './identity'; import { Login } from './login'; import { Password } from './password'; import { SecureNote } from './secureNote'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; export class Cipher extends Domain { id: string; @@ -102,26 +103,26 @@ export class Cipher extends Domain { } } - async decrypt(): Promise { + async decrypt(encKey?: SymmetricCryptoKey): Promise { const model = new CipherView(this); await this.decryptObj(model, { name: null, notes: null, - }, this.organizationId); + }, this.organizationId, encKey); switch (this.type) { case CipherType.Login: - model.login = await this.login.decrypt(this.organizationId); + model.login = await this.login.decrypt(this.organizationId, encKey); break; case CipherType.SecureNote: - model.secureNote = await this.secureNote.decrypt(this.organizationId); + model.secureNote = await this.secureNote.decrypt(this.organizationId, encKey); break; case CipherType.Card: - model.card = await this.card.decrypt(this.organizationId); + model.card = await this.card.decrypt(this.organizationId, encKey); break; case CipherType.Identity: - model.identity = await this.identity.decrypt(this.organizationId); + model.identity = await this.identity.decrypt(this.organizationId, encKey); break; default: break; @@ -133,7 +134,7 @@ export class Cipher extends Domain { const attachments: any[] = []; await this.attachments.reduce((promise, attachment) => { return promise.then(() => { - return attachment.decrypt(orgId); + return attachment.decrypt(orgId, encKey); }).then((decAttachment) => { attachments.push(decAttachment); }); @@ -145,7 +146,7 @@ export class Cipher extends Domain { const fields: any[] = []; await this.fields.reduce((promise, field) => { return promise.then(() => { - return field.decrypt(orgId); + return field.decrypt(orgId, encKey); }).then((decField) => { fields.push(decField); }); @@ -157,7 +158,7 @@ export class Cipher extends Domain { const passwordHistory: any[] = []; await this.passwordHistory.reduce((promise, ph) => { return promise.then(() => { - return ph.decrypt(orgId); + return ph.decrypt(orgId, encKey); }).then((decPh) => { passwordHistory.push(decPh); }); diff --git a/src/models/domain/field.ts b/src/models/domain/field.ts index 2dec447f2b..ac345805a1 100644 --- a/src/models/domain/field.ts +++ b/src/models/domain/field.ts @@ -6,6 +6,7 @@ import { CipherString } from './cipherString'; import Domain from './domainBase'; import { FieldView } from '../view/fieldView'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; export class Field extends Domain { name: CipherString; @@ -25,11 +26,11 @@ export class Field extends Domain { }, alreadyEncrypted, []); } - decrypt(orgId: string): Promise { + decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { return this.decryptObj(new FieldView(this), { name: null, value: null, - }, orgId); + }, orgId, encKey); } toFieldData(): FieldData { diff --git a/src/models/domain/identity.ts b/src/models/domain/identity.ts index 87622e6c6a..81ed571d10 100644 --- a/src/models/domain/identity.ts +++ b/src/models/domain/identity.ts @@ -2,6 +2,7 @@ import { IdentityData } from '../data/identityData'; import { CipherString } from './cipherString'; import Domain from './domainBase'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; import { IdentityView } from '../view/identityView'; @@ -53,7 +54,7 @@ export class Identity extends Domain { }, alreadyEncrypted, []); } - decrypt(orgId: string): Promise { + decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { return this.decryptObj(new IdentityView(this), { title: null, firstName: null, @@ -73,7 +74,7 @@ export class Identity extends Domain { username: null, passportNumber: null, licenseNumber: null, - }, orgId); + }, orgId, encKey); } toIdentityData(): IdentityData { diff --git a/src/models/domain/login.ts b/src/models/domain/login.ts index c40a830aba..c8be800447 100644 --- a/src/models/domain/login.ts +++ b/src/models/domain/login.ts @@ -6,6 +6,7 @@ import { LoginView } from '../view/loginView'; import { CipherString } from './cipherString'; import Domain from './domainBase'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; export class Login extends Domain { uris: LoginUri[]; @@ -35,17 +36,17 @@ export class Login extends Domain { } } - async decrypt(orgId: string): Promise { + async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { const view = await this.decryptObj(new LoginView(this), { username: null, password: null, totp: null, - }, orgId); + }, orgId, encKey); if (this.uris != null) { view.uris = []; for (let i = 0; i < this.uris.length; i++) { - const uri = await this.uris[i].decrypt(orgId); + const uri = await this.uris[i].decrypt(orgId, encKey); view.uris.push(uri); } } diff --git a/src/models/domain/loginUri.ts b/src/models/domain/loginUri.ts index 631d5b0d02..e688b757b9 100644 --- a/src/models/domain/loginUri.ts +++ b/src/models/domain/loginUri.ts @@ -6,6 +6,7 @@ import { LoginUriView } from '../view/loginUriView'; import { CipherString } from './cipherString'; import Domain from './domainBase'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; export class LoginUri extends Domain { uri: CipherString; @@ -23,10 +24,10 @@ export class LoginUri extends Domain { }, alreadyEncrypted, []); } - decrypt(orgId: string): Promise { + decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { return this.decryptObj(new LoginUriView(this), { uri: null, - }, orgId); + }, orgId, encKey); } toLoginUriData(): LoginUriData { diff --git a/src/models/domain/password.ts b/src/models/domain/password.ts index 72c374b6f8..4bd4023a60 100644 --- a/src/models/domain/password.ts +++ b/src/models/domain/password.ts @@ -4,6 +4,7 @@ import { CipherString } from './cipherString'; import Domain from './domainBase'; import { PasswordHistoryView } from '../view/passwordHistoryView'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; export class Password extends Domain { password: CipherString; @@ -21,10 +22,10 @@ export class Password extends Domain { this.lastUsedDate = new Date(obj.lastUsedDate); } - decrypt(orgId: string): Promise { + decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { return this.decryptObj(new PasswordHistoryView(this), { password: null, - }, orgId); + }, orgId, encKey); } toPasswordHistoryData(): PasswordHistoryData { diff --git a/src/models/domain/secureNote.ts b/src/models/domain/secureNote.ts index 620571fbb7..91b23fff99 100644 --- a/src/models/domain/secureNote.ts +++ b/src/models/domain/secureNote.ts @@ -5,6 +5,7 @@ import { SecureNoteData } from '../data/secureNoteData'; import Domain from './domainBase'; import { SecureNoteView } from '../view/secureNoteView'; +import { SymmetricCryptoKey } from './symmetricCryptoKey'; export class SecureNote extends Domain { type: SecureNoteType; @@ -18,7 +19,7 @@ export class SecureNote extends Domain { this.type = obj.type; } - decrypt(orgId: string): Promise { + decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { return Promise.resolve(new SecureNoteView(this)); } diff --git a/src/models/request/emergencyAccessAcceptRequest.ts b/src/models/request/emergencyAccessAcceptRequest.ts new file mode 100644 index 0000000000..ff6f41c482 --- /dev/null +++ b/src/models/request/emergencyAccessAcceptRequest.ts @@ -0,0 +1,3 @@ +export class EmergencyAccessAcceptRequest { + token: string; +} diff --git a/src/models/request/emergencyAccessConfirmRequest.ts b/src/models/request/emergencyAccessConfirmRequest.ts new file mode 100644 index 0000000000..5b138c8487 --- /dev/null +++ b/src/models/request/emergencyAccessConfirmRequest.ts @@ -0,0 +1,3 @@ +export class EmergencyAccessConfirmRequest { + key: string; +} diff --git a/src/models/request/emergencyAccessInviteRequest.ts b/src/models/request/emergencyAccessInviteRequest.ts new file mode 100644 index 0000000000..f97620d584 --- /dev/null +++ b/src/models/request/emergencyAccessInviteRequest.ts @@ -0,0 +1,7 @@ +import { EmergencyAccessType } from '../../enums/emergencyAccessType'; + +export class EmergencyAccessInviteRequest { + email: string; + type: EmergencyAccessType; + waitTimeDays: number; +} diff --git a/src/models/request/emergencyAccessPasswordRequest.ts b/src/models/request/emergencyAccessPasswordRequest.ts new file mode 100644 index 0000000000..9d95f86b13 --- /dev/null +++ b/src/models/request/emergencyAccessPasswordRequest.ts @@ -0,0 +1,4 @@ +export class EmergencyAccessPasswordRequest { + newMasterPasswordHash: string; + key: string; +} diff --git a/src/models/request/emergencyAccessUpdateRequest.ts b/src/models/request/emergencyAccessUpdateRequest.ts new file mode 100644 index 0000000000..ed535cb04b --- /dev/null +++ b/src/models/request/emergencyAccessUpdateRequest.ts @@ -0,0 +1,7 @@ +import { EmergencyAccessType } from '../../enums/emergencyAccessType'; + +export class EmergencyAccessUpdateRequest { + type: EmergencyAccessType; + waitTimeDays: number; + keyEncrypted?: string; +} diff --git a/src/models/response/emergencyAccessResponse.ts b/src/models/response/emergencyAccessResponse.ts new file mode 100644 index 0000000000..fbd6084938 --- /dev/null +++ b/src/models/response/emergencyAccessResponse.ts @@ -0,0 +1,81 @@ +import { EmergencyAccessStatusType } from '../../enums/emergencyAccessStatusType'; +import { EmergencyAccessType } from '../../enums/emergencyAccessType'; +import { KdfType } from '../../enums/kdfType'; +import { BaseResponse } from './baseResponse'; +import { CipherResponse } from './cipherResponse'; + +export class EmergencyAccessGranteeDetailsResponse extends BaseResponse { + id: string; + granteeId: string; + name: string; + email: string; + type: EmergencyAccessType; + status: EmergencyAccessStatusType; + waitTimeDays: number; + creationDate: string; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty('Id'); + this.granteeId = this.getResponseProperty('GranteeId'); + this.name = this.getResponseProperty('Name'); + this.email = this.getResponseProperty('Email'); + this.type = this.getResponseProperty('Type'); + this.status = this.getResponseProperty('Status'); + this.waitTimeDays = this.getResponseProperty('WaitTimeDays'); + this.creationDate = this.getResponseProperty('CreationDate'); + } +} + +export class EmergencyAccessGrantorDetailsResponse extends BaseResponse { + id: string; + grantorId: string; + name: string; + email: string; + type: EmergencyAccessType; + status: EmergencyAccessStatusType; + waitTimeDays: number; + creationDate: string; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty('Id'); + this.grantorId = this.getResponseProperty('GrantorId'); + this.name = this.getResponseProperty('Name'); + this.email = this.getResponseProperty('Email'); + this.type = this.getResponseProperty('Type'); + this.status = this.getResponseProperty('Status'); + this.waitTimeDays = this.getResponseProperty('WaitTimeDays'); + this.creationDate = this.getResponseProperty('CreationDate'); + } +} + +export class EmergencyAccessTakeoverResponse extends BaseResponse { + keyEncrypted: string; + kdf: KdfType; + kdfIterations: number; + + constructor(response: any) { + super(response); + + this.keyEncrypted = this.getResponseProperty('KeyEncrypted'); + this.kdf = this.getResponseProperty('Kdf'); + this.kdfIterations = this.getResponseProperty('KdfIterations'); + } +} + +export class EmergencyAccessViewResponse extends BaseResponse { + keyEncrypted: string; + ciphers: CipherResponse[] = []; + + constructor(response: any) { + super(response); + + this.keyEncrypted = this.getResponseProperty('KeyEncrypted'); + + const ciphers = this.getResponseProperty('Ciphers'); + if (ciphers != null) { + this.ciphers = ciphers.map((c: any) => new CipherResponse(c)); + } + } +} diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 037c8c6d9c..e396839108 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -19,6 +19,11 @@ import { CollectionRequest } from '../models/request/collectionRequest'; import { DeleteRecoverRequest } from '../models/request/deleteRecoverRequest'; import { EmailRequest } from '../models/request/emailRequest'; import { EmailTokenRequest } from '../models/request/emailTokenRequest'; +import { EmergencyAccessAcceptRequest } from '../models/request/emergencyAccessAcceptRequest'; +import { EmergencyAccessConfirmRequest } from '../models/request/emergencyAccessConfirmRequest'; +import { EmergencyAccessInviteRequest } from '../models/request/emergencyAccessInviteRequest'; +import { EmergencyAccessPasswordRequest } from '../models/request/emergencyAccessPasswordRequest'; +import { EmergencyAccessUpdateRequest } from '../models/request/emergencyAccessUpdateRequest'; import { EventRequest } from '../models/request/eventRequest'; import { FolderRequest } from '../models/request/folderRequest'; import { GroupRequest } from '../models/request/groupRequest'; @@ -77,6 +82,12 @@ import { CollectionResponse, } from '../models/response/collectionResponse'; import { DomainsResponse } from '../models/response/domainsResponse'; +import { + EmergencyAccessGranteeDetailsResponse, + EmergencyAccessGrantorDetailsResponse, + EmergencyAccessTakeoverResponse, + EmergencyAccessViewResponse +} from '../models/response/emergencyAccessResponse'; import { ErrorResponse } from '../models/response/errorResponse'; import { EventResponse } from '../models/response/eventResponse'; import { FolderResponse } from '../models/response/folderResponse'; @@ -901,6 +912,73 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/two-factor/send-email-login', request, false, false); } + // Emergency Access APIs + + async getEmergencyAccessTrusted(): Promise> { + const r = await this.send('GET', '/emergency-access/trusted', null, true, true); + return new ListResponse(r, EmergencyAccessGranteeDetailsResponse); + } + + async getEmergencyAccessGranted(): Promise> { + const r = await this.send('GET', '/emergency-access/granted', null, true, true); + return new ListResponse(r, EmergencyAccessGrantorDetailsResponse); + } + + async getEmergencyAccess(id: string): Promise { + const r = await this.send('GET', '/emergency-access/' + id, null, true, true); + return new EmergencyAccessGranteeDetailsResponse(r); + } + + putEmergencyAccess(id: string, request: EmergencyAccessUpdateRequest): Promise { + return this.send('PUT', '/emergency-access/' + id, request, true, false); + } + + deleteEmergencyAccess(id: string): Promise { + return this.send('DELETE', '/emergency-access/' + id, null, true, false); + } + + postEmergencyAccessInvite(request: EmergencyAccessInviteRequest): Promise { + return this.send('POST', '/emergency-access/invite', request, true, false); + } + + postEmergencyAccessReinvite(id: string): Promise { + return this.send('POST', '/emergency-access/' + id + '/reinvite', null, true, false); + } + + postEmergencyAccessAccept(id: string, request: EmergencyAccessAcceptRequest): Promise { + return this.send('POST', '/emergency-access/' + id + '/accept', request, true, false); + } + + postEmergencyAccessConfirm(id: string, request: EmergencyAccessConfirmRequest): Promise { + return this.send('POST', '/emergency-access/' + id + '/confirm', request, true, false); + } + + postEmergencyAccessInitiate(id: string): Promise { + return this.send('POST', '/emergency-access/' + id + '/initiate', null, true, false); + } + + postEmergencyAccessApprove(id: string): Promise { + return this.send('POST', '/emergency-access/' + id + '/approve', null, true, false); + } + + postEmergencyAccessReject(id: string): Promise { + return this.send('POST', '/emergency-access/' + id + '/reject', null, true, false); + } + + async postEmergencyAccessTakeover(id: string): Promise { + const r = await this.send('POST', '/emergency-access/' + id + '/takeover', null, true, true); + return new EmergencyAccessTakeoverResponse(r); + } + + async postEmergencyAccessPassword(id: string, request: EmergencyAccessPasswordRequest): Promise { + const r = await this.send('POST', '/emergency-access/' + id + '/password', request, true, true); + } + + async postEmergencyAccessView(id: string): Promise { + const r = await this.send('POST', '/emergency-access/' + id + '/view', null, true, true); + return new EmergencyAccessViewResponse(r); + } + // Organization APIs async getOrganization(id: string): Promise { diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 0b1061a0cf..f2040caf81 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -380,8 +380,10 @@ export class CryptoService implements CryptoServiceAbstraction { return this.buildEncKey(theKey, encKey); } - async remakeEncKey(key: SymmetricCryptoKey): Promise<[SymmetricCryptoKey, CipherString]> { - const encKey = await this.getEncKey(); + async remakeEncKey(key: SymmetricCryptoKey, encKey?: SymmetricCryptoKey): Promise<[SymmetricCryptoKey, CipherString]> { + if (encKey == null) { + encKey = await this.getEncKey(); + } return this.buildEncKey(key, encKey.key); } @@ -434,6 +436,58 @@ export class CryptoService implements CryptoServiceAbstraction { return new CipherString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encBytes)); } + 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: + // HmacSha256 types are deprecated + case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: + case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: + break; + default: + throw new Error('encType unavailable.'); + } + + if (encPieces == null || encPieces.length <= 0) { + throw new Error('encPieces unavailable.'); + } + + const data = Utils.fromB64ToArray(encPieces[0]).buffer; + const privateKey = await this.getPrivateKey(); + if (privateKey == null) { + throw new Error('No private key.'); + } + + let alg: 'sha1' | 'sha256' = 'sha1'; + switch (encType) { + case EncryptionType.Rsa2048_OaepSha256_B64: + case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: + alg = 'sha256'; + break; + case EncryptionType.Rsa2048_OaepSha1_B64: + case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: + break; + default: + throw new Error('encType unavailable.'); + } + + return this.cryptoFunctionService.rsaDecrypt(data, privateKey, alg); + } + async decryptToBytes(cipherString: CipherString, key?: SymmetricCryptoKey): Promise { const iv = Utils.fromB64ToArray(cipherString.iv).buffer; const data = Utils.fromB64ToArray(cipherString.data).buffer; @@ -604,58 +658,6 @@ export class CryptoService implements CryptoServiceAbstraction { return await this.cryptoFunctionService.aesDecrypt(data, iv, theKey.encKey); } - private 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: - // HmacSha256 types are deprecated - case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: - case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: - break; - default: - throw new Error('encType unavailable.'); - } - - if (encPieces == null || encPieces.length <= 0) { - throw new Error('encPieces unavailable.'); - } - - const data = Utils.fromB64ToArray(encPieces[0]).buffer; - const privateKey = await this.getPrivateKey(); - if (privateKey == null) { - throw new Error('No private key.'); - } - - let alg: 'sha1' | 'sha256' = 'sha1'; - switch (encType) { - case EncryptionType.Rsa2048_OaepSha256_B64: - case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: - alg = 'sha256'; - break; - case EncryptionType.Rsa2048_OaepSha1_B64: - case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: - break; - default: - throw new Error('encType unavailable.'); - } - - return this.cryptoFunctionService.rsaDecrypt(data, privateKey, alg); - } - private async getKeyForEncryption(key?: SymmetricCryptoKey): Promise { if (key != null) { return key; From 91c61aea58eeed427a8f5c5c2a2673e415028ac4 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 22 Dec 2020 16:14:22 -0500 Subject: [PATCH 1132/1626] fix org getter on import and export warning dialog (#238) --- src/angular/components/export.component.ts | 9 +++++++++ src/importers/baseImporter.ts | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/angular/components/export.component.ts b/src/angular/components/export.component.ts index 38ade297ea..d9e4abdd4c 100644 --- a/src/angular/components/export.component.ts +++ b/src/angular/components/export.component.ts @@ -35,6 +35,15 @@ export class ExportComponent { return; } + const acceptedWarning = await this.platformUtilsService.showDialog( + this.i18nService.t(this.encryptedFormat ? 'encExportWarningDesc' : 'exportWarningDesc'), + this.i18nService.t('confirmVaultExport'), this.i18nService.t('exportVault'), + this.i18nService.t('cancel'), 'warning'); + + if (!acceptedWarning) { + return; + } + const keyHash = await this.cryptoService.hashPassword(this.masterPassword, null); const storedKeyHash = await this.cryptoService.getKeyHash(); if (storedKeyHash != null && keyHash != null && storedKeyHash === keyHash) { diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index 1b49d16246..5fb43f3fef 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -76,7 +76,7 @@ export abstract class BaseImporter { skipEmptyLines: false, }; - protected organization() { + protected get organization() { return this.organizationId != null; } From 48144a7eaea34984d98b5f22e14a73a24348dcd2 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Tue, 29 Dec 2020 15:42:11 -0600 Subject: [PATCH 1133/1626] Fixed UI bug with cloning item when personal ownership is not allowed (#240) --- src/angular/components/add-edit.component.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 09cdf1343b..567dbadb45 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -209,6 +209,10 @@ export class AddEditComponent implements OnInit { // Adjust Cipher Name if Cloning if (this.cloneMode) { this.cipher.name += ' - ' + this.i18nService.t('clone'); + // If not allowing personal ownership, update cipher's org Id to prompt downstream changes + if (this.cipher.organizationId == null && !this.allowPersonal) { + this.cipher.organizationId = this.organizationId; + } } } else { this.cipher = new CipherView(); From 14200823488a8b7063396ae23816124ad30a8371 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 30 Dec 2020 15:08:02 -0600 Subject: [PATCH 1134/1626] Do not export trashed items (#241) * Do not export trashed items * Test Item exporting Does not test organization export. Export's use of apiService is not very testable. We will either need a testApiService or to refactor apiService to make mocking easier. * Linter fixes --- package.json | 1 + spec/common/services/export.service.spec.ts | 120 ++++++++++++++++++++ spec/support/karma.conf.js | 1 + spec/utils.ts | 16 +++ src/services/export.service.ts | 23 +++- 5 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 spec/common/services/export.service.spec.ts create mode 100644 spec/utils.ts diff --git a/package.json b/package.json index b42d22374a..ca426d8e23 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "test:node:watch": "concurrently -k -n TSC,Node -c yellow,cyan \"npm run build:watch\" \"nodemon -w ./dist --delay 500ms --exec jasmine\"" }, "devDependencies": { + "@fluffy-spoon/substitute": "^1.179.0", "@types/commander": "^2.12.2", "@types/form-data": "^2.2.1", "@types/inquirer": "^0.0.43", diff --git a/spec/common/services/export.service.spec.ts b/spec/common/services/export.service.spec.ts new file mode 100644 index 0000000000..4cbcf2831a --- /dev/null +++ b/spec/common/services/export.service.spec.ts @@ -0,0 +1,120 @@ +import { Substitute, SubstituteOf } from '@fluffy-spoon/substitute'; + +import { ApiService } from '../../../src/abstractions/api.service'; +import { CipherService } from '../../../src/abstractions/cipher.service'; +import { FolderService } from '../../../src/abstractions/folder.service'; + +import { ExportService } from '../../../src/services/export.service'; + +import { Cipher } from '../../../src/models/domain/cipher'; +import { CipherString } from '../../../src/models/domain/cipherString'; +import { Login } from '../../../src/models/domain/login'; +import { CipherWithIds as CipherExport } from '../../../src/models/export/cipherWithIds'; + +import { CipherType } from '../../../src/enums/cipherType'; +import { CipherView } from '../../../src/models/view/cipherView'; +import { LoginView } from '../../../src/models/view/loginView'; + +import { BuildTestObject, GetUniqueString } from '../../utils'; + +const UserCipherViews = [ + generateCipherView(false), + generateCipherView(false), + generateCipherView(true) +]; + +const UserCipherDomains = [ + generateCipherDomain(false), + generateCipherDomain(false), + generateCipherDomain(true) +]; + +function generateCipherView(deleted: boolean) { + return BuildTestObject({ + id: GetUniqueString('id'), + notes: GetUniqueString('notes'), + type: CipherType.Login, + login: BuildTestObject({ + username: GetUniqueString('username'), + password: GetUniqueString('password'), + }, LoginView), + collectionIds: null, + deletedDate: deleted ? new Date() : null, + }, CipherView); +} + +function generateCipherDomain(deleted: boolean) { + return BuildTestObject({ + id: GetUniqueString('id'), + notes: new CipherString(GetUniqueString('notes')), + type: CipherType.Login, + login: BuildTestObject({ + username: new CipherString(GetUniqueString('username')), + password: new CipherString(GetUniqueString('password')), + }, Login), + collectionIds: null, + deletedDate: deleted ? new Date() : null, + }, Cipher); +} + +function expectEqualCiphers(ciphers: CipherView[] | Cipher[], jsonResult: string) { + const actual = JSON.stringify(JSON.parse(jsonResult).items); + const items: CipherExport[] = []; + ciphers.forEach((c: CipherView | Cipher) => { + const item = new CipherExport(); + item.build(c); + items.push(item); + }); + + expect(actual).toEqual(JSON.stringify(items)); +} + +describe('ExportService', () => { + let exportService: ExportService; + let apiService: SubstituteOf; + let cipherService: SubstituteOf; + let folderService: SubstituteOf; + + beforeEach(() => { + apiService = Substitute.for(); + cipherService = Substitute.for(); + folderService = Substitute.for(); + + folderService.getAllDecrypted().resolves([]); + folderService.getAll().resolves([]); + + exportService = new ExportService(folderService, cipherService, apiService); + }); + + it('exports unecrypted user ciphers', async () => { + cipherService.getAllDecrypted().resolves(UserCipherViews.slice(0, 1)); + + const actual = await exportService.getExport('json'); + + expectEqualCiphers(UserCipherViews.slice(0, 1), actual); + }); + + it('exports encrypted json user ciphers', async () => { + cipherService.getAll().resolves(UserCipherDomains.slice(0, 1)); + + const actual = await exportService.getExport('encrypted_json'); + + expectEqualCiphers(UserCipherDomains.slice(0, 1), actual); + }); + + it('does not unecrypted export trashed user items', async () => { + cipherService.getAllDecrypted().resolves(UserCipherViews); + + const actual = await exportService.getExport('json'); + + expectEqualCiphers(UserCipherViews.slice(0, 2), actual); + }); + + it('does not encrypted export trashed user items', async () => { + cipherService.getAll().resolves(UserCipherDomains); + + const actual = await exportService.getExport('encrypted_json'); + + expectEqualCiphers(UserCipherDomains.slice(0, 2), actual); + }); +}); diff --git a/spec/support/karma.conf.js b/spec/support/karma.conf.js index 4ec04a07df..1196bafb65 100644 --- a/spec/support/karma.conf.js +++ b/spec/support/karma.conf.js @@ -9,6 +9,7 @@ module.exports = (config) => { // list of files / patterns to load in the browser files: [ + 'spec/utils.ts', 'spec/common/**/*.ts', 'spec/web/**/*.ts', 'src/abstractions/**/*.ts', diff --git a/spec/utils.ts b/spec/utils.ts new file mode 100644 index 0000000000..94535d9b82 --- /dev/null +++ b/spec/utils.ts @@ -0,0 +1,16 @@ +function newGuid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + // tslint:disable:no-bitwise + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +export function GetUniqueString(prefix: string = '') { + return prefix + '_' + newGuid(); +} + +export function BuildTestObject(def: Partial> | T, constructor?: (new () => T)): T { + return Object.assign(constructor === null ? {} : new constructor(), def) as T; +} diff --git a/src/services/export.service.ts b/src/services/export.service.ts index e0c4188e87..77a020d864 100644 --- a/src/services/export.service.ts +++ b/src/services/export.service.ts @@ -64,7 +64,7 @@ export class ExportService implements ExportServiceAbstraction { })); promises.push(this.cipherService.getAllDecrypted().then((ciphers) => { - decCiphers = ciphers; + decCiphers = ciphers.filter(f => f.deletedDate == null); })); await Promise.all(promises); @@ -127,8 +127,19 @@ export class ExportService implements ExportServiceAbstraction { } private async getEncryptedExport(): Promise { - const folders = await this.folderService.getAll(); - const ciphers = await this.cipherService.getAll(); + let folders: Folder[] = []; + let ciphers: Cipher[] = []; + const promises = []; + + promises.push(this.folderService.getAll().then((f) => { + folders = f; + })); + + promises.push(this.cipherService.getAll().then((c) => { + ciphers = c.filter((f) => f.deletedDate == null); + })); + + await Promise.all(promises); const jsonDoc: any = { encrypted: true, @@ -179,7 +190,7 @@ export class ExportService implements ExportServiceAbstraction { promises.push(this.apiService.getCiphersOrganization(organizationId).then((ciphers) => { const cipherPromises: any = []; if (ciphers != null && ciphers.data != null && ciphers.data.length > 0) { - ciphers.data.forEach((c) => { + ciphers.data.filter((c) => c.deletedDate === null).forEach((c) => { const cipher = new Cipher(new CipherData(c)); cipherPromises.push(cipher.decrypt().then((decCipher) => { decCiphers.push(decCipher); @@ -256,8 +267,8 @@ export class ExportService implements ExportServiceAbstraction { promises.push(this.apiService.getCiphersOrganization(organizationId).then((c) => { const cipherPromises: any = []; if (c != null && c.data != null && c.data.length > 0) { - c.data.forEach((r) => { - const cipher = new Cipher(new CipherData(r)); + c.data.filter((item) => item.deletedDate === null).forEach((item) => { + const cipher = new Cipher(new CipherData(item)); ciphers.push(cipher); }); } From afa01f67f4f5f45506bb14ccb194bb22e11d803e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 30 Dec 2020 16:23:52 -0500 Subject: [PATCH 1135/1626] send removePasswordWithServer and model updates (#242) --- src/abstractions/send.service.ts | 1 + src/models/view/sendView.ts | 18 ++++++++++++++++++ src/services/send.service.ts | 8 +++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/abstractions/send.service.ts b/src/abstractions/send.service.ts index 03862de6bc..ae0c76311c 100644 --- a/src/abstractions/send.service.ts +++ b/src/abstractions/send.service.ts @@ -19,4 +19,5 @@ export abstract class SendService { clear: (userId: string) => Promise; delete: (id: string | string[]) => Promise; deleteWithServer: (id: string) => Promise; + removePasswordWithServer: (id: string) => Promise; } diff --git a/src/models/view/sendView.ts b/src/models/view/sendView.ts index 61c565dcf0..701a932606 100644 --- a/src/models/view/sendView.ts +++ b/src/models/view/sendView.ts @@ -46,4 +46,22 @@ export class SendView implements View { get urlB64Key(): string { return Utils.fromBufferToUrlB64(this.key); } + + get maxAccessCountReached(): boolean { + if (this.maxAccessCount == null) { + return false; + } + return this.accessCount >= this.maxAccessCount; + } + + get expired(): boolean { + if (this.expirationDate == null) { + return false; + } + return this.expirationDate <= new Date(); + } + + get pendingDelete(): boolean { + return this.deletionDate <= new Date(); + } } diff --git a/src/services/send.service.ts b/src/services/send.service.ts index 06b866efd5..212bbff02e 100644 --- a/src/services/send.service.ts +++ b/src/services/send.service.ts @@ -152,7 +152,6 @@ export class SendService implements SendServiceAbstraction { const userId = await this.userService.getUserId(); const data = new SendData(response, userId); await this.upsert(data); - } async upsert(send: SendData | SendData[]): Promise { @@ -215,6 +214,13 @@ export class SendService implements SendServiceAbstraction { await this.delete(id); } + async removePasswordWithServer(id: string): Promise { + const response = await this.apiService.putSendRemovePassword(id); + const userId = await this.userService.getUserId(); + const data = new SendData(response, userId); + await this.upsert(data); + } + private parseFile(send: Send, file: File, key: SymmetricCryptoKey): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); From cea09a22e533ef3598bb497ba0503c2fcd5b2dc1 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Fri, 8 Jan 2021 08:53:41 -0600 Subject: [PATCH 1136/1626] Update revision date upon cipher restore (#243) * Update revision date upon cipher restore * Receive and use returned datetimes from restore --- src/abstractions/api.service.ts | 6 +++--- src/abstractions/cipher.service.ts | 2 +- src/services/api.service.ts | 15 +++++++++------ src/services/cipher.service.ts | 28 +++++++++++++++++----------- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 955041f281..ffb676102f 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -206,9 +206,9 @@ export abstract class ApiService { putDeleteCipherAdmin: (id: string) => Promise; putDeleteManyCiphers: (request: CipherBulkDeleteRequest) => Promise; putDeleteManyCiphersAdmin: (request: CipherBulkDeleteRequest) => Promise; - putRestoreCipher: (id: string) => Promise; - putRestoreCipherAdmin: (id: string) => Promise; - putRestoreManyCiphers: (request: CipherBulkRestoreRequest) => Promise; + putRestoreCipher: (id: string) => Promise; + putRestoreCipherAdmin: (id: string) => Promise; + putRestoreManyCiphers: (request: CipherBulkRestoreRequest) => Promise>; postCipherAttachment: (id: string, data: FormData) => Promise; postCipherAttachmentAdmin: (id: string, data: FormData) => Promise; diff --git a/src/abstractions/cipher.service.ts b/src/abstractions/cipher.service.ts index cab7a972b3..0fbd115ded 100644 --- a/src/abstractions/cipher.service.ts +++ b/src/abstractions/cipher.service.ts @@ -53,7 +53,7 @@ export abstract class CipherService { softDelete: (id: string | string[]) => Promise; softDeleteWithServer: (id: string) => Promise; softDeleteManyWithServer: (ids: string[]) => Promise; - restore: (id: string | string[]) => Promise; + restore: (cipher: { id: string, revisionDate: string; } | { id: string, revisionDate: string; }[]) => Promise; restoreWithServer: (id: string) => Promise; restoreManyWithServer: (ids: string[]) => Promise; } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index e396839108..ab0c1c54eb 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -556,16 +556,19 @@ export class ApiService implements ApiServiceAbstraction { return this.send('PUT', '/ciphers/delete-admin', request, true, false); } - putRestoreCipher(id: string): Promise { - return this.send('PUT', '/ciphers/' + id + '/restore', null, true, false); + async putRestoreCipher(id: string): Promise { + const r = await this.send('PUT', '/ciphers/' + id + '/restore', null, true, true); + return new CipherResponse(r); } - putRestoreCipherAdmin(id: string): Promise { - return this.send('PUT', '/ciphers/' + id + '/restore-admin', null, true, false); + async putRestoreCipherAdmin(id: string): Promise { + const r = await this.send('PUT', '/ciphers/' + id + '/restore-admin', null, true, true); + return new CipherResponse(r); } - putRestoreManyCiphers(request: CipherBulkDeleteRequest): Promise { - return this.send('PUT', '/ciphers/restore', request, true, false); + async putRestoreManyCiphers(request: CipherBulkDeleteRequest): Promise> { + const r = await this.send('PUT', '/ciphers/restore', request, true, true); + return new ListResponse(r, CipherResponse); } // Attachments APIs diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index c45965367d..539321f5db 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -876,7 +876,7 @@ export class CipherService implements CipherServiceAbstraction { await this.softDelete(ids); } - async restore(id: string | string[]): Promise { + async restore(cipher: { id: string, revisionDate: string; } | { id: string, revisionDate: string; }[]) { const userId = await this.userService.getUserId(); const ciphers = await this.storageService.get<{ [id: string]: CipherData; }>( Keys.ciphersPrefix + userId); @@ -884,17 +884,19 @@ export class CipherService implements CipherServiceAbstraction { return; } - const clearDeletedDate = (cipherId: string) => { - if (ciphers[cipherId] == null) { + const clearDeletedDate = (c: { id: string, revisionDate: string; }) => { + if (ciphers[c.id] == null) { return; } - ciphers[cipherId].deletedDate = null; + ciphers[c.id].deletedDate = null; + ciphers[c.id].revisionDate = c.revisionDate; }; - if (typeof id === 'string') { - clearDeletedDate(id); + + if (cipher.constructor.name === 'Array') { + (cipher as { id: string, revisionDate: string; }[]).forEach(clearDeletedDate); } else { - (id as string[]).forEach(clearDeletedDate); + clearDeletedDate(cipher as { id: string, revisionDate: string; }); } await this.storageService.save(Keys.ciphersPrefix + userId, ciphers); @@ -902,13 +904,17 @@ export class CipherService implements CipherServiceAbstraction { } async restoreWithServer(id: string): Promise { - await this.apiService.putRestoreCipher(id); - await this.restore(id); + const response = await this.apiService.putRestoreCipher(id); + await this.restore({ id: id, revisionDate: response.revisionDate }); } async restoreManyWithServer(ids: string[]): Promise { - await this.apiService.putRestoreManyCiphers(new CipherBulkRestoreRequest(ids)); - await this.restore(ids); + const response = await this.apiService.putRestoreManyCiphers(new CipherBulkRestoreRequest(ids)); + const restores: { id: string, revisionDate: string; }[] = []; + for (const cipher of response.data) { + restores.push({ id: cipher.id, revisionDate: cipher.revisionDate }); + } + await this.restore(restores); } // Helpers From 3ac42f2f44535036c7dc233c9070fdf271fa7b5b Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Mon, 11 Jan 2021 10:51:23 -0600 Subject: [PATCH 1137/1626] Remove unused toData methods (#245) --- src/models/domain/send.ts | 34 ---------------------------------- src/models/domain/sendFile.ts | 12 ------------ src/models/domain/sendText.ts | 9 --------- 3 files changed, 55 deletions(-) diff --git a/src/models/domain/send.ts b/src/models/domain/send.ts index c80ceac56d..fabfc1471f 100644 --- a/src/models/domain/send.ts +++ b/src/models/domain/send.ts @@ -103,38 +103,4 @@ export class Send extends Domain { return model; } - - toSendData(userId: string): SendData { - const s = new SendData(); - s.id = this.id; - s.accessId = this.accessId; - s.userId = userId; - s.maxAccessCount = this.maxAccessCount; - s.accessCount = this.accessCount; - s.disabled = this.disabled; - s.password = this.password; - s.revisionDate = this.revisionDate != null ? this.revisionDate.toISOString() : null; - s.deletionDate = this.deletionDate != null ? this.deletionDate.toISOString() : null; - s.expirationDate = this.expirationDate != null ? this.expirationDate.toISOString() : null; - s.type = this.type; - - this.buildDataModel(this, s, { - name: null, - notes: null, - key: null, - }); - - switch (s.type) { - case SendType.File: - s.text = this.text.toSendTextData(); - break; - case SendType.Text: - s.file = this.file.toSendFileData(); - break; - default: - break; - } - - return s; - } } diff --git a/src/models/domain/sendFile.ts b/src/models/domain/sendFile.ts index ff462ce823..876529ecf7 100644 --- a/src/models/domain/sendFile.ts +++ b/src/models/domain/sendFile.ts @@ -34,16 +34,4 @@ export class SendFile extends Domain { }, null, key); return view; } - - toSendFileData(): SendFileData { - const f = new SendFileData(); - f.size = this.size; - this.buildDataModel(this, f, { - id: null, - url: null, - sizeName: null, - fileName: null, - }, ['id', 'url', 'sizeName']); - return f; - } } diff --git a/src/models/domain/sendText.ts b/src/models/domain/sendText.ts index 592a085290..82b3665db2 100644 --- a/src/models/domain/sendText.ts +++ b/src/models/domain/sendText.ts @@ -27,13 +27,4 @@ export class SendText extends Domain { text: null, }, null, key); } - - toSendTextData(): SendTextData { - const t = new SendTextData(); - this.buildDataModel(this, t, { - text: null, - hidden: null, - }, ['hidden']); - return t; - } } From 8d161d9245f69fe81b0a49269874e48f88932042 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Mon, 11 Jan 2021 19:12:40 +0100 Subject: [PATCH 1138/1626] Improve system.service biometrics condition (#244) --- src/services/constants.service.ts | 2 ++ src/services/system.service.ts | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index d3cca66aec..ee1b3636b2 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -28,6 +28,7 @@ export class ConstantsService { static readonly biometricUnlockKey: string = 'biometric'; static readonly biometricText: string = 'biometricText'; static readonly biometricAwaitingAcceptance: string = 'biometricAwaitingAcceptance'; + static readonly biometricFingerprintValidated: string = 'biometricFingerprintValidated'; readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; readonly disableGaKey: string = ConstantsService.disableGaKey; @@ -57,4 +58,5 @@ export class ConstantsService { readonly biometricUnlockKey: string = ConstantsService.biometricUnlockKey; readonly biometricText: string = ConstantsService.biometricText; readonly biometricAwaitingAcceptance: string = ConstantsService.biometricAwaitingAcceptance; + readonly biometricFingerprintValidated: string = ConstantsService.biometricFingerprintValidated; } diff --git a/src/services/system.service.ts b/src/services/system.service.ts index 4edf0e3a2e..d5d0d6ea01 100644 --- a/src/services/system.service.ts +++ b/src/services/system.service.ts @@ -19,7 +19,7 @@ export class SystemService implements SystemServiceAbstraction { } startProcessReload(): void { - if (this.vaultTimeoutService.pinProtectedKey != null || this.vaultTimeoutService.biometricLocked || this.reloadInterval != null) { + if (this.vaultTimeoutService.pinProtectedKey != null || this.reloadInterval != null) { return; } this.cancelProcessReload(); @@ -31,7 +31,9 @@ export class SystemService implements SystemServiceAbstraction { // Don't refresh if they are still active in the window doRefresh = diffSeconds >= 5000; } - if (doRefresh) { + const biometricLockedFingerprintValidated = + await this.storageService.get(ConstantsService.biometricFingerprintValidated) && this.vaultTimeoutService.biometricLocked; + if (doRefresh && !biometricLockedFingerprintValidated) { clearInterval(this.reloadInterval); this.reloadInterval = null; this.messagingService.send('reloadProcess'); From 6ac6df75d7a9bd5ea58f5d8310f1b3e34abd2bde Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Mon, 11 Jan 2021 17:01:39 -0500 Subject: [PATCH 1139/1626] Implemented Custom role and permissions (#237) * Implemented Custom role and permissions * converted Permissions interface into a class * formatting fix --- src/angular/components/add-edit.component.ts | 2 +- src/enums/organizationUserType.ts | 1 + src/enums/permissions.ts | 12 +++++ src/models/api/permissionsApi.ts | 33 ++++++++++++++ src/models/data/organizationData.ts | 3 ++ src/models/domain/organization.ts | 44 +++++++++++++++++++ .../request/organizationUserInviteRequest.ts | 6 ++- .../request/organizationUserUpdateRequest.ts | 6 ++- .../response/organizationUserResponse.ts | 10 +++-- .../response/profileOrganizationResponse.ts | 3 ++ 10 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 src/enums/permissions.ts create mode 100644 src/models/api/permissionsApi.ts diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index 567dbadb45..ede0b2f107 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -162,7 +162,7 @@ export class AddEditComponent implements OnInit { orgs.sort(Utils.getSortFunction(this.i18nService, 'name')).forEach((o) => { if (o.enabled && o.status === OrganizationUserStatusType.Confirmed) { this.ownershipOptions.push({ name: o.name, value: o.id }); - if (policies != null && o.usePolicies && !o.isAdmin && this.allowPersonal) { + if (policies != null && o.usePolicies && !o.canManagePolicies && this.allowPersonal) { for (const policy of policies) { if (policy.organizationId === o.id && policy.enabled) { this.allowPersonal = false; diff --git a/src/enums/organizationUserType.ts b/src/enums/organizationUserType.ts index de794fe690..632d22b01e 100644 --- a/src/enums/organizationUserType.ts +++ b/src/enums/organizationUserType.ts @@ -3,4 +3,5 @@ export enum OrganizationUserType { Admin = 1, User = 2, Manager = 3, + Custom = 4, } diff --git a/src/enums/permissions.ts b/src/enums/permissions.ts new file mode 100644 index 0000000000..bcf76c6080 --- /dev/null +++ b/src/enums/permissions.ts @@ -0,0 +1,12 @@ +export enum Permissions { + AccessBusinessPortal, + AccessEventLogs, + AccessImportExport, + AccessReports, + ManageAllCollections, + ManageAssignedCollections, + ManageGroups, + ManageOrganization, + ManagePolicies, + ManageUsers, +} diff --git a/src/models/api/permissionsApi.ts b/src/models/api/permissionsApi.ts new file mode 100644 index 0000000000..93e141982a --- /dev/null +++ b/src/models/api/permissionsApi.ts @@ -0,0 +1,33 @@ +import { BaseResponse } from '../response/baseResponse'; + +export class PermissionsApi extends BaseResponse { + accessBusinessPortal: boolean; + accessEventLogs: boolean; + accessImportExport: boolean; + accessReports: boolean; + manageAllCollections: boolean; + manageAssignedCollections: boolean; + manageCiphers: boolean; + manageGroups: boolean; + manageSso: boolean; + managePolicies: boolean; + manageUsers: boolean; + + constructor(data: any = null) { + super(data); + if (data == null) { + return this; + } + this.accessBusinessPortal = this.getResponseProperty('AccessBusinessPortal'); + this.accessEventLogs = this.getResponseProperty('AccessEventLogs'); + this.accessImportExport = this.getResponseProperty('AccessImportExport'); + this.accessReports = this.getResponseProperty('AccessReports'); + this.manageAllCollections = this.getResponseProperty('ManageAllCollections'); + this.manageAssignedCollections = this.getResponseProperty('ManageAssignedCollections'); + this.manageCiphers = this.getResponseProperty('ManageCiphers'); + this.manageGroups = this.getResponseProperty('ManageGroups'); + this.manageSso = this.getResponseProperty('ManageSso'); + this.managePolicies = this.getResponseProperty('ManagePolicies'); + this.manageUsers = this.getResponseProperty('ManageUsers'); + } +} diff --git a/src/models/data/organizationData.ts b/src/models/data/organizationData.ts index d6d10dbbd4..9470ab37a4 100644 --- a/src/models/data/organizationData.ts +++ b/src/models/data/organizationData.ts @@ -2,6 +2,7 @@ import { ProfileOrganizationResponse } from '../response/profileOrganizationResp import { OrganizationUserStatusType } from '../../enums/organizationUserStatusType'; import { OrganizationUserType } from '../../enums/organizationUserType'; +import { PermissionsApi } from '../api/permissionsApi'; export class OrganizationData { id: string; @@ -25,6 +26,7 @@ export class OrganizationData { maxStorageGb?: number; ssoBound: boolean; identifier: string; + permissions: PermissionsApi; constructor(response: ProfileOrganizationResponse) { this.id = response.id; @@ -48,5 +50,6 @@ export class OrganizationData { this.maxStorageGb = response.maxStorageGb; this.ssoBound = response.ssoBound; this.identifier = response.identifier; + this.permissions = response.permissions; } } diff --git a/src/models/domain/organization.ts b/src/models/domain/organization.ts index f616f2e1c3..c3c24dfe88 100644 --- a/src/models/domain/organization.ts +++ b/src/models/domain/organization.ts @@ -2,6 +2,8 @@ import { OrganizationData } from '../data/organizationData'; import { OrganizationUserStatusType } from '../../enums/organizationUserStatusType'; import { OrganizationUserType } from '../../enums/organizationUserType'; +import { PermissionsApi } from '../api/permissionsApi'; + export class Organization { id: string; @@ -25,6 +27,7 @@ export class Organization { maxStorageGb?: number; ssoBound: boolean; identifier: string; + permissions: PermissionsApi; constructor(obj?: OrganizationData) { if (obj == null) { @@ -52,6 +55,7 @@ export class Organization { this.maxStorageGb = obj.maxStorageGb; this.ssoBound = obj.ssoBound; this.identifier = obj.identifier; + this.permissions = obj.permissions; } get canAccess() { @@ -73,4 +77,44 @@ export class Organization { get isOwner() { return this.type === OrganizationUserType.Owner; } + + get canAccessBusinessPortal() { + return this.isAdmin || this.permissions.accessBusinessPortal; + } + + get canAccessEventLogs() { + return this.isAdmin || this.permissions.accessEventLogs; + } + + get canAccessImportExport() { + return this.isAdmin || this.permissions.accessImportExport; + } + + get canAccessReports() { + return this.isAdmin || this.permissions.accessReports; + } + + get canManageAllCollections() { + return this.isAdmin || this.permissions.manageAllCollections; + } + + get canManageAssignedCollections() { + return this.isManager || this.permissions.manageAssignedCollections; + } + + get canManageGroups() { + return this.isAdmin || this.permissions.manageGroups; + } + + get canManageSso() { + return this.isAdmin || this.permissions.manageSso; + } + + get canManagePolicies() { + return this.isAdmin || this.permissions.managePolicies; + } + + get canManageUsers() { + return this.isAdmin || this.permissions.manageUsers; + } } diff --git a/src/models/request/organizationUserInviteRequest.ts b/src/models/request/organizationUserInviteRequest.ts index 79371e99ed..4195eec85a 100644 --- a/src/models/request/organizationUserInviteRequest.ts +++ b/src/models/request/organizationUserInviteRequest.ts @@ -1,10 +1,12 @@ -import { OrganizationUserType } from '../../enums/organizationUserType'; - import { SelectionReadOnlyRequest } from './selectionReadOnlyRequest'; +import { OrganizationUserType } from '../../enums/organizationUserType'; +import { PermissionsApi } from '../api/permissionsApi'; + export class OrganizationUserInviteRequest { emails: string[] = []; type: OrganizationUserType; accessAll: boolean; collections: SelectionReadOnlyRequest[] = []; + permissions: PermissionsApi; } diff --git a/src/models/request/organizationUserUpdateRequest.ts b/src/models/request/organizationUserUpdateRequest.ts index 7b8c9cd237..80803cd650 100644 --- a/src/models/request/organizationUserUpdateRequest.ts +++ b/src/models/request/organizationUserUpdateRequest.ts @@ -1,9 +1,11 @@ -import { OrganizationUserType } from '../../enums/organizationUserType'; - import { SelectionReadOnlyRequest } from './selectionReadOnlyRequest'; +import { OrganizationUserType } from '../../enums/organizationUserType'; +import { PermissionsApi } from '../api/permissionsApi'; + export class OrganizationUserUpdateRequest { type: OrganizationUserType; accessAll: boolean; collections: SelectionReadOnlyRequest[] = []; + permissions: PermissionsApi; } diff --git a/src/models/response/organizationUserResponse.ts b/src/models/response/organizationUserResponse.ts index 444e27906b..08d3dbed98 100644 --- a/src/models/response/organizationUserResponse.ts +++ b/src/models/response/organizationUserResponse.ts @@ -1,15 +1,18 @@ -import { OrganizationUserStatusType } from '../../enums/organizationUserStatusType'; -import { OrganizationUserType } from '../../enums/organizationUserType'; - import { BaseResponse } from './baseResponse'; import { SelectionReadOnlyResponse } from './selectionReadOnlyResponse'; +import { PermissionsApi } from '../api/permissionsApi'; + +import { OrganizationUserStatusType } from '../../enums/organizationUserStatusType'; +import { OrganizationUserType } from '../../enums/organizationUserType'; + export class OrganizationUserResponse extends BaseResponse { id: string; userId: string; type: OrganizationUserType; status: OrganizationUserStatusType; accessAll: boolean; + permissions: PermissionsApi; constructor(response: any) { super(response); @@ -17,6 +20,7 @@ export class OrganizationUserResponse extends BaseResponse { this.userId = this.getResponseProperty('UserId'); this.type = this.getResponseProperty('Type'); this.status = this.getResponseProperty('Status'); + this.permissions = new PermissionsApi(this.getResponseProperty('Permissions')); this.accessAll = this.getResponseProperty('AccessAll'); } } diff --git a/src/models/response/profileOrganizationResponse.ts b/src/models/response/profileOrganizationResponse.ts index 91a4c350d6..9b8a15dae8 100644 --- a/src/models/response/profileOrganizationResponse.ts +++ b/src/models/response/profileOrganizationResponse.ts @@ -2,6 +2,7 @@ import { BaseResponse } from './baseResponse'; import { OrganizationUserStatusType } from '../../enums/organizationUserStatusType'; import { OrganizationUserType } from '../../enums/organizationUserType'; +import { PermissionsApi } from '../api/permissionsApi'; export class ProfileOrganizationResponse extends BaseResponse { id: string; @@ -26,6 +27,7 @@ export class ProfileOrganizationResponse extends BaseResponse { enabled: boolean; ssoBound: boolean; identifier: string; + permissions: PermissionsApi; constructor(response: any) { super(response); @@ -51,5 +53,6 @@ export class ProfileOrganizationResponse extends BaseResponse { this.enabled = this.getResponseProperty('Enabled'); this.ssoBound = this.getResponseProperty('SsoBound'); this.identifier = this.getResponseProperty('Identifier'); + this.permissions = new PermissionsApi(this.getResponseProperty('permissions')); } } From 68bd93e45bd9acb9df18cf7806d51710f1986237 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 22 Jan 2021 16:52:08 -0500 Subject: [PATCH 1140/1626] added Send sync notification support (#250) --- src/abstractions/sync.service.ts | 3 ++ src/enums/notificationType.ts | 4 +++ src/models/response/notificationResponse.ts | 17 +++++++++++ src/services/notifications.service.ts | 8 ++++++ src/services/sync.service.ts | 32 +++++++++++++++++++++ 5 files changed, 64 insertions(+) diff --git a/src/abstractions/sync.service.ts b/src/abstractions/sync.service.ts index ad6eb498e8..5d35cf20bc 100644 --- a/src/abstractions/sync.service.ts +++ b/src/abstractions/sync.service.ts @@ -1,6 +1,7 @@ import { SyncCipherNotification, SyncFolderNotification, + SyncSendNotification, } from '../models/response/notificationResponse'; export abstract class SyncService { @@ -13,4 +14,6 @@ export abstract class SyncService { syncDeleteFolder: (notification: SyncFolderNotification) => Promise; syncUpsertCipher: (notification: SyncCipherNotification, isEdit: boolean) => Promise; syncDeleteCipher: (notification: SyncFolderNotification) => Promise; + syncUpsertSend: (notification: SyncSendNotification, isEdit: boolean) => Promise; + syncDeleteSend: (notification: SyncSendNotification) => Promise; } diff --git a/src/enums/notificationType.ts b/src/enums/notificationType.ts index 4b655404e7..5adaac307d 100644 --- a/src/enums/notificationType.ts +++ b/src/enums/notificationType.ts @@ -13,4 +13,8 @@ export enum NotificationType { SyncSettings = 10, LogOut = 11, + + SyncSendCreate = 12, + SyncSendUpdate = 13, + SyncSendDelete = 14, } diff --git a/src/models/response/notificationResponse.ts b/src/models/response/notificationResponse.ts index 9187be71fd..b60f38a6d8 100644 --- a/src/models/response/notificationResponse.ts +++ b/src/models/response/notificationResponse.ts @@ -32,6 +32,10 @@ export class NotificationResponse extends BaseResponse { case NotificationType.LogOut: this.payload = new UserNotification(payload); break; + case NotificationType.SyncSendCreate: + case NotificationType.SyncSendUpdate: + case NotificationType.SyncSendDelete: + this.payload = new SyncSendNotification(payload); default: break; } @@ -78,3 +82,16 @@ export class UserNotification extends BaseResponse { this.date = new Date(this.getResponseProperty('Date')); } } + +export class SyncSendNotification extends BaseResponse { + id: string; + userId: string; + revisionDate: Date; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty('Id'); + this.userId = this.getResponseProperty('UserId'); + this.revisionDate = new Date(this.getResponseProperty('RevisionDate')); + } +} diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index a4df8299f5..b8ae1a07d8 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -16,6 +16,7 @@ import { NotificationResponse, SyncCipherNotification, SyncFolderNotification, + SyncSendNotification, } from '../models/response/notificationResponse'; export class NotificationsService implements NotificationsServiceAbstraction { @@ -159,6 +160,13 @@ export class NotificationsService implements NotificationsServiceAbstraction { this.logoutCallback(); } break; + case NotificationType.SyncSendCreate: + case NotificationType.SyncSendUpdate: + await this.syncService.syncUpsertSend(notification.payload as SyncSendNotification, + notification.type === NotificationType.SyncSendUpdate); + break; + case NotificationType.SyncSendDelete: + await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification); default: break; } diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index 791d5f0240..a609748b58 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -25,6 +25,7 @@ import { FolderResponse } from '../models/response/folderResponse'; import { SyncCipherNotification, SyncFolderNotification, + SyncSendNotification, } from '../models/response/notificationResponse'; import { PolicyResponse } from '../models/response/policyResponse'; import { ProfileResponse } from '../models/response/profileResponse'; @@ -212,6 +213,37 @@ export class SyncService implements SyncServiceAbstraction { return this.syncCompleted(false); } + async syncUpsertSend(notification: SyncSendNotification, isEdit: boolean): Promise { + this.syncStarted(); + if (await this.userService.isAuthenticated()) { + try { + const localSend = await this.sendService.get(notification.id); + if ((!isEdit && localSend == null) || + (isEdit && localSend != null && localSend.revisionDate < notification.revisionDate)) { + const remoteSend = await this.apiService.getSend(notification.id); + if (remoteSend != null) { + const userId = await this.userService.getUserId(); + await this.sendService.upsert(new SendData(remoteSend, userId)); + this.messagingService.send('syncedUpsertedSend', { sendId: notification.id }); + return this.syncCompleted(true); + } + } + } catch { } + } + return this.syncCompleted(false); + } + + async syncDeleteSend(notification: SyncSendNotification): Promise { + this.syncStarted(); + if (await this.userService.isAuthenticated()) { + await this.sendService.delete(notification.id); + this.messagingService.send('syncedDeletedSend', { sendId: notification.id }); + this.syncCompleted(true); + return true; + } + return this.syncCompleted(false); + } + // Helpers private syncStarted() { From e1f1d7e7027fdb1d179aeec29f5a22e706d47a5c Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Fri, 22 Jan 2021 19:54:22 -0600 Subject: [PATCH 1141/1626] Enable search for sends (#249) --- src/abstractions/search.service.ts | 2 ++ src/services/search.service.ts | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/abstractions/search.service.ts b/src/abstractions/search.service.ts index d6d0364faf..127a970353 100644 --- a/src/abstractions/search.service.ts +++ b/src/abstractions/search.service.ts @@ -1,4 +1,5 @@ import { CipherView } from '../models/view/cipherView'; +import { SendView } from '../models/view/sendView'; export abstract class SearchService { clearIndex: () => void; @@ -8,4 +9,5 @@ export abstract class SearchService { filter?: ((cipher: CipherView) => boolean) | (((cipher: CipherView) => boolean)[]), ciphers?: CipherView[]) => Promise; searchCiphersBasic: (ciphers: CipherView[], query: string, deleted?: boolean) => CipherView[]; + searchSends: (sends: SendView[], query: string) => SendView[]; } diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 4a7207f93b..0552ffc8c4 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -9,6 +9,7 @@ import { SearchService as SearchServiceAbstraction } from '../abstractions/searc import { CipherType } from '../enums/cipherType'; import { FieldType } from '../enums/fieldType'; import { UriMatchType } from '../enums/uriMatchType'; +import { SendView } from '../models/view/sendView'; export class SearchService implements SearchServiceAbstraction { private indexing = false; @@ -161,6 +162,28 @@ export class SearchService implements SearchServiceAbstraction { }); } + searchSends(sends: SendView[], query: string) { + query = query.trim().toLocaleLowerCase(); + + return sends.filter(s => { + if (s.name != null && s.name.toLowerCase().indexOf(query) > -1) { + return true; + } + if (query.length >= 8 && (s.id.startsWith(query) || (s.file?.id != null && s.file.id.startsWith(query)))) { + return true; + } + if (s.notes != null && s.notes.toLowerCase().indexOf(query) > -1) { + return true; + } + if (s.text?.text != null && s.text.text.toLowerCase().indexOf(query) > -1) { + return true; + } + if (s.file?.fileName != null && s.file.fileName.toLowerCase().indexOf(query) > -1) { + return true; + } + }); + } + getIndexForSearch(): lunr.Index { return this.index; } From 9ddec9baf8b6e7de58c00744eb371dc68e1b6383 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Mon, 25 Jan 2021 15:03:28 -0500 Subject: [PATCH 1142/1626] Lunr search bug (#251) * changed hrtime library * changed import style --- package-lock.json | 8 ++++---- package.json | 2 +- src/services/consoleLog.service.ts | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 09edad89f2..122115c8af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1168,10 +1168,10 @@ "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", "dev": true }, - "browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" + "browser-hrtime": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/browser-hrtime/-/browser-hrtime-1.1.8.tgz", + "integrity": "sha512-kzXheikaJsBtzUBlyVtPIY5r0soQePzjwVwT4IlDpU2RvfB5Py52gpU98M77rgqMCheoSSZvrcrdj3t6cZ3suA==" }, "browser-resolve": { "version": "1.11.3", diff --git a/package.json b/package.json index ca426d8e23..8c04d82a50 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "@microsoft/signalr-protocol-msgpack": "3.1.0", "@nodert-win10-rs4/windows.security.credentials.ui": "^0.4.4", "big-integer": "1.6.36", - "browser-process-hrtime": "1.0.0", + "browser-hrtime": "^1.1.8", "chalk": "2.4.1", "commander": "2.18.0", "core-js": "2.6.2", diff --git a/src/services/consoleLog.service.ts b/src/services/consoleLog.service.ts index f6d119b2ce..64d812660b 100644 --- a/src/services/consoleLog.service.ts +++ b/src/services/consoleLog.service.ts @@ -2,8 +2,7 @@ import { LogLevelType } from '../enums/logLevelType'; import { LogService as LogServiceAbstraction } from '../abstractions/log.service'; -// @ts-ignore: import * as ns from "mod" error, need to do it this way -import hrtime = require('browser-process-hrtime'); +import * as hrtime from 'browser-hrtime'; export class ConsoleLogService implements LogServiceAbstraction { protected timersMap: Map = new Map(); From d1c46e6bdc9332bcf47acbd235c3a6278e086d8a Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 26 Jan 2021 22:49:47 +0100 Subject: [PATCH 1143/1626] Update electron to 11.1.1 (#247) --- package-lock.json | 925 +++++++++++++++++++++++++++--------- package.json | 6 +- src/electron/utils.ts | 7 +- src/electron/window.main.ts | 3 +- 4 files changed, 711 insertions(+), 230 deletions(-) diff --git a/package-lock.json b/package-lock.json index 122115c8af..814424b931 100644 --- a/package-lock.json +++ b/package-lock.json @@ -192,6 +192,122 @@ "to-fast-properties": "^2.0.0" } }, + "@electron/get": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.12.2.tgz", + "integrity": "sha512-vAuHUbfvBQpYTJ5wB7uVIDq5c/Ry0fiTBMs7lnEYAo/qXXppIVcWdfBr57u6eRnKdVso7KSiH6p/LbQAG6Izrg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "global-agent": "^2.0.2", + "global-tunnel-ng": "^2.7.1", + "got": "^9.6.0", + "progress": "^2.0.3", + "sanitize-filename": "^1.6.2", + "sumchecker": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "env-paths": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", + "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==", + "dev": true + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dev": true, + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "dev": true, + "requires": { + "prepend-http": "^2.0.0" + } + } + } + }, + "@fluffy-spoon/substitute": { + "version": "1.183.0", + "resolved": "https://registry.npmjs.org/@fluffy-spoon/substitute/-/substitute-1.183.0.tgz", + "integrity": "sha512-wVDnNQ5U6MZUUgKLrYTIn/ss/e2CI5r4xRag2Jz4hpA2qEqSd2y+kckr5BZZxAVgIYygZ3CUILZzSyvIFdVzyg==", + "dev": true + }, "@microsoft/signalr": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-3.1.0.tgz", @@ -236,6 +352,21 @@ } } }, + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "dev": true + }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dev": true, + "requires": { + "defer-to-connect": "^1.0.1" + } + }, "@types/commander": { "version": "2.12.2", "resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.2.tgz", @@ -814,6 +945,11 @@ "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=", "dev": true }, + "atomically": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz", + "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==" + }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -1045,6 +1181,13 @@ } } }, + "boolean": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.0.2.tgz", + "integrity": "sha512-RwywHlpCRc3/Wh81MiCKun4ydaIFyW5Ea6JbL6sRCVx5q5irDw7pMXBUFYF/jArQ6YrG36q0kpovc9P/Kd3I4g==", + "dev": true, + "optional": true + }, "boxen": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", @@ -1285,6 +1428,12 @@ "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true + }, "buffer-fill": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", @@ -1375,6 +1524,48 @@ } } }, + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, "callsite": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", @@ -2118,6 +2309,15 @@ "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", "dev": true }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -2300,15 +2500,111 @@ } }, "conf": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/conf/-/conf-1.4.0.tgz", - "integrity": "sha512-bzlVWS2THbMetHqXKB8ypsXN4DQ/1qopGwNJi1eYbpwesJcd86FBjFciCQX/YwAhp9bM7NVnPFqZ5LpV7gP0Dg==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/conf/-/conf-7.1.2.tgz", + "integrity": "sha512-r8/HEoWPFn4CztjhMJaWNAe5n+gPUCSaJ0oufbqDLFKsA1V8JjAG7G+p0pgoDFAws9Bpk2VtVLLXqOBA7WxLeg==", "requires": { - "dot-prop": "^4.1.0", - "env-paths": "^1.0.0", - "make-dir": "^1.0.0", - "pkg-up": "^2.0.0", - "write-file-atomic": "^2.3.0" + "ajv": "^6.12.2", + "atomically": "^1.3.1", + "debounce-fn": "^4.0.0", + "dot-prop": "^5.2.0", + "env-paths": "^2.2.0", + "json-schema-typed": "^7.0.3", + "make-dir": "^3.1.0", + "onetime": "^5.1.0", + "pkg-up": "^3.1.0", + "semver": "^7.3.2" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "requires": { + "is-obj": "^2.0.0" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "config-chain": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", + "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", + "dev": true, + "optional": true, + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" } }, "configstore": { @@ -2567,6 +2863,21 @@ "meow": "^3.3.0" } }, + "debounce-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", + "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==", + "requires": { + "mimic-fn": "^3.0.0" + }, + "dependencies": { + "mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==" + } + } + }, "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", @@ -2632,6 +2943,22 @@ "clone": "^1.0.2" } }, + "defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "optional": true, + "requires": { + "object-keys": "^1.0.12" + } + }, "define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", @@ -2716,6 +3043,13 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, + "detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", + "dev": true, + "optional": true + }, "di": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", @@ -2769,6 +3103,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "dev": true, "requires": { "is-obj": "^1.0.0" } @@ -2811,71 +3146,43 @@ "dev": true }, "electron": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/electron/-/electron-6.1.7.tgz", - "integrity": "sha512-QhBA/fcYJit2XJGkD2xEfxlWTtTaWYu7qkKVohtVWXpELFqkpel2DCDxet5BTo0qs8ukuZHxlQPnIonODnl2bw==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-11.1.1.tgz", + "integrity": "sha512-tlbex3xosJgfileN6BAQRotevPRXB/wQIq48QeQ08tUJJrXwE72c8smsM/hbHx5eDgnbfJ2G3a60PmRjHU2NhA==", "dev": true, "requires": { - "@types/node": "^10.12.18", - "electron-download": "^4.1.0", + "@electron/get": "^1.0.1", + "@types/node": "^12.0.12", "extract-zip": "^1.0.3" }, "dependencies": { "@types/node": { - "version": "10.17.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.13.tgz", - "integrity": "sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg==", - "dev": true - } - } - }, - "electron-download": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-4.1.1.tgz", - "integrity": "sha512-FjEWG9Jb/ppK/2zToP+U5dds114fM1ZOJqMAR4aXXL5CvyPE9fiqBK/9YcwC9poIFQTEJk/EM/zyRwziziRZrg==", - "dev": true, - "requires": { - "debug": "^3.0.0", - "env-paths": "^1.0.0", - "fs-extra": "^4.0.1", - "minimist": "^1.2.0", - "nugget": "^2.0.1", - "path-exists": "^3.0.0", - "rc": "^1.2.1", - "semver": "^5.4.1", - "sumchecker": "^2.0.2" - }, - "dependencies": { - "fs-extra": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", - "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "version": "12.19.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.13.tgz", + "integrity": "sha512-qdixo2f0U7z6m0UJUugTJqVF94GNDkdgQhfBtMs8t5898JE7G/D2kJYw4rc1nzjIPLVAsDkY2MdABnLAP5lM1w==", "dev": true } } }, "electron-log": { - "version": "2.2.17", - "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.17.tgz", - "integrity": "sha512-v+Af5W5z99ehhaLOfE9eTSXUwjzh2wFlQjz51dvkZ6ZIrET6OB/zAZPvsuwT6tm3t5x+M1r+Ed3U3xtPZYAyuQ==" + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.3.0.tgz", + "integrity": "sha512-iuJjH/ZEJkDyCbuAMvvFxAjCMDLMXIQ5NqvppETGrbtf4b/007r5P36BSvexdy0UzwDNzDtIuEXLR34vRXWZrg==" }, "electron-store": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-1.3.0.tgz", - "integrity": "sha512-r1Pdl5MwpiCxgbsl0qnwv/GABO5+J/JTO16+KyqL+bOITIk9o3cq3Sw69uO9NgPkpfcKeEwxtJFbtbiBlGTiDA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-6.0.1.tgz", + "integrity": "sha512-8rdM0XEmDGsLuZM2oRABzsLX+XmD5x3rwxPMEPv0MrN9/BWanyy3ilb2v+tCrKtIZVF3MxUiZ9Bfqe8e0popKQ==", "requires": { - "conf": "^1.3.0" + "conf": "^7.1.2", + "type-fest": "^0.16.0" + }, + "dependencies": { + "type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==" + } } }, "electron-updater": { @@ -3028,9 +3335,9 @@ "dev": true }, "env-paths": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz", - "integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", + "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==" }, "error-ex": { "version": "1.3.2", @@ -3050,6 +3357,13 @@ "stackframe": "^1.0.4" } }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "optional": true + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -3305,15 +3619,15 @@ } }, "extract-zip": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz", - "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", + "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", "dev": true, "requires": { - "concat-stream": "1.6.2", - "debug": "2.6.9", - "mkdirp": "0.5.1", - "yauzl": "2.4.1" + "concat-stream": "^1.6.2", + "debug": "^2.6.9", + "mkdirp": "^0.5.4", + "yauzl": "^2.10.0" }, "dependencies": { "debug": { @@ -3324,6 +3638,21 @@ "requires": { "ms": "2.0.0" } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } } } }, @@ -3348,9 +3677,9 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, "fd-slicer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", - "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", "dev": true, "requires": { "pend": "~1.2.0" @@ -3622,6 +3951,58 @@ } } }, + "global-agent": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-2.1.12.tgz", + "integrity": "sha512-caAljRMS/qcDo69X9BfkgrihGUgGx44Fb4QQToNQjsiWh+YlQ66uqYVAdA8Olqit+5Ng0nkz09je3ZzANMZcjg==", + "dev": true, + "optional": true, + "requires": { + "boolean": "^3.0.1", + "core-js": "^3.6.5", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "dependencies": { + "core-js": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.2.tgz", + "integrity": "sha512-FfApuSRgrR6G5s58casCBd9M2k+4ikuu4wbW6pJyYU7bd9zvFc9qf7vr5xmrZOhT9nn+8uwlH1oRR9jTnFoA3A==", + "dev": true, + "optional": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "optional": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "optional": true + } + } + }, "global-dirs": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", @@ -3631,12 +4012,35 @@ "ini": "^1.3.4" } }, + "global-tunnel-ng": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz", + "integrity": "sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg==", + "dev": true, + "optional": true, + "requires": { + "encodeurl": "^1.0.2", + "lodash": "^4.17.10", + "npm-conf": "^1.1.3", + "tunnel": "^0.0.6" + } + }, "globals": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.11.0.tgz", "integrity": "sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw==", "dev": true }, + "globalthis": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.1.tgz", + "integrity": "sha512-mJPRTc/P39NH/iNG4mXa9aIhNymaQikTrnspeCa2ZuJ+mH2QN/rXwtX3XwKrHqWgUQFbNZKtHM105aHzJalElw==", + "dev": true, + "optional": true, + "requires": { + "define-properties": "^1.1.3" + } + }, "got": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", @@ -3843,6 +4247,12 @@ "whatwg-encoding": "^1.0.1" } }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true + }, "http-errors": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", @@ -3935,7 +4345,8 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true }, "indent-string": { "version": "2.1.0", @@ -4185,7 +4596,8 @@ "is-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true }, "is-path-inside": { "version": "1.0.1", @@ -4620,6 +5032,12 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "dev": true + }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", @@ -4630,6 +5048,11 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "json-schema-typed": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz", + "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==" + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -4912,6 +5335,15 @@ } } }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "dev": true, + "requires": { + "json-buffer": "3.0.0" + } + }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -4966,11 +5398,11 @@ } }, "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "requires": { - "p-locate": "^2.0.0", + "p-locate": "^3.0.0", "path-exists": "^3.0.0" }, "dependencies": { @@ -5079,6 +5511,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, "requires": { "pify": "^3.0.0" } @@ -5116,6 +5549,25 @@ "object-visit": "^1.0.0" } }, + "matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "optional": true, + "requires": { + "escape-string-regexp": "^4.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "optional": true + } + } + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -6104,6 +6556,23 @@ "remove-trailing-separator": "^1.0.1" } }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", + "dev": true + }, + "npm-conf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", + "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", + "dev": true, + "optional": true, + "requires": { + "config-chain": "^1.1.11", + "pify": "^3.0.0" + } + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -6124,32 +6593,6 @@ "set-blocking": "~2.0.0" } }, - "nugget": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nugget/-/nugget-2.0.1.tgz", - "integrity": "sha1-IBCVpIfhrTYIGzQy+jytpPjQcbA=", - "dev": true, - "requires": { - "debug": "^2.1.3", - "minimist": "^1.1.0", - "pretty-bytes": "^1.0.2", - "progress-stream": "^1.1.0", - "request": "^2.45.0", - "single-line-log": "^1.1.2", - "throttleit": "0.0.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, "null-check": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz", @@ -6205,10 +6648,11 @@ } }, "object-keys": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", - "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "optional": true }, "object-visit": { "version": "1.0.1", @@ -6477,6 +6921,12 @@ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "dev": true + }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -6484,25 +6934,25 @@ "dev": true }, "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "requires": { - "p-try": "^1.0.0" + "p-try": "^2.0.0" } }, "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "requires": { - "p-limit": "^1.1.0" + "p-limit": "^2.0.0" } }, "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "package-json": { "version": "4.0.1", @@ -6710,19 +7160,19 @@ } }, "pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", - "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", "requires": { - "find-up": "^2.1.0" + "find-up": "^3.0.0" }, "dependencies": { "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "requires": { - "locate-path": "^2.0.0" + "locate-path": "^3.0.0" } } } @@ -6811,16 +7261,6 @@ "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", "dev": true }, - "pretty-bytes": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz", - "integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=", - "dev": true, - "requires": { - "get-stdin": "^4.0.1", - "meow": "^3.1.0" - } - }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -6832,15 +7272,18 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, - "progress-stream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-1.2.0.tgz", - "integrity": "sha1-LNPP6jO6OonJwSHsM0er6asSX3c=", + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", "dev": true, - "requires": { - "speedometer": "~0.1.2", - "through2": "~0.2.3" - } + "optional": true }, "ps-tree": { "version": "1.1.0", @@ -7192,6 +7635,15 @@ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", "dev": true }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "dev": true, + "requires": { + "lowercase-keys": "^1.0.0" + } + }, "restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -7232,6 +7684,30 @@ "inherits": "^2.0.1" } }, + "roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "optional": true, + "requires": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "dependencies": { + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true, + "optional": true + } + } + }, "run-async": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", @@ -7273,6 +7749,15 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "requires": { + "truncate-utf8-bytes": "^1.0.0" + } + }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -7291,6 +7776,13 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" }, + "semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", + "dev": true, + "optional": true + }, "semver-diff": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", @@ -7300,6 +7792,16 @@ "semver": "^5.0.3" } }, + "serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "optional": true, + "requires": { + "type-fest": "^0.13.1" + } + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -7374,15 +7876,6 @@ "simple-concat": "^1.0.0" } }, - "single-line-log": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-1.1.2.tgz", - "integrity": "sha1-wvg/Jzo+GhbtsJlWYdoO1e8DM2Q=", - "dev": true, - "requires": { - "string-width": "^1.0.1" - } - }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -7639,12 +8132,6 @@ "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", "dev": true }, - "speedometer": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-0.1.4.tgz", - "integrity": "sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0=", - "dev": true - }, "split": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", @@ -7864,22 +8351,28 @@ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "sumchecker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-2.0.2.tgz", - "integrity": "sha1-D0LBDl0F2l1C7qPlbDOZo31sWz4=", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", "dev": true, "requires": { - "debug": "^2.2.0" + "debug": "^4.1.0" }, "dependencies": { "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { - "ms": "2.0.0" + "ms": "2.1.2" } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true } } }, @@ -7942,62 +8435,11 @@ "execa": "^0.7.0" } }, - "throttleit": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz", - "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8=", - "dev": true - }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, - "through2": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz", - "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=", - "dev": true, - "requires": { - "readable-stream": "~1.1.9", - "xtend": "~2.1.1" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "xtend": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", - "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", - "dev": true, - "requires": { - "object-keys": "~0.4.0" - } - } - } - }, "timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", @@ -8055,6 +8497,12 @@ "kind-of": "^3.0.2" } }, + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "dev": true + }, "to-regex": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", @@ -8171,6 +8619,15 @@ "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "dev": true }, + "truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=", + "dev": true, + "requires": { + "utf8-byte-length": "^1.0.1" + } + }, "ts-node": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", @@ -8249,6 +8706,13 @@ "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", "dev": true }, + "tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "optional": true + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -8270,6 +8734,13 @@ "prelude-ls": "~1.1.2" } }, + "type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "optional": true + }, "type-is": { "version": "1.6.16", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", @@ -8562,6 +9033,12 @@ "tmp": "0.0.x" } }, + "utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=", + "dev": true + }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", @@ -8770,6 +9247,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", + "dev": true, "requires": { "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", @@ -8821,12 +9299,13 @@ "dev": true }, "yauzl": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", - "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", "dev": true, "requires": { - "fd-slicer": "~1.0.1" + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" } }, "yeast": { diff --git a/package.json b/package.json index 8c04d82a50..6c38632654 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@types/zxcvbn": "^4.4.0", "concurrently": "3.5.1", "cssstyle": "1.2.1", - "electron": "6.1.7", + "electron": "11.1.1", "jasmine": "^3.3.1", "jasmine-core": "^3.3.0", "jasmine-spec-reporter": "^4.2.1", @@ -82,8 +82,8 @@ "commander": "2.18.0", "core-js": "2.6.2", "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", - "electron-log": "2.2.17", - "electron-store": "1.3.0", + "electron-log": "4.3.0", + "electron-store": "6.0.1", "electron-updater": "4.3.5", "forcefocus": "^1.1.0", "form-data": "2.3.2", diff --git a/src/electron/utils.ts b/src/electron/utils.ts index 0bb34ca49e..f9d72ee721 100644 --- a/src/electron/utils.ts +++ b/src/electron/utils.ts @@ -16,11 +16,12 @@ export function isMacAppStore() { export function isWindowsStore() { const isWindows = process.platform === 'win32'; - if (isWindows && !process.windowsStore && + let windowsStore = process.windowsStore; + if (isWindows && !windowsStore && process.resourcesPath.indexOf('8bitSolutionsLLC.bitwardendesktop_') > -1) { - process.windowsStore = true; + windowsStore = true; } - return isWindows && process.windowsStore && process.windowsStore === true; + return isWindows && windowsStore === true; } export function isSnapStore() { diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index 6d7381c4b9..e34d6f6560 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -110,7 +110,7 @@ export class WindowMain { minHeight: 500, x: this.windowStates[Keys.mainWindowSize].x, y: this.windowStates[Keys.mainWindowSize].y, - title: app.getName(), + title: app.name, icon: process.platform === 'linux' ? path.join(__dirname, '/images/icon.png') : undefined, titleBarStyle: this.hideTitleBar && process.platform === 'darwin' ? 'hiddenInset' : undefined, show: false, @@ -119,6 +119,7 @@ export class WindowMain { nodeIntegration: true, webviewTag: true, backgroundThrottling: false, + enableRemoteModule: true, // TODO: This needs to be removed prior to Electron 14. }, }); From 06239aea2d811852561711bd73e14729fba2071a Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Wed, 27 Jan 2021 20:08:56 -0500 Subject: [PATCH 1144/1626] update package https-proxy-agent (#246) * update package http-proxy-agent * Update syntax appropriately * Explicitly add utils package to fix broken tests * Revert "Explicitly add utils package to fix broken tests" This reverts commit 2cf03fdcbae89e55124c7e36201238520434f8cc. * Import util in spec to make sure it gets bundled * Revert "Import util in spec to make sure it gets bundled" This reverts commit 79264cdab0ef37af855d64a6dc3ef574575309d2. * Add alias to ensure util module resolves in tests Co-authored-by: Thomas Rittson --- package-lock.json | 40 ++++++++++++++++++++++++--------- package.json | 2 +- spec/support/karma.conf.js | 7 +++++- src/services/nodeApi.service.ts | 2 +- 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 814424b931..2bd00923e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -668,9 +668,27 @@ "dev": true }, "agent-base": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", - "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==" + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } }, "ajv": { "version": "6.7.0", @@ -4293,20 +4311,20 @@ "dev": true }, "https-proxy-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", - "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", "requires": { - "agent-base": "5", + "agent-base": "6", "debug": "4" }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { diff --git a/package.json b/package.json index 6c38632654..d627e37f31 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "electron-updater": "4.3.5", "forcefocus": "^1.1.0", "form-data": "2.3.2", - "https-proxy-agent": "4.0.0", + "https-proxy-agent": "5.0.0", "inquirer": "6.2.0", "jsdom": "13.2.0", "keytar": "4.13.0", diff --git a/spec/support/karma.conf.js b/spec/support/karma.conf.js index 1196bafb65..b9f064f080 100644 --- a/spec/support/karma.conf.js +++ b/spec/support/karma.conf.js @@ -57,7 +57,12 @@ module.exports = (config) => { tsconfig: './tsconfig.json', bundlerOptions: { entrypoints: /\.spec\.ts$/, - sourceMap: true + sourceMap: true, + resolve: { + alias: { + "util": "node_modules/util/util.js" + } + } } }, diff --git a/src/services/nodeApi.service.ts b/src/services/nodeApi.service.ts index b60b2128f4..744a90cc02 100644 --- a/src/services/nodeApi.service.ts +++ b/src/services/nodeApi.service.ts @@ -1,5 +1,5 @@ import * as FormData from 'form-data'; -import * as HttpsProxyAgent from 'https-proxy-agent'; +import { HttpsProxyAgent } from 'https-proxy-agent'; import * as fe from 'node-fetch'; import { ApiService } from './api.service'; From 09c444ddd4498b5417769e8a795671a6a8ef6ade Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Fri, 29 Jan 2021 15:08:52 -0600 Subject: [PATCH 1145/1626] Add send to cli (#253) * Upgrade commander to 7.0.0 * Add url to Api call This is needed to allow access to sends that are available from a different Bitwarden server than configured for the CLI * Allow upload of send files from CLI * Allow send search by accessId * Utils methods used in Send CLI implementation * Revert adding string type to encrypted file data * linter fixes * Add Buffer to ArrayBuffer used in CLI send implementation --- package-lock.json | 526 ++++++++++++++++++++--------- package.json | 2 +- src/abstractions/api.service.ts | 2 +- src/abstractions/send.service.ts | 2 +- src/cli/commands/login.command.ts | 32 +- src/cli/commands/update.command.ts | 2 +- src/misc/nodeUtils.ts | 5 + src/misc/utils.ts | 8 + src/services/api.service.ts | 10 +- src/services/search.service.ts | 2 +- src/services/send.service.ts | 24 +- 11 files changed, 424 insertions(+), 191 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2bd00923e8..3a0164304f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1680,24 +1680,32 @@ "dependencies": { "abbrev": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "bundled": true, "dev": true, "optional": true }, "ansi-regex": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "bundled": true, "dev": true, "optional": true }, "aproba": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "bundled": true, "dev": true, "optional": true }, "are-we-there-yet": { "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "bundled": true, "dev": true, "optional": true, @@ -1708,12 +1716,16 @@ }, "balanced-match": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "bundled": true, "dev": true, "optional": true }, "brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "bundled": true, "dev": true, "optional": true, @@ -1724,36 +1736,48 @@ }, "chownr": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", "bundled": true, "dev": true, "optional": true }, "code-point-at": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "bundled": true, "dev": true, "optional": true }, "concat-map": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "bundled": true, "dev": true, "optional": true }, "console-control-strings": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "bundled": true, "dev": true, "optional": true }, "core-util-is": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "bundled": true, "dev": true, "optional": true }, "debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "bundled": true, "dev": true, "optional": true, @@ -1763,24 +1787,32 @@ }, "deep-extend": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "bundled": true, "dev": true, "optional": true }, "delegates": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "bundled": true, "dev": true, "optional": true }, "detect-libc": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "bundled": true, "dev": true, "optional": true }, "fs-minipass": { "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "bundled": true, "dev": true, "optional": true, @@ -1790,12 +1822,16 @@ }, "fs.realpath": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "bundled": true, "dev": true, "optional": true }, "gauge": { "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "bundled": true, "dev": true, "optional": true, @@ -1812,6 +1848,8 @@ }, "glob": { "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "bundled": true, "dev": true, "optional": true, @@ -1826,12 +1864,16 @@ }, "has-unicode": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "bundled": true, "dev": true, "optional": true }, "iconv-lite": { "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "bundled": true, "dev": true, "optional": true, @@ -1841,6 +1883,8 @@ }, "ignore-walk": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "bundled": true, "dev": true, "optional": true, @@ -1850,6 +1894,8 @@ }, "inflight": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "bundled": true, "dev": true, "optional": true, @@ -1860,18 +1906,24 @@ }, "inherits": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "bundled": true, "dev": true, "optional": true }, "ini": { "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "bundled": true, "dev": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "bundled": true, "dev": true, "optional": true, @@ -1881,12 +1933,16 @@ }, "isarray": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "bundled": true, "dev": true, "optional": true }, "minimatch": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "bundled": true, "dev": true, "optional": true, @@ -1896,12 +1952,16 @@ }, "minimist": { "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "bundled": true, "dev": true, "optional": true }, "minipass": { "version": "2.3.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "bundled": true, "dev": true, "optional": true, @@ -1912,6 +1972,8 @@ }, "minizlib": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "bundled": true, "dev": true, "optional": true, @@ -1921,6 +1983,8 @@ }, "mkdirp": { "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "bundled": true, "dev": true, "optional": true, @@ -1930,12 +1994,16 @@ }, "ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "bundled": true, "dev": true, "optional": true }, "needle": { "version": "2.2.4", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.4.tgz", + "integrity": "sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==", "bundled": true, "dev": true, "optional": true, @@ -1947,6 +2015,8 @@ }, "node-pre-gyp": { "version": "0.10.3", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", + "integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==", "bundled": true, "dev": true, "optional": true, @@ -1965,6 +2035,8 @@ }, "nopt": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "bundled": true, "dev": true, "optional": true, @@ -1975,12 +2047,16 @@ }, "npm-bundled": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", + "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==", "bundled": true, "dev": true, "optional": true }, "npm-packlist": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.2.0.tgz", + "integrity": "sha512-7Mni4Z8Xkx0/oegoqlcao/JpPCPEMtUvsmB0q7mgvlMinykJLSRTYuFqoQLYgGY8biuxIeiHO+QNJKbCfljewQ==", "bundled": true, "dev": true, "optional": true, @@ -1991,6 +2067,8 @@ }, "npmlog": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "bundled": true, "dev": true, "optional": true, @@ -2003,18 +2081,24 @@ }, "number-is-nan": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "bundled": true, "dev": true, "optional": true }, "object-assign": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "bundled": true, "dev": true, "optional": true }, "once": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "bundled": true, "dev": true, "optional": true, @@ -2024,18 +2108,24 @@ }, "os-homedir": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "bundled": true, "dev": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "bundled": true, "dev": true, "optional": true }, "osenv": { "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "bundled": true, "dev": true, "optional": true, @@ -2046,18 +2136,24 @@ }, "path-is-absolute": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "bundled": true, "dev": true, "optional": true }, "process-nextick-args": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "bundled": true, "dev": true, "optional": true }, "rc": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "bundled": true, "dev": true, "optional": true, @@ -2070,6 +2166,8 @@ "dependencies": { "minimist": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "bundled": true, "dev": true, "optional": true @@ -2078,6 +2176,8 @@ }, "readable-stream": { "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "bundled": true, "dev": true, "optional": true, @@ -2093,6 +2193,8 @@ }, "rimraf": { "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "bundled": true, "dev": true, "optional": true, @@ -2102,42 +2204,67 @@ }, "safe-buffer": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "bundled": true, "dev": true, "optional": true }, "safer-buffer": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "bundled": true, "dev": true, "optional": true }, "sax": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "bundled": true, "dev": true, "optional": true }, "semver": { "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", "bundled": true, "dev": true, "optional": true }, "set-blocking": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "bundled": true, "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "bundled": true, "dev": true, "optional": true }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, "string-width": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "bundled": true, "dev": true, "optional": true, @@ -2147,17 +2274,10 @@ "strip-ansi": "^3.0.0" } }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, "strip-ansi": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "bundled": true, "dev": true, "optional": true, @@ -2167,12 +2287,16 @@ }, "strip-json-comments": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "bundled": true, "dev": true, "optional": true }, "tar": { "version": "4.4.8", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "bundled": true, "dev": true, "optional": true, @@ -2188,12 +2312,16 @@ }, "util-deprecate": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "bundled": true, "dev": true, "optional": true }, "wide-align": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "bundled": true, "dev": true, "optional": true, @@ -2203,12 +2331,16 @@ }, "wrappy": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "bundled": true, "dev": true, "optional": true }, "yallist": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", "bundled": true, "dev": true, "optional": true @@ -2405,9 +2537,9 @@ } }, "commander": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.18.0.tgz", - "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==" + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.0.0.tgz", + "integrity": "sha512-ovx/7NkTrnPuIV8sqk/GjUIIM1+iUQeqA3ye2VNpq9sVoiZsooObWlQy+OPWGI17GDaEoybuAGJm6U8yC077BA==" }, "compare-versions": { "version": "3.4.0", @@ -3127,8 +3259,8 @@ } }, "duo_web_sdk": { - "version": "git+https://github.com/duosecurity/duo_web_sdk.git#410a9186cc34663c4913b17d6528067cd3331f1d", - "from": "git+https://github.com/duosecurity/duo_web_sdk.git" + "version": "git+ssh://git@github.com/duosecurity/duo_web_sdk.git#410a9186cc34663c4913b17d6528067cd3331f1d", + "from": "duo_web_sdk@git+https://github.com/duosecurity/duo_web_sdk.git" }, "duplexer": { "version": "0.1.1", @@ -5962,27 +6094,31 @@ "dependencies": { "abbrev": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "ansi-regex": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "aproba": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "are-we-there-yet": { "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -5990,15 +6126,17 @@ }, "balanced-match": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6006,81 +6144,93 @@ }, "chownr": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "code-point-at": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "concat-map": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "console-control-strings": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "core-util-is": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "debug": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "ms": "^2.1.1" } }, "deep-extend": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "delegates": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "detect-libc": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "fs-minipass": { "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "minipass": "^2.2.1" } }, "fs.realpath": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "gauge": { "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -6094,9 +6244,10 @@ }, "glob": { "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -6108,33 +6259,37 @@ }, "has-unicode": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "iconv-lite": { "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } }, "ignore-walk": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "minimatch": "^3.0.4" } }, "inflight": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -6142,51 +6297,58 @@ }, "inherits": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "ini": { "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "is-fullwidth-code-point": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "number-is-nan": "^1.0.0" } }, "isarray": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "minimatch": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "minipass": { "version": "2.3.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -6194,33 +6356,37 @@ }, "minizlib": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "minipass": "^2.2.1" } }, "mkdirp": { "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "minimist": "0.0.8" } }, "ms": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "needle": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.3.0.tgz", + "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "debug": "^4.1.0", "iconv-lite": "^0.4.4", @@ -6229,9 +6395,10 @@ }, "node-pre-gyp": { "version": "0.12.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz", + "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", @@ -6247,9 +6414,10 @@ }, "nopt": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "abbrev": "1", "osenv": "^0.1.4" @@ -6257,15 +6425,17 @@ }, "npm-bundled": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", + "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "npm-packlist": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz", + "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "ignore-walk": "^3.0.1", "npm-bundled": "^1.0.1" @@ -6273,9 +6443,10 @@ }, "npmlog": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -6285,42 +6456,48 @@ }, "number-is-nan": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "object-assign": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "once": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "wrappy": "1" } }, "os-homedir": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "os-tmpdir": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "osenv": { "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -6328,21 +6505,24 @@ }, "path-is-absolute": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "process-nextick-args": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "rc": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -6352,17 +6532,19 @@ "dependencies": { "minimist": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true } } }, "readable-stream": { "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -6375,89 +6557,101 @@ }, "rimraf": { "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "glob": "^7.1.3" } }, "safe-buffer": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "safer-buffer": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "sax": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "semver": { "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "set-blocking": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "signal-exit": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, + "extraneous": true, + "requires": { + "safe-buffer": "~5.1.0" + } }, "string-width": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", "strip-ansi": "^3.0.0" } }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, "strip-ansi": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "ansi-regex": "^2.0.0" } }, "strip-json-comments": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "tar": { "version": "4.4.8", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", @@ -6470,30 +6664,34 @@ }, "util-deprecate": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "wide-align": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "bundled": true, - "dev": true, - "optional": true, + "extraneous": true, "requires": { "string-width": "^1.0.2 || 2" } }, "wrappy": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "bundled": true, - "dev": true, - "optional": true + "extraneous": true }, "yallist": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", "bundled": true, - "dev": true, - "optional": true + "extraneous": true } } }, @@ -8313,6 +8511,14 @@ "lodash": "^4.17.10" } }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -8323,14 +8529,6 @@ "strip-ansi": "^3.0.0" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -8686,6 +8884,12 @@ "tsutils": "^2.29.0" }, "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", diff --git a/package.json b/package.json index d627e37f31..d9bab067b3 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "big-integer": "1.6.36", "browser-hrtime": "^1.1.8", "chalk": "2.4.1", - "commander": "2.18.0", + "commander": "7.0.0", "core-js": "2.6.2", "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", "electron-log": "4.3.0", diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index ffb676102f..beb9fec305 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -174,7 +174,7 @@ export abstract class ApiService { deleteFolder: (id: string) => Promise; getSend: (id: string) => Promise; - postSendAccess: (id: string, request: SendAccessRequest) => Promise; + postSendAccess: (id: string, request: SendAccessRequest, apiUrl?: string) => Promise; getSends: () => Promise>; postSend: (request: SendRequest) => Promise; postSendFile: (data: FormData) => Promise; diff --git a/src/abstractions/send.service.ts b/src/abstractions/send.service.ts index ae0c76311c..d7901e3d36 100644 --- a/src/abstractions/send.service.ts +++ b/src/abstractions/send.service.ts @@ -9,7 +9,7 @@ export abstract class SendService { decryptedSendCache: SendView[]; clearCache: () => void; - encrypt: (model: SendView, file: File, password: string, key?: SymmetricCryptoKey) => Promise<[Send, ArrayBuffer]>; + encrypt: (model: SendView, file: File | ArrayBuffer, password: string, key?: SymmetricCryptoKey) => Promise<[Send, ArrayBuffer]>; get: (id: string) => Promise; getAll: () => Promise; getAllDecrypted: () => Promise; diff --git a/src/cli/commands/login.command.ts b/src/cli/commands/login.command.ts index 0f58cb361b..46ac1fb2cc 100644 --- a/src/cli/commands/login.command.ts +++ b/src/cli/commands/login.command.ts @@ -41,7 +41,7 @@ export class LoginCommand { this.clientId = clientId; } - async run(email: string, password: string, cmd: program.Command) { + async run(email: string, password: string, options: program.OptionValues) { this.canInteract = process.env.BW_NOINTERACTION !== 'true'; let ssoCodeVerifier: string = null; @@ -50,7 +50,7 @@ export class LoginCommand { let clientId: string = null; let clientSecret: string = null; - if (cmd.apikey != null) { + if (options.apikey != null) { const storedClientId: string = process.env.BW_CLIENTID; const storedClientSecret: string = process.env.BW_CLIENTSECRET; if (storedClientId == null) { @@ -77,7 +77,7 @@ export class LoginCommand { } else { clientSecret = storedClientSecret; } - } else if (cmd.sso != null && this.canInteract) { + } else if (options.sso != null && this.canInteract) { const passwordOptions: any = { type: 'password', length: 64, @@ -112,10 +112,10 @@ export class LoginCommand { } if (password == null || password === '') { - if (cmd.passwordfile) { - password = await NodeUtils.readFirstLine(cmd.passwordfile); - } else if (cmd.passwordenv && process.env[cmd.passwordenv]) { - password = process.env[cmd.passwordenv]; + if (options.passwordfile) { + password = await NodeUtils.readFirstLine(options.passwordfile); + } else if (options.passwordenv && process.env[options.passwordenv]) { + password = process.env[options.passwordenv]; } else if (this.canInteract) { const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ type: 'password', @@ -131,11 +131,11 @@ export class LoginCommand { } } - let twoFactorToken: string = cmd.code; + let twoFactorToken: string = options.code; let twoFactorMethod: TwoFactorProviderType = null; try { - if (cmd.method != null) { - twoFactorMethod = parseInt(cmd.method, null); + if (options.method != null) { + twoFactorMethod = parseInt(options.method, null); } } catch (e) { return Response.error('Invalid two-step login method.'); @@ -185,18 +185,18 @@ export class LoginCommand { if (twoFactorProviders.length === 1) { selectedProvider = twoFactorProviders[0]; } else if (this.canInteract) { - const options = twoFactorProviders.map((p) => p.name); - options.push(new inquirer.Separator()); - options.push('Cancel'); + const twoFactorOptions = twoFactorProviders.map((p) => p.name); + twoFactorOptions.push(new inquirer.Separator()); + twoFactorOptions.push('Cancel'); const answer: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({ type: 'list', name: 'method', message: 'Two-step login method:', - choices: options, + choices: twoFactorOptions, }); - const i = options.indexOf(answer.method); - if (i === (options.length - 1)) { + const i = twoFactorOptions.indexOf(answer.method); + if (i === (twoFactorOptions.length - 1)) { return Response.error('Login failed.'); } selectedProvider = twoFactorProviders[i]; diff --git a/src/cli/commands/update.command.ts b/src/cli/commands/update.command.ts index 93b539ee82..4dc1c58893 100644 --- a/src/cli/commands/update.command.ts +++ b/src/cli/commands/update.command.ts @@ -15,7 +15,7 @@ export class UpdateCommand { this.inPkg = !!(process as any).pkg; } - async run(cmd: program.Command): Promise { + async run(): Promise { const currentVersion = this.platformUtilsService.getApplicationVersion(); const response = await fetch.default('https://api.github.com/repos/bitwarden/' + diff --git a/src/misc/nodeUtils.ts b/src/misc/nodeUtils.ts index f5e216c43f..e1567e26ba 100644 --- a/src/misc/nodeUtils.ts +++ b/src/misc/nodeUtils.ts @@ -26,4 +26,9 @@ export class NodeUtils { .on('error', (err) => reject(err)); }); } + + // https://stackoverflow.com/a/31394257 + static bufferToArrayBuffer(buf: Buffer): ArrayBuffer { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); + } } diff --git a/src/misc/utils.ts b/src/misc/utils.ts index 619270212f..b512cb925d 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -273,6 +273,14 @@ export class Utils { return str == null || typeof str !== 'string' || str.trim() === ''; } + static nameOf(name: string & keyof T) { + return name; + } + + static assign(target: T, source: Partial): T { + return Object.assign(target, source); + } + private static validIpAddress(ipString: string): boolean { // tslint:disable-next-line const ipRegex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index ab0c1c54eb..b35a1d0886 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -73,6 +73,7 @@ import { VerifyBankRequest } from '../models/request/verifyBankRequest'; import { VerifyDeleteRecoverRequest } from '../models/request/verifyDeleteRecoverRequest'; import { VerifyEmailRequest } from '../models/request/verifyEmailRequest'; +import { Utils } from '../misc/utils'; import { ApiKeyResponse } from '../models/response/apiKeyResponse'; import { BillingResponse } from '../models/response/billingResponse'; import { BreachAccountResponse } from '../models/response/breachAccountResponse'; @@ -410,8 +411,8 @@ export class ApiService implements ApiServiceAbstraction { return new SendResponse(r); } - async postSendAccess(id: string, request: SendAccessRequest): Promise { - const r = await this.send('POST', '/sends/access/' + id, request, false, true); + async postSendAccess(id: string, request: SendAccessRequest, apiUrl?: string): Promise { + const r = await this.send('POST', '/sends/access/' + id, request, false, true, apiUrl); return new SendAccessResponse(r); } @@ -1210,7 +1211,8 @@ export class ApiService implements ApiServiceAbstraction { } private async send(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body: any, - authed: boolean, hasResponse: boolean): Promise { + authed: boolean, hasResponse: boolean, apiUrl?: string): Promise { + apiUrl = Utils.isNullOrWhitespace(apiUrl) ? this.apiBaseUrl : apiUrl; const headers = new Headers({ 'Device-Type': this.deviceType, }); @@ -1246,7 +1248,7 @@ export class ApiService implements ApiServiceAbstraction { } requestInit.headers = headers; - const response = await this.fetch(new Request(this.apiBaseUrl + path, requestInit)); + const response = await this.fetch(new Request(apiUrl + path, requestInit)); if (hasResponse && response.status === 200) { const responseJson = await response.json(); diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 0552ffc8c4..0fc96ac5b0 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -169,7 +169,7 @@ export class SearchService implements SearchServiceAbstraction { if (s.name != null && s.name.toLowerCase().indexOf(query) > -1) { return true; } - if (query.length >= 8 && (s.id.startsWith(query) || (s.file?.id != null && s.file.id.startsWith(query)))) { + if (query.length >= 8 && (s.id.startsWith(query) || s.accessId.toLocaleLowerCase().startsWith(query) || (s.file?.id != null && s.file.id.startsWith(query)))) { return true; } if (s.notes != null && s.notes.toLowerCase().indexOf(query) > -1) { diff --git a/src/services/send.service.ts b/src/services/send.service.ts index 212bbff02e..284f60cce3 100644 --- a/src/services/send.service.ts +++ b/src/services/send.service.ts @@ -22,6 +22,7 @@ import { StorageService } from '../abstractions/storage.service'; import { UserService } from '../abstractions/user.service'; import { Utils } from '../misc/utils'; +import { CipherString } from '../models/domain'; const Keys = { sendsPrefix: 'sends_', @@ -38,7 +39,7 @@ export class SendService implements SendServiceAbstraction { this.decryptedSendCache = null; } - async encrypt(model: SendView, file: File, password: string, + async encrypt(model: SendView, file: File | ArrayBuffer, password: string, key?: SymmetricCryptoKey): Promise<[Send, ArrayBuffer]> { let fileData: ArrayBuffer = null; const send = new Send(); @@ -64,7 +65,13 @@ export class SendService implements SendServiceAbstraction { } else if (send.type === SendType.File) { send.file = new SendFile(); if (file != null) { - fileData = await this.parseFile(send, file, model.cryptoKey); + if (file instanceof ArrayBuffer) { + const [name, data] = await this.encryptFileData(model.file.fileName, file, model.cryptoKey); + send.file.fileName = name; + fileData = data; + } else { + fileData = await this.parseFile(send, file, model.cryptoKey); + } } } @@ -227,9 +234,9 @@ export class SendService implements SendServiceAbstraction { reader.readAsArrayBuffer(file); reader.onload = async (evt) => { try { - send.file.fileName = await this.cryptoService.encrypt(file.name, key); - const fileData = await this.cryptoService.encryptToBytes(evt.target.result as ArrayBuffer, key); - resolve(fileData); + const [name, data] = await this.encryptFileData(file.name, evt.target.result as ArrayBuffer, key); + send.file.fileName = name; + resolve(data); } catch (e) { reject(e); } @@ -239,4 +246,11 @@ export class SendService implements SendServiceAbstraction { }; }); } + + private async encryptFileData(fileName: string, data: ArrayBuffer, + key: SymmetricCryptoKey): Promise<[CipherString, ArrayBuffer]> { + const encFileName = await this.cryptoService.encrypt(fileName, key); + const encFileData = await this.cryptoService.encryptToBytes(data, key); + return [encFileName, encFileData]; + } } From 859f317d59189d223072a406bc2d6924e1fb71bc Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Mon, 1 Feb 2021 09:44:30 -0600 Subject: [PATCH 1146/1626] [Send] Port web based components (#254) * Initial port of web based send components * Updated import order to satisfy linter --- .../components/send/add-edit.component.ts | 231 ++++++++++++++++++ src/angular/components/send/send.component.ts | 195 +++++++++++++++ 2 files changed, 426 insertions(+) create mode 100644 src/angular/components/send/add-edit.component.ts create mode 100644 src/angular/components/send/send.component.ts diff --git a/src/angular/components/send/add-edit.component.ts b/src/angular/components/send/add-edit.component.ts new file mode 100644 index 0000000000..fb90fa9762 --- /dev/null +++ b/src/angular/components/send/add-edit.component.ts @@ -0,0 +1,231 @@ +import { DatePipe } from '@angular/common'; + +import { + EventEmitter, + Input, + OnInit, + Output, +} from '@angular/core'; + +import { SendType } from '../../../enums/sendType'; + +import { EnvironmentService } from '../../../abstractions/environment.service'; +import { I18nService } from '../../../abstractions/i18n.service'; +import { MessagingService } from '../../../abstractions/messaging.service'; +import { PlatformUtilsService } from '../../../abstractions/platformUtils.service'; +import { SendService } from '../../../abstractions/send.service'; +import { UserService } from '../../../abstractions/user.service'; + +import { SendFileView } from '../../../models/view/sendFileView'; +import { SendTextView } from '../../../models/view/sendTextView'; +import { SendView } from '../../../models/view/sendView'; + +import { Send } from '../../../models/domain/send'; + +export class AddEditComponent implements OnInit { + @Input() sendId: string; + @Input() type: SendType; + + @Output() onSavedSend = new EventEmitter(); + @Output() onDeletedSend = new EventEmitter(); + @Output() onCancelled = new EventEmitter(); + + editMode: boolean = false; + send: SendView; + link: string; + title: string; + deletionDate: string; + expirationDate: string; + hasPassword: boolean; + password: string; + formPromise: Promise; + deletePromise: Promise; + sendType = SendType; + typeOptions: any[]; + deletionDateOptions: any[]; + expirationDateOptions: any[]; + deletionDateSelect = 168; + expirationDateSelect: number = null; + canAccessPremium = true; + premiumRequiredAlertShown = false; + + constructor(protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, + protected environmentService: EnvironmentService, protected datePipe: DatePipe, + protected sendService: SendService, protected userService: UserService, + protected messagingService: MessagingService) { + this.typeOptions = [ + { name: i18nService.t('sendTypeFile'), value: SendType.File }, + { name: i18nService.t('sendTypeText'), value: SendType.Text }, + ]; + this.deletionDateOptions = this.expirationDateOptions = [ + { name: i18nService.t('oneHour'), value: 1 }, + { name: i18nService.t('oneDay'), value: 24 }, + { name: i18nService.t('days', '2'), value: 48 }, + { name: i18nService.t('days', '3'), value: 72 }, + { name: i18nService.t('days', '7'), value: 168 }, + { name: i18nService.t('days', '30'), value: 720 }, + { name: i18nService.t('custom'), value: 0 }, + ]; + this.expirationDateOptions = [ + { name: i18nService.t('never'), value: null }, + ].concat([...this.deletionDateOptions]); + } + + async ngOnInit() { + await this.load(); + } + + async load() { + this.editMode = this.sendId != null; + if (this.editMode) { + this.editMode = true; + this.title = this.i18nService.t('editSend'); + } else { + this.title = this.i18nService.t('createSend'); + } + + this.canAccessPremium = await this.userService.canAccessPremium(); + if (!this.canAccessPremium) { + this.type = SendType.Text; + } + + if (this.send == null) { + if (this.editMode) { + const send = await this.loadSend(); + this.send = await send.decrypt(); + } else { + this.send = new SendView(); + this.send.type = this.type == null ? SendType.File : this.type; + this.send.file = new SendFileView(); + this.send.text = new SendTextView(); + this.send.deletionDate = new Date(); + this.send.deletionDate.setDate(this.send.deletionDate.getDate() + 7); + } + } + + this.hasPassword = this.send.password != null && this.send.password.trim() !== ''; + + // Parse dates + this.deletionDate = this.dateToString(this.send.deletionDate); + this.expirationDate = this.dateToString(this.send.expirationDate); + + if (this.editMode) { + let webVaultUrl = this.environmentService.getWebVaultUrl(); + if (webVaultUrl == null) { + webVaultUrl = 'https://vault.bitwarden.com'; + } + this.link = webVaultUrl + '/#/send/' + this.send.accessId + '/' + this.send.urlB64Key; + } + } + + async submit(): Promise { + if (this.send.name == null || this.send.name === '') { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('nameRequired')); + return false; + } + + let file: File = null; + if (this.send.type === SendType.File && !this.editMode) { + const fileEl = document.getElementById('file') as HTMLInputElement; + const files = fileEl.files; + if (files == null || files.length === 0) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('selectFile')); + return; + } + + file = files[0]; + if (file.size > 104857600) { // 100 MB + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('maxFileSize')); + return; + } + } + + if (!this.editMode) { + const now = new Date(); + if (this.deletionDateSelect > 0) { + const d = new Date(); + d.setHours(now.getHours() + this.deletionDateSelect); + this.deletionDate = this.dateToString(d); + } + if (this.expirationDateSelect != null && this.expirationDateSelect > 0) { + const d = new Date(); + d.setHours(now.getHours() + this.expirationDateSelect); + this.expirationDate = this.dateToString(d); + } + } + + const encSend = await this.encryptSend(file); + try { + this.formPromise = this.sendService.saveWithServer(encSend); + await this.formPromise; + this.send.id = encSend[0].id; + this.platformUtilsService.showToast('success', null, + this.i18nService.t(this.editMode ? 'editedSend' : 'createdSend')); + this.onSavedSend.emit(this.send); + return true; + } catch { } + + return false; + } + + clearExpiration() { + this.expirationDate = null; + } + + async delete(): Promise { + if (this.deletePromise != null) { + return; + } + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('deleteSendConfirmation'), + this.i18nService.t('deleteSend'), + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return; + } + + try { + this.deletePromise = this.sendService.deleteWithServer(this.send.id); + await this.deletePromise; + this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend')); + await this.load(); + this.onDeletedSend.emit(this.send); + } catch { } + } + + typeChanged() { + if (!this.canAccessPremium && this.send.type === SendType.File && !this.premiumRequiredAlertShown) { + this.premiumRequiredAlertShown = true; + this.messagingService.send('premiumRequired'); + } + } + + protected async loadSend(): Promise { + return this.sendService.get(this.sendId); + } + + protected async encryptSend(file: File): Promise<[Send, ArrayBuffer]> { + const sendData = await this.sendService.encrypt(this.send, file, this.password, null); + + // Parse dates + try { + sendData[0].deletionDate = this.deletionDate == null ? null : new Date(this.deletionDate); + } catch { + sendData[0].deletionDate = null; + } + try { + sendData[0].expirationDate = this.expirationDate == null ? null : new Date(this.expirationDate); + } catch { + sendData[0].expirationDate = null; + } + + return sendData; + } + + protected dateToString(d: Date) { + return d == null ? null : this.datePipe.transform(d, 'yyyy-MM-ddTHH:mm'); + } +} diff --git a/src/angular/components/send/send.component.ts b/src/angular/components/send/send.component.ts new file mode 100644 index 0000000000..2d250f2ff4 --- /dev/null +++ b/src/angular/components/send/send.component.ts @@ -0,0 +1,195 @@ +import { + NgZone, + OnInit, +} from '@angular/core'; + +import { SendType } from '../../../enums/sendType'; + +import { SendView } from '../../../models/view/sendView'; + +import { EnvironmentService } from '../../../abstractions/environment.service'; +import { I18nService } from '../../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../../abstractions/platformUtils.service'; +import { SendService } from '../../../abstractions/send.service'; + +import { BroadcasterService } from '../../../angular/services/broadcaster.service'; + +const BroadcasterSubscriptionId = 'SendComponent'; + +export class SendComponent implements OnInit { + + sendType = SendType; + loaded = false; + loading = true; + refreshing = false; + expired: boolean = false; + type: SendType = null; + sends: SendView[] = []; + filteredSends: SendView[] = []; + searchText: string; + selectedType: SendType; + selectedAll: boolean; + searchPlaceholder: string; + filter: (cipher: SendView) => boolean; + searchPending = false; + + actionPromise: any; + onSuccessfulRemovePassword: () => Promise; + onSuccessfulDelete: () => Promise; + + private searchTimeout: any; + + constructor(protected sendService: SendService, protected i18nService: I18nService, + protected platformUtilsService: PlatformUtilsService, protected environmentService: EnvironmentService, + protected broadcasterService: BroadcasterService, protected ngZone: NgZone) { } + + async ngOnInit() { + this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { + this.ngZone.run(async () => { + switch (message.command) { + case 'syncCompleted': + if (message.successfully) { + await this.load(); + } + break; + } + }); + }); + + await this.load(); + } + + ngOnDestroy() { + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + } + + async load(filter: (send: SendView) => boolean = null) { + this.loading = true; + const sends = await this.sendService.getAllDecrypted(); + this.sends = sends; + this.selectAll(); + this.loading = false; + this.loaded = true; + } + + async reload(filter: (send: SendView) => boolean = null) { + this.loaded = false; + this.sends = []; + await this.load(filter); + } + + async refresh() { + try { + this.refreshing = true; + await this.reload(this.filter); + } finally { + this.refreshing = false; + } + } + + async applyFilter(filter: (send: SendView) => boolean = null) { + this.filter = filter; + await this.search(null); + } + + async search(timeout: number = null) { + this.searchPending = false; + if (this.searchTimeout != null) { + clearTimeout(this.searchTimeout); + } + if (timeout == null) { + this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s)); + return; + } + this.searchPending = true; + this.searchTimeout = setTimeout(async () => { + this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s)); + this.searchPending = false; + }, timeout); + } + + async removePassword(s: SendView): Promise { + if (this.actionPromise != null || s.password == null) { + return; + } + const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('removePasswordConfirmation'), + this.i18nService.t('removePassword'), + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return false; + } + + try { + this.actionPromise = this.sendService.removePasswordWithServer(s.id); + await this.actionPromise; + if (this.onSuccessfulRemovePassword() != null) { + this.onSuccessfulRemovePassword(); + } else { + // Default actions + this.platformUtilsService.showToast('success', null, this.i18nService.t('removedPassword')); + await this.load(); + } + } catch { } + this.actionPromise = null; + } + + async delete(s: SendView): Promise { + if (this.actionPromise != null) { + return false; + } + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('deleteSendConfirmation'), + this.i18nService.t('deleteSend'), + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return false; + } + + try { + this.actionPromise = this.sendService.deleteWithServer(s.id); + await this.actionPromise; + + if (this.onSuccessfulDelete() != null) { + this.onSuccessfulDelete(); + } else { + // Default actions + this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend')); + await this.load(); + } + } catch { } + this.actionPromise = null; + return true; + } + + copy(s: SendView) { + let webVaultUrl = this.environmentService.getWebVaultUrl(); + if (webVaultUrl == null) { + webVaultUrl = 'https://vault.bitwarden.com'; + } + const link = webVaultUrl + '/#/send/' + s.accessId + '/' + s.urlB64Key; + this.platformUtilsService.copyToClipboard(link); + this.platformUtilsService.showToast('success', null, + this.i18nService.t('valueCopied', this.i18nService.t('sendLink'))); + } + + searchTextChanged() { + this.search(200); + } + + selectAll() { + this.clearSelections(); + this.selectedAll = true; + this.applyFilter(null); + } + + selectType(type: SendType) { + this.clearSelections(); + this.selectedType = type; + this.applyFilter((s) => s.type === type); + } + + clearSelections() { + this.selectedAll = false; + this.selectedType = null; + } +} From 11249e34441ea747f53fcb0b6e38f690366b46b5 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Wed, 3 Feb 2021 18:02:02 +0100 Subject: [PATCH 1147/1626] Remove a safari hack (#224) --- src/services/api.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/api.service.ts b/src/services/api.service.ts index b35a1d0886..41908283f3 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -1318,7 +1318,7 @@ export class ApiService implements ApiServiceAbstraction { } private getCredentials(): RequestCredentials { - if (this.device !== DeviceType.SafariExtension && (!this.isWebClient || this.usingBaseUrl)) { + if (!this.isWebClient || this.usingBaseUrl) { return 'include'; } return undefined; From a16d8f7de7abe63532bcf7452cb7517f9174189a Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Wed, 3 Feb 2021 15:36:15 -0500 Subject: [PATCH 1148/1626] Send search (#258) * fixed text searching sends * fixed text searching sends * cleanup * cleanup --- src/angular/components/send/send.component.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/angular/components/send/send.component.ts b/src/angular/components/send/send.component.ts index 2d250f2ff4..85843fecca 100644 --- a/src/angular/components/send/send.component.ts +++ b/src/angular/components/send/send.component.ts @@ -10,6 +10,7 @@ import { SendView } from '../../../models/view/sendView'; import { EnvironmentService } from '../../../abstractions/environment.service'; import { I18nService } from '../../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../../abstractions/platformUtils.service'; +import { SearchService } from '../../../abstractions/search.service'; import { SendService } from '../../../abstractions/send.service'; import { BroadcasterService } from '../../../angular/services/broadcaster.service'; @@ -41,7 +42,8 @@ export class SendComponent implements OnInit { constructor(protected sendService: SendService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected environmentService: EnvironmentService, - protected broadcasterService: BroadcasterService, protected ngZone: NgZone) { } + protected broadcasterService: BroadcasterService, protected ngZone: NgZone, + protected searchService: SearchService) { } async ngOnInit() { this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { @@ -99,11 +101,13 @@ export class SendComponent implements OnInit { } if (timeout == null) { this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s)); + this.applyTextSearch(); return; } this.searchPending = true; this.searchTimeout = setTimeout(async () => { this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s)); + this.applyTextSearch(); this.searchPending = false; }, timeout); } @@ -192,4 +196,10 @@ export class SendComponent implements OnInit { this.selectedAll = false; this.selectedType = null; } + + private applyTextSearch() { + if (this.searchText != null) { + this.filteredSends = this.searchService.searchSends(this.filteredSends, this.searchText); + } + } } From 58f40b008520018d1c987c9eac64585343432109 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Thu, 4 Feb 2021 09:49:23 -0600 Subject: [PATCH 1149/1626] Fix glob processing in npm. Ban single param parens (#257) --- package.json | 4 +- .../importers/lastpassCsvImporter.spec.ts | 2 +- spec/common/misc/sequentialize.spec.ts | 32 +++++----- spec/common/misc/throttle.spec.ts | 8 +-- .../services/consoleLog.service.spec.ts | 4 +- spec/utils.ts | 2 +- .../webCryptoFunction.service.spec.ts | 4 +- src/angular/components/add-edit.component.ts | 12 ++-- .../components/attachments.component.ts | 2 +- src/angular/components/ciphers.component.ts | 2 +- .../components/collections.component.ts | 10 ++-- src/angular/components/groupings.component.ts | 2 +- src/angular/components/modal.component.ts | 2 +- src/angular/components/send/send.component.ts | 6 +- .../components/set-password.component.ts | 2 +- src/angular/components/share.component.ts | 12 ++-- src/angular/components/sso.component.ts | 2 +- .../components/two-factor.component.ts | 2 +- src/angular/directives/box-row.directive.ts | 2 +- src/angular/pipes/search-ciphers.pipe.ts | 4 +- src/angular/pipes/search.pipe.ts | 2 +- src/cli/commands/login.command.ts | 4 +- src/cli/models/response.ts | 2 +- .../services/electronPlatformUtils.service.ts | 6 +- src/electron/updater.main.ts | 4 +- src/importers/ascendoCsvImporter.ts | 2 +- src/importers/avastCsvImporter.ts | 2 +- src/importers/aviraCsvImporter.ts | 2 +- src/importers/baseImporter.ts | 10 ++-- src/importers/bitwardenCsvImporter.ts | 4 +- src/importers/bitwardenJsonImporter.ts | 4 +- src/importers/blackBerryCsvImporter.ts | 2 +- src/importers/blurCsvImporter.ts | 2 +- src/importers/buttercupCsvImporter.ts | 2 +- src/importers/chromeCsvImporter.ts | 2 +- src/importers/codebookCsvImporter.ts | 2 +- src/importers/encryptrCsvImporter.ts | 2 +- src/importers/enpassCsvImporter.ts | 4 +- src/importers/firefoxCsvImporter.ts | 2 +- src/importers/kasperskyTxtImporter.ts | 8 +-- src/importers/keepass2XmlImporter.ts | 6 +- src/importers/keepassxCsvImporter.ts | 2 +- src/importers/keeperCsvImporter.ts | 2 +- src/importers/lastpassCsvImporter.ts | 2 +- src/importers/logMeOnceCsvImporter.ts | 2 +- src/importers/meldiumCsvImporter.ts | 2 +- src/importers/msecureCsvImporter.ts | 2 +- src/importers/mykiCsvImporter.ts | 2 +- .../onepassword1PifImporter.ts | 4 +- .../onepasswordCsvImporter.ts | 2 +- src/importers/padlockCsvImporter.ts | 4 +- src/importers/passkeepCsvImporter.ts | 2 +- src/importers/passpackCsvImporter.ts | 4 +- src/importers/passwordAgentCsvImporter.ts | 2 +- src/importers/passwordDragonXmlImporter.ts | 4 +- src/importers/passwordSafeXmlImporter.ts | 2 +- src/importers/passwordWalletTxtImporter.ts | 2 +- src/importers/rememBearCsvImporter.ts | 2 +- src/importers/roboformCsvImporter.ts | 2 +- src/importers/safeInCloudXmlImporter.ts | 8 +-- src/importers/saferpassCsvImport.ts | 2 +- src/importers/secureSafeCsvImporter.ts | 2 +- src/importers/splashIdCsvImporter.ts | 2 +- src/importers/stickyPasswordXmlImporter.ts | 2 +- src/importers/truekeyCsvImporter.ts | 2 +- src/importers/upmCsvImporter.ts | 2 +- src/importers/yotiCsvImporter.ts | 2 +- src/importers/zohoVaultCsvImporter.ts | 4 +- src/misc/analytics.ts | 2 +- src/misc/nodeUtils.ts | 4 +- src/misc/serviceUtils.ts | 2 +- src/misc/utils.ts | 6 +- src/models/data/cipherData.ts | 6 +- src/models/data/loginData.ts | 2 +- src/models/domain/cipher.ts | 18 +++--- src/models/domain/login.ts | 4 +- src/models/export/cipher.ts | 8 +-- src/models/export/login.ts | 8 +-- src/models/request/cipherBulkShareRequest.ts | 2 +- src/models/request/cipherRequest.ts | 8 +-- src/models/view/loginView.ts | 4 +- src/services/api.service.ts | 2 +- src/services/audit.service.ts | 2 +- src/services/broadcaster.service.ts | 2 +- src/services/cipher.service.ts | 44 +++++++------- src/services/collection.service.ts | 10 ++-- src/services/crypto.service.ts | 2 +- src/services/event.service.ts | 4 +- src/services/export.service.ts | 60 +++++++++---------- src/services/folder.service.ts | 10 ++-- src/services/import.service.ts | 4 +- src/services/passwordGeneration.service.ts | 6 +- src/services/policy.service.ts | 6 +- src/services/search.service.ts | 28 ++++----- src/services/send.service.ts | 12 ++-- src/services/sync.service.ts | 14 ++--- src/services/system.service.ts | 2 +- tslint.json | 4 ++ 98 files changed, 275 insertions(+), 271 deletions(-) diff --git a/package.json b/package.json index d9bab067b3..237c582897 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "clean": "rimraf dist/**/*", "build": "npm run clean && tsc", "build:watch": "npm run clean && tsc -watch", - "lint": "tslint src/**/*.ts spec/**/*.ts", - "lint:fix": "tslint src/**/*.ts spec/**/*.ts --fix", + "lint": "tslint 'src/**/*.ts' 'spec/**/*.ts'", + "lint:fix": "tslint 'src/**/*.ts' 'spec/**/*.ts' --fix", "test": "karma start ./spec/support/karma.conf.js --single-run", "test:watch": "karma start ./spec/support/karma.conf.js", "test:node": "npm run build && jasmine", diff --git a/spec/common/importers/lastpassCsvImporter.spec.ts b/spec/common/importers/lastpassCsvImporter.spec.ts index df1dea0fb8..fc8d152620 100644 --- a/spec/common/importers/lastpassCsvImporter.spec.ts +++ b/spec/common/importers/lastpassCsvImporter.spec.ts @@ -160,7 +160,7 @@ Notes:",nomonth,,0`, ]; describe('Lastpass CSV Importer', () => { - CipherData.forEach((data) => { + CipherData.forEach(data => { it(data.title, async () => { const importer = new Importer(); const result = await importer.parse(data.csv); diff --git a/spec/common/misc/sequentialize.spec.ts b/spec/common/misc/sequentialize.spec.ts index f3ec1d0ec3..fa184ef444 100644 --- a/spec/common/misc/sequentialize.spec.ts +++ b/spec/common/misc/sequentialize.spec.ts @@ -84,12 +84,12 @@ describe('sequentialize decorator', () => { const allRes: number[] = []; await Promise.all([ - foo.bar(1).then((res) => allRes.push(res)), - foo.bar(1).then((res) => allRes.push(res)), - foo.bar(2).then((res) => allRes.push(res)), - foo.bar(2).then((res) => allRes.push(res)), - foo.bar(3).then((res) => allRes.push(res)), - foo.bar(3).then((res) => allRes.push(res)), + foo.bar(1).then(res => allRes.push(res)), + foo.bar(1).then(res => allRes.push(res)), + foo.bar(2).then(res => allRes.push(res)), + foo.bar(2).then(res => allRes.push(res)), + foo.bar(3).then(res => allRes.push(res)), + foo.bar(3).then(res => allRes.push(res)), ]); expect(foo.calls).toBe(3); expect(allRes.length).toBe(6); @@ -102,12 +102,12 @@ describe('sequentialize decorator', () => { const allRes: number[] = []; await Promise.all([ - foo.baz(1).then((res) => allRes.push(res)), - foo.baz(1).then((res) => allRes.push(res)), - foo.baz(2).then((res) => allRes.push(res)), - foo.baz(2).then((res) => allRes.push(res)), - foo.baz(3).then((res) => allRes.push(res)), - foo.baz(3).then((res) => allRes.push(res)), + foo.baz(1).then(res => allRes.push(res)), + foo.baz(1).then(res => allRes.push(res)), + foo.baz(2).then(res => allRes.push(res)), + foo.baz(2).then(res => allRes.push(res)), + foo.baz(3).then(res => allRes.push(res)), + foo.baz(3).then(res => allRes.push(res)), ]); expect(foo.calls).toBe(3); expect(allRes.length).toBe(6); @@ -119,20 +119,20 @@ describe('sequentialize decorator', () => { class Foo { calls = 0; - @sequentialize((args) => 'bar' + args[0]) + @sequentialize(args => 'bar' + args[0]) bar(a: number): Promise { this.calls++; - return new Promise((res) => { + return new Promise(res => { setTimeout(() => { res(a * 2); }, Math.random() * 100); }); } - @sequentialize((args) => 'baz' + args[0]) + @sequentialize(args => 'baz' + args[0]) baz(a: number): Promise { this.calls++; - return new Promise((res) => { + return new Promise(res => { setTimeout(() => { res(a * 3); }, Math.random() * 100); diff --git a/spec/common/misc/throttle.spec.ts b/spec/common/misc/throttle.spec.ts index f7a273a6f9..6f9237573f 100644 --- a/spec/common/misc/throttle.spec.ts +++ b/spec/common/misc/throttle.spec.ts @@ -72,7 +72,7 @@ class Foo { bar(a: number) { this.calls++; this.inflight++; - return new Promise((res) => { + return new Promise(res => { setTimeout(() => { expect(this.inflight).toBe(1); this.inflight--; @@ -85,7 +85,7 @@ class Foo { baz(a: number) { this.calls++; this.inflight++; - return new Promise((res) => { + return new Promise(res => { setTimeout(() => { expect(this.inflight).toBeLessThanOrEqual(5); this.inflight--; @@ -94,12 +94,12 @@ class Foo { }); } - @sequentialize((args) => 'qux' + args[0]) + @sequentialize(args => 'qux' + args[0]) @throttle(1, () => 'qux') qux(a: number) { this.calls++; this.inflight++; - return new Promise((res) => { + return new Promise(res => { setTimeout(() => { expect(this.inflight).toBe(1); this.inflight--; diff --git a/spec/common/services/consoleLog.service.spec.ts b/spec/common/services/consoleLog.service.spec.ts index b4f7d9d612..2aa062418b 100644 --- a/spec/common/services/consoleLog.service.spec.ts +++ b/spec/common/services/consoleLog.service.spec.ts @@ -40,7 +40,7 @@ describe('ConsoleLogService', () => { }); it('filters messages below the set threshold', () => { - logService = new ConsoleLogService(true, (level) => true); + logService = new ConsoleLogService(true, level => true); logService.debug('debug'); logService.info('info'); logService.warning('warning'); @@ -86,7 +86,7 @@ describe('ConsoleLogService', () => { }); it('filters time output', async () => { - logService = new ConsoleLogService(true, (level) => true); + logService = new ConsoleLogService(true, level => true); logService.time(); logService.timeEnd(); diff --git a/spec/utils.ts b/spec/utils.ts index 94535d9b82..4df925ed5a 100644 --- a/spec/utils.ts +++ b/spec/utils.ts @@ -1,5 +1,5 @@ function newGuid() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { // tslint:disable:no-bitwise const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); diff --git a/spec/web/services/webCryptoFunction.service.spec.ts b/spec/web/services/webCryptoFunction.service.spec.ts index ee81c14cdc..35204d29c5 100644 --- a/spec/web/services/webCryptoFunction.service.spec.ts +++ b/spec/web/services/webCryptoFunction.service.spec.ts @@ -468,8 +468,8 @@ function testRsaGenerateKeyPair(length: 1024 | 2048 | 4096) { function getWebCryptoFunctionService() { const platformUtilsMock = TypeMoq.Mock.ofType(PlatformUtilsServiceMock); - platformUtilsMock.setup((x) => x.isEdge()).returns(() => navigator.userAgent.indexOf(' Edg/') !== -1); - platformUtilsMock.setup((x) => x.isIE()).returns(() => navigator.userAgent.indexOf(' Edg/') === -1 && + platformUtilsMock.setup(x => x.isEdge()).returns(() => navigator.userAgent.indexOf(' Edg/') !== -1); + platformUtilsMock.setup(x => x.isIE()).returns(() => navigator.userAgent.indexOf(' Edg/') === -1 && navigator.userAgent.indexOf(' Trident/') !== -1); return new WebCryptoFunctionService(window, platformUtilsMock.object); } diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts index ede0b2f107..a348878089 100644 --- a/src/angular/components/add-edit.component.ts +++ b/src/angular/components/add-edit.component.ts @@ -159,7 +159,7 @@ export class AddEditComponent implements OnInit { const myEmail = await this.userService.getEmail(); this.ownershipOptions.push({ name: myEmail, value: null }); const orgs = await this.userService.getAllOrganizations(); - orgs.sort(Utils.getSortFunction(this.i18nService, 'name')).forEach((o) => { + orgs.sort(Utils.getSortFunction(this.i18nService, 'name')).forEach(o => { if (o.enabled && o.status === OrganizationUserStatusType.Confirmed) { this.ownershipOptions.push({ name: o.name, value: o.id }); if (policies != null && o.usePolicies && !o.canManagePolicies && this.allowPersonal) { @@ -231,7 +231,7 @@ export class AddEditComponent implements OnInit { if (this.cipher != null && (!this.editMode || addEditCipherInfo != null || this.cloneMode)) { await this.organizationChanged(); if (this.collectionIds != null && this.collectionIds.length > 0 && this.collections.length > 0) { - this.collections.forEach((c) => { + this.collections.forEach(c => { if (this.collectionIds.indexOf(c.id) > -1) { (c as any).checked = true; } @@ -273,7 +273,7 @@ export class AddEditComponent implements OnInit { // Allows saving of selected collections during "Add" and "Clone" flows if ((!this.editMode || this.cloneMode) && this.cipher.organizationId != null) { this.cipher.collectionIds = this.collections == null ? [] : - this.collections.filter((c) => (c as any).checked).map((c) => c.id); + this.collections.filter(c => (c as any).checked).map(c => c.id); } // Clear current Cipher Id to trigger "Add" cipher flow @@ -459,10 +459,10 @@ export class AddEditComponent implements OnInit { async organizationChanged() { if (this.writeableCollections != null) { - this.writeableCollections.forEach((c) => (c as any).checked = false); + this.writeableCollections.forEach(c => (c as any).checked = false); } if (this.cipher.organizationId != null) { - this.collections = this.writeableCollections.filter((c) => c.organizationId === this.cipher.organizationId); + this.collections = this.writeableCollections.filter(c => c.organizationId === this.cipher.organizationId); const org = await this.userService.getOrganization(this.cipher.organizationId); if (org != null) { this.cipher.organizationUseTotp = org.useTotp; @@ -496,7 +496,7 @@ export class AddEditComponent implements OnInit { protected async loadCollections() { const allCollections = await this.collectionService.getAllDecrypted(); - return allCollections.filter((c) => !c.readOnly); + return allCollections.filter(c => !c.readOnly); } protected loadCipher() { diff --git a/src/angular/components/attachments.component.ts b/src/angular/components/attachments.component.ts index 5c8e00c137..8de40cf1fe 100644 --- a/src/angular/components/attachments.component.ts +++ b/src/angular/components/attachments.component.ts @@ -192,7 +192,7 @@ export class AttachmentsComponent implements OnInit { // 3. Delete old this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id); await this.deletePromises[attachment.id]; - const foundAttachment = this.cipher.attachments.filter((a2) => a2.id === attachment.id); + const foundAttachment = this.cipher.attachments.filter(a2 => a2.id === attachment.id); if (foundAttachment.length > 0) { const i = this.cipher.attachments.indexOf(foundAttachment[0]); if (i > -1) { diff --git a/src/angular/components/ciphers.component.ts b/src/angular/components/ciphers.component.ts index 0cca07d3d6..94330da652 100644 --- a/src/angular/components/ciphers.component.ts +++ b/src/angular/components/ciphers.component.ts @@ -82,7 +82,7 @@ export class CiphersComponent { if (this.searchTimeout != null) { clearTimeout(this.searchTimeout); } - const deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted; + const deletedFilter: (cipher: CipherView) => boolean = c => c.isDeleted === this.deleted; if (timeout == null) { this.ciphers = await this.searchService.searchCiphers(this.searchText, [this.filter, deletedFilter], null); await this.resetPaging(); diff --git a/src/angular/components/collections.component.ts b/src/angular/components/collections.component.ts index c6be2fc985..336334e9c4 100644 --- a/src/angular/components/collections.component.ts +++ b/src/angular/components/collections.component.ts @@ -42,9 +42,9 @@ export class CollectionsComponent implements OnInit { this.cipher = await this.cipherDomain.decrypt(); this.collections = await this.loadCollections(); - this.collections.forEach((c) => (c as any).checked = false); + this.collections.forEach(c => (c as any).checked = false); if (this.collectionIds != null) { - this.collections.forEach((c) => { + this.collections.forEach(c => { (c as any).checked = this.collectionIds != null && this.collectionIds.indexOf(c.id) > -1; }); } @@ -52,8 +52,8 @@ export class CollectionsComponent implements OnInit { async submit() { const selectedCollectionIds = this.collections - .filter((c) => !!(c as any).checked) - .map((c) => c.id); + .filter(c => !!(c as any).checked) + .map(c => c.id); if (!this.allowSelectNone && selectedCollectionIds.length === 0) { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('selectOneCollection')); @@ -79,7 +79,7 @@ export class CollectionsComponent implements OnInit { protected async loadCollections() { const allCollections = await this.collectionService.getAllDecrypted(); - return allCollections.filter((c) => !c.readOnly && c.organizationId === this.cipher.organizationId); + return allCollections.filter(c => !c.readOnly && c.organizationId === this.cipher.organizationId); } protected saveCollections() { diff --git a/src/angular/components/groupings.component.ts b/src/angular/components/groupings.component.ts index f8f1c8cf2d..9141106944 100644 --- a/src/angular/components/groupings.component.ts +++ b/src/angular/components/groupings.component.ts @@ -79,7 +79,7 @@ export class GroupingsComponent { } const collections = await this.collectionService.getAllDecrypted(); if (organizationId != null) { - this.collections = collections.filter((c) => c.organizationId === organizationId); + this.collections = collections.filter(c => c.organizationId === organizationId); } else { this.collections = collections; } diff --git a/src/angular/components/modal.component.ts b/src/angular/components/modal.component.ts index b58c739a7f..3057aeb3ab 100644 --- a/src/angular/components/modal.component.ts +++ b/src/angular/components/modal.component.ts @@ -56,7 +56,7 @@ export class ModalComponent implements OnDestroy { const modals = Array.from(document.querySelectorAll('.modal, .modal *[data-dismiss="modal"]')); for (const closeElement of modals) { - closeElement.addEventListener('click', (event) => { + closeElement.addEventListener('click', event => { this.close(); }); } diff --git a/src/angular/components/send/send.component.ts b/src/angular/components/send/send.component.ts index 85843fecca..674e9622f6 100644 --- a/src/angular/components/send/send.component.ts +++ b/src/angular/components/send/send.component.ts @@ -100,13 +100,13 @@ export class SendComponent implements OnInit { clearTimeout(this.searchTimeout); } if (timeout == null) { - this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s)); + this.filteredSends = this.sends.filter(s => this.filter == null || this.filter(s)); this.applyTextSearch(); return; } this.searchPending = true; this.searchTimeout = setTimeout(async () => { - this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s)); + this.filteredSends = this.sends.filter(s => this.filter == null || this.filter(s)); this.applyTextSearch(); this.searchPending = false; }, timeout); @@ -189,7 +189,7 @@ export class SendComponent implements OnInit { selectType(type: SendType) { this.clearSelections(); this.selectedType = type; - this.applyFilter((s) => s.type === type); + this.applyFilter(s => s.type === type); } clearSelections() { diff --git a/src/angular/components/set-password.component.ts b/src/angular/components/set-password.component.ts index 6c29be00b7..5eeabd02f0 100644 --- a/src/angular/components/set-password.component.ts +++ b/src/angular/components/set-password.component.ts @@ -44,7 +44,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { await this.syncService.fullSync(true); this.syncLoading = false; - const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => { + const queryParamsSub = this.route.queryParams.subscribe(async qParams => { if (qParams.identifier != null) { this.identifier = qParams.identifier; } diff --git a/src/angular/components/share.component.ts b/src/angular/components/share.component.ts index 3cfc5d8eb5..7c9b428296 100644 --- a/src/angular/components/share.component.ts +++ b/src/angular/components/share.component.ts @@ -43,10 +43,10 @@ export class ShareComponent implements OnInit { async load() { const allCollections = await this.collectionService.getAllDecrypted(); - this.writeableCollections = allCollections.map((c) => c).filter((c) => !c.readOnly); + this.writeableCollections = allCollections.map(c => c).filter(c => !c.readOnly); const orgs = await this.userService.getAllOrganizations(); this.organizations = orgs.sort(Utils.getSortFunction(this.i18nService, 'name')) - .filter((o) => o.enabled && o.status === OrganizationUserStatusType.Confirmed); + .filter(o => o.enabled && o.status === OrganizationUserStatusType.Confirmed); const cipherDomain = await this.cipherService.get(this.cipherId); this.cipher = await cipherDomain.decrypt(); @@ -57,18 +57,18 @@ export class ShareComponent implements OnInit { } filterCollections() { - this.writeableCollections.forEach((c) => (c as any).checked = false); + this.writeableCollections.forEach(c => (c as any).checked = false); if (this.organizationId == null || this.writeableCollections.length === 0) { this.collections = []; } else { - this.collections = this.writeableCollections.filter((c) => c.organizationId === this.organizationId); + this.collections = this.writeableCollections.filter(c => c.organizationId === this.organizationId); } } async submit(): Promise { const selectedCollectionIds = this.collections - .filter((c) => !!(c as any).checked) - .map((c) => c.id); + .filter(c => !!(c as any).checked) + .map(c => c.id); if (selectedCollectionIds.length === 0) { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('selectOneCollection')); diff --git a/src/angular/components/sso.component.ts b/src/angular/components/sso.component.ts index 655be7db7f..7fbdb37211 100644 --- a/src/angular/components/sso.component.ts +++ b/src/angular/components/sso.component.ts @@ -45,7 +45,7 @@ export class SsoComponent { protected passwordGenerationService: PasswordGenerationService) { } async ngOnInit() { - const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => { + const queryParamsSub = this.route.queryParams.subscribe(async qParams => { if (qParams.code != null && qParams.state != null) { const codeVerifier = await this.storageService.get(ConstantsService.ssoCodeVerifierKey); const state = await this.storageService.get(ConstantsService.ssoStateKey); diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 3459b96934..440f5efb6e 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -63,7 +63,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy { return; } - const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => { + const queryParamsSub = this.route.queryParams.subscribe(async qParams => { if (qParams.identifier != null) { this.identifier = qParams.identifier; } diff --git a/src/angular/directives/box-row.directive.ts b/src/angular/directives/box-row.directive.ts index 9f2cb60368..a7dd0bee50 100644 --- a/src/angular/directives/box-row.directive.ts +++ b/src/angular/directives/box-row.directive.ts @@ -18,7 +18,7 @@ export class BoxRowDirective implements OnInit { ngOnInit(): void { this.formEls = Array.from(this.el.querySelectorAll('input:not([type="hidden"]), select, textarea')); - this.formEls.forEach((formEl) => { + this.formEls.forEach(formEl => { formEl.addEventListener('focus', (event: Event) => { this.el.classList.add('active'); }, false); diff --git a/src/angular/pipes/search-ciphers.pipe.ts b/src/angular/pipes/search-ciphers.pipe.ts index 407011e911..480a272231 100644 --- a/src/angular/pipes/search-ciphers.pipe.ts +++ b/src/angular/pipes/search-ciphers.pipe.ts @@ -15,13 +15,13 @@ export class SearchCiphersPipe implements PipeTransform { } if (searchText == null || searchText.length < 2) { - return ciphers.filter((c) => { + return ciphers.filter(c => { return deleted !== c.isDeleted; }); } searchText = searchText.trim().toLowerCase(); - return ciphers.filter((c) => { + return ciphers.filter(c => { if (deleted !== c.isDeleted) { return false; } diff --git a/src/angular/pipes/search.pipe.ts b/src/angular/pipes/search.pipe.ts index adc6da0daa..6c9e140b5a 100644 --- a/src/angular/pipes/search.pipe.ts +++ b/src/angular/pipes/search.pipe.ts @@ -17,7 +17,7 @@ export class SearchPipe implements PipeTransform { } searchText = searchText.trim().toLowerCase(); - return items.filter((i) => { + return items.filter(i => { if (prop1 != null && i[prop1] != null && i[prop1].toString().toLowerCase().indexOf(searchText) > -1) { return true; } diff --git a/src/cli/commands/login.command.ts b/src/cli/commands/login.command.ts index 46ac1fb2cc..107a523029 100644 --- a/src/cli/commands/login.command.ts +++ b/src/cli/commands/login.command.ts @@ -175,7 +175,7 @@ export class LoginCommand { if (twoFactorMethod != null) { try { - selectedProvider = twoFactorProviders.filter((p) => p.type === twoFactorMethod)[0]; + selectedProvider = twoFactorProviders.filter(p => p.type === twoFactorMethod)[0]; } catch (e) { return Response.error('Invalid two-step login method.'); } @@ -185,7 +185,7 @@ export class LoginCommand { if (twoFactorProviders.length === 1) { selectedProvider = twoFactorProviders[0]; } else if (this.canInteract) { - const twoFactorOptions = twoFactorProviders.map((p) => p.name); + const twoFactorOptions = twoFactorProviders.map(p => p.name); twoFactorOptions.push(new inquirer.Separator()); twoFactorOptions.push('Cancel'); const answer: inquirer.Answers = diff --git a/src/cli/models/response.ts b/src/cli/models/response.ts index 24ca7d3c36..8e3dc5819f 100644 --- a/src/cli/models/response.ts +++ b/src/cli/models/response.ts @@ -24,7 +24,7 @@ export class Response { static multipleResults(ids: string[]): Response { let msg = 'More than one result was found. Try getting a specific object by `id` instead. ' + 'The following objects were found:'; - ids.forEach((id) => { + ids.forEach(id => { msg += '\n' + id; }); return Response.error(msg, ids); diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index 0ea3f2188a..0a43100b9c 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -118,9 +118,9 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { remote.dialog.showSaveDialog(remote.getCurrentWindow(), { defaultPath: fileName, showsTagField: false, - }).then((ret) => { + }).then(ret => { if (ret.filePath != null) { - fs.writeFile(ret.filePath, Buffer.from(blobData), (err) => { + fs.writeFile(ret.filePath, Buffer.from(blobData), err => { // error check? }); } @@ -213,7 +213,7 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { } authenticateBiometric(): Promise { - return new Promise((resolve) => { + return new Promise(resolve => { const val = ipcRenderer.sendSync('biometric', { action: 'authenticate', }); diff --git a/src/electron/updater.main.ts b/src/electron/updater.main.ts index f09ca46847..bdf8cd9dab 100644 --- a/src/electron/updater.main.ts +++ b/src/electron/updater.main.ts @@ -89,7 +89,7 @@ export class UpdaterMain { this.reset(); }); - autoUpdater.on('update-downloaded', async (info) => { + autoUpdater.on('update-downloaded', async info => { if (this.onUpdateDownloaded != null) { this.onUpdateDownloaded(); } @@ -114,7 +114,7 @@ export class UpdaterMain { } }); - autoUpdater.on('error', (error) => { + autoUpdater.on('error', error => { if (this.doingUpdateCheckWithFeedback) { dialog.showErrorBox(this.i18nService.t('updateError'), error == null ? this.i18nService.t('unknown') : (error.stack || error).toString()); diff --git a/src/importers/ascendoCsvImporter.ts b/src/importers/ascendoCsvImporter.ts index f729c0f6d9..c8179dc62a 100644 --- a/src/importers/ascendoCsvImporter.ts +++ b/src/importers/ascendoCsvImporter.ts @@ -12,7 +12,7 @@ export class AscendoCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { if (value.length < 2) { return; } diff --git a/src/importers/avastCsvImporter.ts b/src/importers/avastCsvImporter.ts index 8e12a05f9c..5519b67aca 100644 --- a/src/importers/avastCsvImporter.ts +++ b/src/importers/avastCsvImporter.ts @@ -12,7 +12,7 @@ export class AvastCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(value.name); cipher.login.uris = this.makeUriArray(value.web); diff --git a/src/importers/aviraCsvImporter.ts b/src/importers/aviraCsvImporter.ts index 2586aa63be..1547d7f279 100644 --- a/src/importers/aviraCsvImporter.ts +++ b/src/importers/aviraCsvImporter.ts @@ -12,7 +12,7 @@ export class AviraCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(value.name, this.getValueOrDefault(this.nameFromUrl(value.website), '--')); diff --git a/src/importers/baseImporter.ts b/src/importers/baseImporter.ts index 5fb43f3fef..366f97a651 100644 --- a/src/importers/baseImporter.ts +++ b/src/importers/baseImporter.ts @@ -91,7 +91,7 @@ export abstract class BaseImporter { data = this.splitNewLine(data).join('\n').trim(); const result = papa.parse(data, parseOptions); if (result.errors != null && result.errors.length > 0) { - result.errors.forEach((e) => { + result.errors.forEach(e => { if (e.row != null) { // tslint:disable-next-line this.logService.warning('Error parsing row ' + e.row + ': ' + e.message); @@ -129,7 +129,7 @@ export abstract class BaseImporter { if (uri.length > 0) { const returnArr: LoginUriView[] = []; - uri.forEach((u) => { + uri.forEach(u => { const loginUri = new LoginUriView(); loginUri.uri = this.fixUri(u); if (this.isNullOrWhitespace(loginUri.uri)) { @@ -265,8 +265,8 @@ export abstract class BaseImporter { } protected moveFoldersToCollections(result: ImportResult) { - result.folderRelationships.forEach((r) => result.collectionRelationships.push(r)); - result.collections = result.folders.map((f) => { + result.folderRelationships.forEach(r => result.collectionRelationships.push(r)); + result.collections = result.folders.map(f => { const collection = new CollectionView(); collection.name = f.name; return collection; @@ -281,7 +281,7 @@ export abstract class BaseImporter { } protected querySelectorAllDirectChild(parentEl: Element, query: string) { - return Array.from(parentEl.querySelectorAll(query)).filter((el) => el.parentNode === parentEl); + return Array.from(parentEl.querySelectorAll(query)).filter(el => el.parentNode === parentEl); } protected initLoginCipher() { diff --git a/src/importers/bitwardenCsvImporter.ts b/src/importers/bitwardenCsvImporter.ts index ab2bc0d83d..65a123d37b 100644 --- a/src/importers/bitwardenCsvImporter.ts +++ b/src/importers/bitwardenCsvImporter.ts @@ -23,10 +23,10 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { if (this.organization && !this.isNullOrWhitespace(value.collections)) { const collections = (value.collections as string).split(','); - collections.forEach((col) => { + collections.forEach(col => { let addCollection = true; let collectionIndex = result.collections.length; diff --git a/src/importers/bitwardenJsonImporter.ts b/src/importers/bitwardenJsonImporter.ts index 25f72abe3b..3c37d2867d 100644 --- a/src/importers/bitwardenJsonImporter.ts +++ b/src/importers/bitwardenJsonImporter.ts @@ -71,7 +71,7 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { if (!this.organization && c.folderId != null && groupingsMap.has(c.folderId)) { this.result.folderRelationships.push([this.result.ciphers.length, groupingsMap.get(c.folderId)]); } else if (this.organization && c.collectionIds != null) { - c.collectionIds.forEach((cId) => { + c.collectionIds.forEach(cId => { if (groupingsMap.has(cId)) { this.result.collectionRelationships.push([this.result.ciphers.length, groupingsMap.get(cId)]); } @@ -123,7 +123,7 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { if (!this.organization && c.folderId != null && groupingsMap.has(c.folderId)) { this.result.folderRelationships.push([this.result.ciphers.length, groupingsMap.get(c.folderId)]); } else if (this.organization && c.collectionIds != null) { - c.collectionIds.forEach((cId) => { + c.collectionIds.forEach(cId => { if (groupingsMap.has(cId)) { this.result.collectionRelationships.push([this.result.ciphers.length, groupingsMap.get(cId)]); } diff --git a/src/importers/blackBerryCsvImporter.ts b/src/importers/blackBerryCsvImporter.ts index 5d26bcc9a6..1e1113487d 100644 --- a/src/importers/blackBerryCsvImporter.ts +++ b/src/importers/blackBerryCsvImporter.ts @@ -12,7 +12,7 @@ export class BlackBerryCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { if (value.grouping === 'list') { return; } diff --git a/src/importers/blurCsvImporter.ts b/src/importers/blurCsvImporter.ts index 6a6001b4f7..3f36e166e0 100644 --- a/src/importers/blurCsvImporter.ts +++ b/src/importers/blurCsvImporter.ts @@ -12,7 +12,7 @@ export class BlurCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { if (value.label === 'null') { value.label = null; } diff --git a/src/importers/buttercupCsvImporter.ts b/src/importers/buttercupCsvImporter.ts index 344f7c0895..bd9e5553ab 100644 --- a/src/importers/buttercupCsvImporter.ts +++ b/src/importers/buttercupCsvImporter.ts @@ -16,7 +16,7 @@ export class ButtercupCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { this.processFolder(result, this.getValueOrDefault(value['!group_name'])); const cipher = this.initLoginCipher(); diff --git a/src/importers/chromeCsvImporter.ts b/src/importers/chromeCsvImporter.ts index 8f4a77822b..3580ec5c29 100644 --- a/src/importers/chromeCsvImporter.ts +++ b/src/importers/chromeCsvImporter.ts @@ -12,7 +12,7 @@ export class ChromeCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(value.name, '--'); cipher.login.username = this.getValueOrDefault(value.username); diff --git a/src/importers/codebookCsvImporter.ts b/src/importers/codebookCsvImporter.ts index 96e56ef481..fdc563d262 100644 --- a/src/importers/codebookCsvImporter.ts +++ b/src/importers/codebookCsvImporter.ts @@ -12,7 +12,7 @@ export class CodebookCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { this.processFolder(result, this.getValueOrDefault(value.Category)); const cipher = this.initLoginCipher(); diff --git a/src/importers/encryptrCsvImporter.ts b/src/importers/encryptrCsvImporter.ts index 3deb155541..adad9aa768 100644 --- a/src/importers/encryptrCsvImporter.ts +++ b/src/importers/encryptrCsvImporter.ts @@ -16,7 +16,7 @@ export class EncryptrCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(value.Label, '--'); cipher.notes = this.getValueOrDefault(value.Notes); diff --git a/src/importers/enpassCsvImporter.ts b/src/importers/enpassCsvImporter.ts index 8543ae2f6e..0b421d9687 100644 --- a/src/importers/enpassCsvImporter.ts +++ b/src/importers/enpassCsvImporter.ts @@ -19,7 +19,7 @@ export class EnpassCsvImporter extends BaseImporter implements Importer { } let firstRow = true; - results.forEach((value) => { + results.forEach(value => { if (value.length < 2 || (firstRow && (value[0] === 'Title' || value[0] === 'title'))) { firstRow = false; return; @@ -106,7 +106,7 @@ export class EnpassCsvImporter extends BaseImporter implements Importer { if (fields == null || name == null) { return false; } - return fields.filter((f) => !this.isNullOrWhitespace(f) && + return fields.filter(f => !this.isNullOrWhitespace(f) && f.toLowerCase() === name.toLowerCase()).length > 0; } } diff --git a/src/importers/firefoxCsvImporter.ts b/src/importers/firefoxCsvImporter.ts index 87dd13c35a..fb41cc29a8 100644 --- a/src/importers/firefoxCsvImporter.ts +++ b/src/importers/firefoxCsvImporter.ts @@ -12,7 +12,7 @@ export class FirefoxCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { const cipher = this.initLoginCipher(); const url = this.getValueOrDefault(value.url, this.getValueOrDefault(value.hostname)); cipher.name = this.getValueOrDefault(this.nameFromUrl(url), '--'); diff --git a/src/importers/kasperskyTxtImporter.ts b/src/importers/kasperskyTxtImporter.ts index d01f4ba296..efaf92286d 100644 --- a/src/importers/kasperskyTxtImporter.ts +++ b/src/importers/kasperskyTxtImporter.ts @@ -43,7 +43,7 @@ export class KasperskyTxtImporter extends BaseImporter implements Importer { const applications = this.parseDataCategory(applicationsData); const websites = this.parseDataCategory(websitesData); - notes.forEach((n) => { + notes.forEach(n => { const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(n.get('Name')); cipher.notes = this.getValueOrDefault(n.get('Text')); @@ -51,7 +51,7 @@ export class KasperskyTxtImporter extends BaseImporter implements Importer { result.ciphers.push(cipher); }); - websites.concat(applications).forEach((w) => { + websites.concat(applications).forEach(w => { const cipher = this.initLoginCipher(); const nameKey = w.has('Website name') ? 'Website name' : 'Application'; cipher.name = this.getValueOrDefault(w.get(nameKey), ''); @@ -80,14 +80,14 @@ export class KasperskyTxtImporter extends BaseImporter implements Importer { return []; } const items: Map[] = []; - data.split(Delimiter).forEach((p) => { + data.split(Delimiter).forEach(p => { if (p.indexOf('\n') === -1) { return; } const item = new Map(); let itemComment: string; let itemCommentKey: string; - p.split('\n').forEach((l) => { + p.split('\n').forEach(l => { if (itemComment != null) { itemComment += ('\n' + l); return; diff --git a/src/importers/keepass2XmlImporter.ts b/src/importers/keepass2XmlImporter.ts index 604a6f7d26..c91a925f50 100644 --- a/src/importers/keepass2XmlImporter.ts +++ b/src/importers/keepass2XmlImporter.ts @@ -49,11 +49,11 @@ export class KeePass2XmlImporter extends BaseImporter implements Importer { this.result.folders.push(folder); } - this.querySelectorAllDirectChild(node, 'Entry').forEach((entry) => { + this.querySelectorAllDirectChild(node, 'Entry').forEach(entry => { const cipherIndex = this.result.ciphers.length; const cipher = this.initLoginCipher(); - this.querySelectorAllDirectChild(entry, 'String').forEach((entryString) => { + this.querySelectorAllDirectChild(entry, 'String').forEach(entryString => { const valueEl = this.querySelectorDirectChild(entryString, 'Value'); const value = valueEl != null ? valueEl.textContent : null; if (this.isNullOrWhitespace(value)) { @@ -93,7 +93,7 @@ export class KeePass2XmlImporter extends BaseImporter implements Importer { } }); - this.querySelectorAllDirectChild(node, 'Group').forEach((group) => { + this.querySelectorAllDirectChild(node, 'Group').forEach(group => { this.traverse(group, false, groupName); }); } diff --git a/src/importers/keepassxCsvImporter.ts b/src/importers/keepassxCsvImporter.ts index 69a3d2144e..6845c8e8cf 100644 --- a/src/importers/keepassxCsvImporter.ts +++ b/src/importers/keepassxCsvImporter.ts @@ -12,7 +12,7 @@ export class KeePassXCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { if (this.isNullOrWhitespace(value.Title)) { return; } diff --git a/src/importers/keeperCsvImporter.ts b/src/importers/keeperCsvImporter.ts index 68bfd79dbc..e10b8f3343 100644 --- a/src/importers/keeperCsvImporter.ts +++ b/src/importers/keeperCsvImporter.ts @@ -14,7 +14,7 @@ export class KeeperCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { if (value.length < 6) { return; } diff --git a/src/importers/lastpassCsvImporter.ts b/src/importers/lastpassCsvImporter.ts index 63c76bffce..5abee25fe2 100644 --- a/src/importers/lastpassCsvImporter.ts +++ b/src/importers/lastpassCsvImporter.ts @@ -233,7 +233,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { const dataObj: any = {}; let processingNotes = false; - extraParts.forEach((extraPart) => { + extraParts.forEach(extraPart => { let key: string = null; let val: string = null; if (!processingNotes) { diff --git a/src/importers/logMeOnceCsvImporter.ts b/src/importers/logMeOnceCsvImporter.ts index 8e3542bae5..f6e3c5dbe8 100644 --- a/src/importers/logMeOnceCsvImporter.ts +++ b/src/importers/logMeOnceCsvImporter.ts @@ -12,7 +12,7 @@ export class LogMeOnceCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { if (value.length < 4) { return; } diff --git a/src/importers/meldiumCsvImporter.ts b/src/importers/meldiumCsvImporter.ts index 98117758b9..0b7ed11d1a 100644 --- a/src/importers/meldiumCsvImporter.ts +++ b/src/importers/meldiumCsvImporter.ts @@ -12,7 +12,7 @@ export class MeldiumCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(value.DisplayName, '--'); cipher.notes = this.getValueOrDefault(value.Notes); diff --git a/src/importers/msecureCsvImporter.ts b/src/importers/msecureCsvImporter.ts index e37a76c53e..248b53801a 100644 --- a/src/importers/msecureCsvImporter.ts +++ b/src/importers/msecureCsvImporter.ts @@ -17,7 +17,7 @@ export class MSecureCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { if (value.length < 3) { return; } diff --git a/src/importers/mykiCsvImporter.ts b/src/importers/mykiCsvImporter.ts index 365cb2cfef..e206ae1a03 100644 --- a/src/importers/mykiCsvImporter.ts +++ b/src/importers/mykiCsvImporter.ts @@ -19,7 +19,7 @@ export class MykiCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(value.nickname, '--'); cipher.notes = this.getValueOrDefault(value.additionalInfo); diff --git a/src/importers/onepasswordImporters/onepassword1PifImporter.ts b/src/importers/onepasswordImporters/onepassword1PifImporter.ts index 66f5e77d9b..e82f31852e 100644 --- a/src/importers/onepasswordImporters/onepassword1PifImporter.ts +++ b/src/importers/onepasswordImporters/onepassword1PifImporter.ts @@ -17,7 +17,7 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { result = new ImportResult(); parse(data: string): Promise { - data.split(this.newLineRegex).forEach((line) => { + data.split(this.newLineRegex).forEach(line => { if (this.isNullOrWhitespace(line) || line[0] !== '{') { return; } @@ -237,7 +237,7 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { const fieldName = this.isNullOrWhitespace(field[nameKey]) ? 'no_name' : field[nameKey]; if (fieldName === 'password' && cipher.passwordHistory != null && - cipher.passwordHistory.some((h) => h.password === fieldValue)) { + cipher.passwordHistory.some(h => h.password === fieldValue)) { return; } diff --git a/src/importers/onepasswordImporters/onepasswordCsvImporter.ts b/src/importers/onepasswordImporters/onepasswordCsvImporter.ts index 87ee36e54b..b16bebe074 100644 --- a/src/importers/onepasswordImporters/onepasswordCsvImporter.ts +++ b/src/importers/onepasswordImporters/onepasswordCsvImporter.ts @@ -27,7 +27,7 @@ export abstract class OnePasswordCsvImporter extends BaseImporter implements Imp return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { if (this.isNullOrWhitespace(this.getProp(value, 'title'))) { return; } diff --git a/src/importers/padlockCsvImporter.ts b/src/importers/padlockCsvImporter.ts index 8ec5e8228d..e106af3e49 100644 --- a/src/importers/padlockCsvImporter.ts +++ b/src/importers/padlockCsvImporter.ts @@ -16,7 +16,7 @@ export class PadlockCsvImporter extends BaseImporter implements Importer { } let headers: string[] = null; - results.forEach((value) => { + results.forEach(value => { if (headers == null) { headers = value.map((v: string) => v); return; @@ -29,7 +29,7 @@ export class PadlockCsvImporter extends BaseImporter implements Importer { if (!this.isNullOrWhitespace(value[1])) { if (this.organization) { const tags = (value[1] as string).split(','); - tags.forEach((tag) => { + tags.forEach(tag => { tag = tag.trim(); let addCollection = true; let collectionIndex = result.collections.length; diff --git a/src/importers/passkeepCsvImporter.ts b/src/importers/passkeepCsvImporter.ts index e1c2873c02..a010550c32 100644 --- a/src/importers/passkeepCsvImporter.ts +++ b/src/importers/passkeepCsvImporter.ts @@ -12,7 +12,7 @@ export class PassKeepCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { this.processFolder(result, this.getValue('category', value)); const cipher = this.initLoginCipher(); cipher.notes = this.getValue('description', value); diff --git a/src/importers/passpackCsvImporter.ts b/src/importers/passpackCsvImporter.ts index 3fcdf4120e..d2450b9058 100644 --- a/src/importers/passpackCsvImporter.ts +++ b/src/importers/passpackCsvImporter.ts @@ -14,7 +14,7 @@ export class PasspackCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { const tagsJson = !this.isNullOrWhitespace(value.Tags) ? JSON.parse(value.Tags) : null; const tags: string[] = tagsJson != null && tagsJson.tags != null && tagsJson.tags.length > 0 ? tagsJson.tags.map((tagJson: string) => { @@ -26,7 +26,7 @@ export class PasspackCsvImporter extends BaseImporter implements Importer { }).filter((t: string) => !this.isNullOrWhitespace(t)) : null; if (this.organization && tags != null && tags.length > 0) { - tags.forEach((tag) => { + tags.forEach(tag => { let addCollection = true; let collectionIndex = result.collections.length; diff --git a/src/importers/passwordAgentCsvImporter.ts b/src/importers/passwordAgentCsvImporter.ts index d14e8036cc..b505c633b0 100644 --- a/src/importers/passwordAgentCsvImporter.ts +++ b/src/importers/passwordAgentCsvImporter.ts @@ -13,7 +13,7 @@ export class PasswordAgentCsvImporter extends BaseImporter implements Importer { } let newVersion = true; - results.forEach((value) => { + results.forEach(value => { if (value.length !== 5 && value.length < 9) { return; } diff --git a/src/importers/passwordDragonXmlImporter.ts b/src/importers/passwordDragonXmlImporter.ts index 9195fca5ba..15110b8243 100644 --- a/src/importers/passwordDragonXmlImporter.ts +++ b/src/importers/passwordDragonXmlImporter.ts @@ -13,7 +13,7 @@ export class PasswordDragonXmlImporter extends BaseImporter implements Importer } const records = doc.querySelectorAll('PasswordManager > record'); - Array.from(records).forEach((record) => { + Array.from(records).forEach(record => { const category = this.querySelectorDirectChild(record, 'Category'); const categoryText = category != null && !this.isNullOrWhitespace(category.textContent) && category.textContent !== 'Unfiled' ? category.textContent : null; @@ -36,7 +36,7 @@ export class PasswordDragonXmlImporter extends BaseImporter implements Importer attributes.push('Attribute-' + i); } - this.querySelectorAllDirectChild(record, attributes.join(',')).forEach((attr) => { + this.querySelectorAllDirectChild(record, attributes.join(',')).forEach(attr => { if (this.isNullOrWhitespace(attr.textContent) || attr.textContent === 'null') { return; } diff --git a/src/importers/passwordSafeXmlImporter.ts b/src/importers/passwordSafeXmlImporter.ts index e105e186e8..e8e607311c 100644 --- a/src/importers/passwordSafeXmlImporter.ts +++ b/src/importers/passwordSafeXmlImporter.ts @@ -21,7 +21,7 @@ export class PasswordSafeXmlImporter extends BaseImporter implements Importer { const notesDelimiter = passwordSafe.getAttribute('delimiter'); const entries = doc.querySelectorAll('passwordsafe > entry'); - Array.from(entries).forEach((entry) => { + Array.from(entries).forEach(entry => { const group = this.querySelectorDirectChild(entry, 'group'); const groupText = group != null && !this.isNullOrWhitespace(group.textContent) ? group.textContent.split('.').join('/') : null; diff --git a/src/importers/passwordWalletTxtImporter.ts b/src/importers/passwordWalletTxtImporter.ts index 561085aa04..8a27956260 100644 --- a/src/importers/passwordWalletTxtImporter.ts +++ b/src/importers/passwordWalletTxtImporter.ts @@ -12,7 +12,7 @@ export class PasswordWalletTxtImporter extends BaseImporter implements Importer return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { if (value.length < 1) { return; } diff --git a/src/importers/rememBearCsvImporter.ts b/src/importers/rememBearCsvImporter.ts index bc9aaeb945..c12788b7c2 100644 --- a/src/importers/rememBearCsvImporter.ts +++ b/src/importers/rememBearCsvImporter.ts @@ -16,7 +16,7 @@ export class RememBearCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { if (value.trash === 'true') { return; } diff --git a/src/importers/roboformCsvImporter.ts b/src/importers/roboformCsvImporter.ts index 99c763665c..cb0c0de8b1 100644 --- a/src/importers/roboformCsvImporter.ts +++ b/src/importers/roboformCsvImporter.ts @@ -13,7 +13,7 @@ export class RoboFormCsvImporter extends BaseImporter implements Importer { } let i = 1; - results.forEach((value) => { + results.forEach(value => { const folder = !this.isNullOrWhitespace(value.Folder) && value.Folder.startsWith('/') ? value.Folder.replace('/', '') : value.Folder; const folderName = !this.isNullOrWhitespace(folder) ? folder : null; diff --git a/src/importers/safeInCloudXmlImporter.ts b/src/importers/safeInCloudXmlImporter.ts index ea22fe2aa4..f1a9569250 100644 --- a/src/importers/safeInCloudXmlImporter.ts +++ b/src/importers/safeInCloudXmlImporter.ts @@ -27,7 +27,7 @@ export class SafeInCloudXmlImporter extends BaseImporter implements Importer { const foldersMap = new Map(); - Array.from(doc.querySelectorAll('database > label')).forEach((labelEl) => { + Array.from(doc.querySelectorAll('database > label')).forEach(labelEl => { const name = labelEl.getAttribute('name'); const id = labelEl.getAttribute('id'); if (!this.isNullOrWhitespace(name) && !this.isNullOrWhitespace(id)) { @@ -38,7 +38,7 @@ export class SafeInCloudXmlImporter extends BaseImporter implements Importer { } }); - Array.from(doc.querySelectorAll('database > card')).forEach((cardEl) => { + Array.from(doc.querySelectorAll('database > card')).forEach(cardEl => { if (cardEl.getAttribute('template') === 'true') { return; } @@ -60,7 +60,7 @@ export class SafeInCloudXmlImporter extends BaseImporter implements Importer { cipher.secureNote = new SecureNoteView(); cipher.secureNote.type = SecureNoteType.Generic; } else { - Array.from(this.querySelectorAllDirectChild(cardEl, 'field')).forEach((fieldEl) => { + Array.from(this.querySelectorAllDirectChild(cardEl, 'field')).forEach(fieldEl => { const text = fieldEl.textContent; if (this.isNullOrWhitespace(text)) { return; @@ -83,7 +83,7 @@ export class SafeInCloudXmlImporter extends BaseImporter implements Importer { }); } - Array.from(this.querySelectorAllDirectChild(cardEl, 'notes')).forEach((notesEl) => { + Array.from(this.querySelectorAllDirectChild(cardEl, 'notes')).forEach(notesEl => { cipher.notes += (notesEl.textContent + '\n'); }); diff --git a/src/importers/saferpassCsvImport.ts b/src/importers/saferpassCsvImport.ts index 3fdad40b8a..1273e74120 100644 --- a/src/importers/saferpassCsvImport.ts +++ b/src/importers/saferpassCsvImport.ts @@ -12,7 +12,7 @@ export class SaferPassCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(this.nameFromUrl(value.url), '--'); cipher.notes = this.getValueOrDefault(value.notes); diff --git a/src/importers/secureSafeCsvImporter.ts b/src/importers/secureSafeCsvImporter.ts index 068c70098e..fc15efec96 100644 --- a/src/importers/secureSafeCsvImporter.ts +++ b/src/importers/secureSafeCsvImporter.ts @@ -12,7 +12,7 @@ export class SecureSafeCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(value.Title); cipher.notes = this.getValueOrDefault(value.Comment); diff --git a/src/importers/splashIdCsvImporter.ts b/src/importers/splashIdCsvImporter.ts index 9210824c0a..4e895d55c3 100644 --- a/src/importers/splashIdCsvImporter.ts +++ b/src/importers/splashIdCsvImporter.ts @@ -13,7 +13,7 @@ export class SplashIdCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { if (value.length < 3) { return; } diff --git a/src/importers/stickyPasswordXmlImporter.ts b/src/importers/stickyPasswordXmlImporter.ts index 3955099255..313ab1fcd1 100644 --- a/src/importers/stickyPasswordXmlImporter.ts +++ b/src/importers/stickyPasswordXmlImporter.ts @@ -13,7 +13,7 @@ export class StickyPasswordXmlImporter extends BaseImporter implements Importer } const loginNodes = doc.querySelectorAll('root > Database > Logins > Login'); - Array.from(loginNodes).forEach((loginNode) => { + Array.from(loginNodes).forEach(loginNode => { const accountId = loginNode.getAttribute('ID'); if (this.isNullOrWhitespace(accountId)) { return; diff --git a/src/importers/truekeyCsvImporter.ts b/src/importers/truekeyCsvImporter.ts index a7d8f8f9c9..52f43da81f 100644 --- a/src/importers/truekeyCsvImporter.ts +++ b/src/importers/truekeyCsvImporter.ts @@ -22,7 +22,7 @@ export class TrueKeyCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { const cipher = this.initLoginCipher(); cipher.favorite = this.getValueOrDefault(value.favorite, '').toLowerCase() === 'true'; cipher.name = this.getValueOrDefault(value.name, '--'); diff --git a/src/importers/upmCsvImporter.ts b/src/importers/upmCsvImporter.ts index 40cb814b79..23e4ece3e7 100644 --- a/src/importers/upmCsvImporter.ts +++ b/src/importers/upmCsvImporter.ts @@ -12,7 +12,7 @@ export class UpmCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { if (value.length !== 5) { return; } diff --git a/src/importers/yotiCsvImporter.ts b/src/importers/yotiCsvImporter.ts index abfe0d6c63..56a80d5df7 100644 --- a/src/importers/yotiCsvImporter.ts +++ b/src/importers/yotiCsvImporter.ts @@ -12,7 +12,7 @@ export class YotiCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { const cipher = this.initLoginCipher(); cipher.name = this.getValueOrDefault(value.Name, '--'); cipher.login.username = this.getValueOrDefault(value['User name']); diff --git a/src/importers/zohoVaultCsvImporter.ts b/src/importers/zohoVaultCsvImporter.ts index 4b91e4f5a7..4ff99f2804 100644 --- a/src/importers/zohoVaultCsvImporter.ts +++ b/src/importers/zohoVaultCsvImporter.ts @@ -13,7 +13,7 @@ export class ZohoVaultCsvImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - results.forEach((value) => { + results.forEach(value => { if (this.isNullOrWhitespace(value['Password Name']) && this.isNullOrWhitespace(value['Secret Name'])) { return; } @@ -45,7 +45,7 @@ export class ZohoVaultCsvImporter extends BaseImporter implements Importer { return; } const dataLines = this.splitNewLine(data); - dataLines.forEach((line) => { + dataLines.forEach(line => { const delimPosition = line.indexOf(':'); if (delimPosition < 0) { return; diff --git a/src/misc/analytics.ts b/src/misc/analytics.ts index b6d0808cb9..970e4dee73 100644 --- a/src/misc/analytics.ts +++ b/src/misc/analytics.ts @@ -109,7 +109,7 @@ export class Analytics { } const pathParts = pagePath.split('/'); const newPathParts: string[] = []; - pathParts.forEach((p) => { + pathParts.forEach(p => { if (p.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i)) { newPathParts.push('__guid__'); } else { diff --git a/src/misc/nodeUtils.ts b/src/misc/nodeUtils.ts index e1567e26ba..d3864fe56d 100644 --- a/src/misc/nodeUtils.ts +++ b/src/misc/nodeUtils.ts @@ -19,11 +19,11 @@ export class NodeUtils { const readStream = fs.createReadStream(fileName, {encoding: 'utf8'}); const readInterface = readline.createInterface(readStream); readInterface - .on('line', (line) => { + .on('line', line => { readStream.close(); resolve(line); }) - .on('error', (err) => reject(err)); + .on('error', err => reject(err)); }); } diff --git a/src/misc/serviceUtils.ts b/src/misc/serviceUtils.ts index 50ee43eb7d..f8cb5257fe 100644 --- a/src/misc/serviceUtils.ts +++ b/src/misc/serviceUtils.ts @@ -27,7 +27,7 @@ export class ServiceUtils { return; } - if (nodeTree.filter((n) => n.node.name === partName).length === 0) { + if (nodeTree.filter(n => n.node.name === partName).length === 0) { if (end) { nodeTree.push(new TreeNode(obj, partName, parent)); return; diff --git a/src/misc/utils.ts b/src/misc/utils.ts index b512cb925d..91daaa8938 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -159,7 +159,7 @@ export class Utils { // ref: http://stackoverflow.com/a/2117523/1090359 static newGuid(): string { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { // tslint:disable-next-line const r = Math.random() * 16 | 0; // tslint:disable-next-line @@ -242,7 +242,7 @@ export class Utils { } const map = new Map(); const pairs = (url.search[0] === '?' ? url.search.substr(1) : url.search).split('&'); - pairs.forEach((pair) => { + pairs.forEach(pair => { const parts = pair.split('='); if (parts.length < 1) { return; @@ -289,7 +289,7 @@ export class Utils { private static isMobile(win: Window) { let mobile = false; - ((a) => { + (a => { // tslint:disable-next-line if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) { mobile = true; diff --git a/src/models/data/cipherData.ts b/src/models/data/cipherData.ts index fc87ae60d6..487fc5ba6c 100644 --- a/src/models/data/cipherData.ts +++ b/src/models/data/cipherData.ts @@ -72,13 +72,13 @@ export class CipherData { } if (response.fields != null) { - this.fields = response.fields.map((f) => new FieldData(f)); + this.fields = response.fields.map(f => new FieldData(f)); } if (response.attachments != null) { - this.attachments = response.attachments.map((a) => new AttachmentData(a)); + this.attachments = response.attachments.map(a => new AttachmentData(a)); } if (response.passwordHistory != null) { - this.passwordHistory = response.passwordHistory.map((ph) => new PasswordHistoryData(ph)); + this.passwordHistory = response.passwordHistory.map(ph => new PasswordHistoryData(ph)); } } } diff --git a/src/models/data/loginData.ts b/src/models/data/loginData.ts index c173c2f162..90673e5695 100644 --- a/src/models/data/loginData.ts +++ b/src/models/data/loginData.ts @@ -20,7 +20,7 @@ export class LoginData { this.totp = data.totp; if (data.uris) { - this.uris = data.uris.map((u) => new LoginUriData(u)); + this.uris = data.uris.map(u => new LoginUriData(u)); } } } diff --git a/src/models/domain/cipher.ts b/src/models/domain/cipher.ts index ddf78557e1..1a7aaa9032 100644 --- a/src/models/domain/cipher.ts +++ b/src/models/domain/cipher.ts @@ -85,19 +85,19 @@ export class Cipher extends Domain { } if (obj.attachments != null) { - this.attachments = obj.attachments.map((a) => new Attachment(a, alreadyEncrypted)); + this.attachments = obj.attachments.map(a => new Attachment(a, alreadyEncrypted)); } else { this.attachments = null; } if (obj.fields != null) { - this.fields = obj.fields.map((f) => new Field(f, alreadyEncrypted)); + this.fields = obj.fields.map(f => new Field(f, alreadyEncrypted)); } else { this.fields = null; } if (obj.passwordHistory != null) { - this.passwordHistory = obj.passwordHistory.map((ph) => new Password(ph, alreadyEncrypted)); + this.passwordHistory = obj.passwordHistory.map(ph => new Password(ph, alreadyEncrypted)); } else { this.passwordHistory = null; } @@ -135,7 +135,7 @@ export class Cipher extends Domain { await this.attachments.reduce((promise, attachment) => { return promise.then(() => { return attachment.decrypt(orgId, encKey); - }).then((decAttachment) => { + }).then(decAttachment => { attachments.push(decAttachment); }); }, Promise.resolve()); @@ -147,7 +147,7 @@ export class Cipher extends Domain { await this.fields.reduce((promise, field) => { return promise.then(() => { return field.decrypt(orgId, encKey); - }).then((decField) => { + }).then(decField => { fields.push(decField); }); }, Promise.resolve()); @@ -159,7 +159,7 @@ export class Cipher extends Domain { await this.passwordHistory.reduce((promise, ph) => { return promise.then(() => { return ph.decrypt(orgId, encKey); - }).then((decPh) => { + }).then(decPh => { passwordHistory.push(decPh); }); }, Promise.resolve()); @@ -207,13 +207,13 @@ export class Cipher extends Domain { } if (this.fields != null) { - c.fields = this.fields.map((f) => f.toFieldData()); + c.fields = this.fields.map(f => f.toFieldData()); } if (this.attachments != null) { - c.attachments = this.attachments.map((a) => a.toAttachmentData()); + c.attachments = this.attachments.map(a => a.toAttachmentData()); } if (this.passwordHistory != null) { - c.passwordHistory = this.passwordHistory.map((ph) => ph.toPasswordHistoryData()); + c.passwordHistory = this.passwordHistory.map(ph => ph.toPasswordHistoryData()); } return c; } diff --git a/src/models/domain/login.ts b/src/models/domain/login.ts index c8be800447..2dcbeac561 100644 --- a/src/models/domain/login.ts +++ b/src/models/domain/login.ts @@ -30,7 +30,7 @@ export class Login extends Domain { if (obj.uris) { this.uris = []; - obj.uris.forEach((u) => { + obj.uris.forEach(u => { this.uris.push(new LoginUri(u, alreadyEncrypted)); }); } @@ -65,7 +65,7 @@ export class Login extends Domain { if (this.uris != null && this.uris.length > 0) { l.uris = []; - this.uris.forEach((u) => { + this.uris.forEach(u => { l.uris.push(u.toLoginUriData()); }); } diff --git a/src/models/export/cipher.ts b/src/models/export/cipher.ts index d88db8f7a4..abb4438b04 100644 --- a/src/models/export/cipher.ts +++ b/src/models/export/cipher.ts @@ -39,7 +39,7 @@ export class Cipher { view.favorite = req.favorite; if (req.fields != null) { - view.fields = req.fields.map((f) => Field.toView(f)); + view.fields = req.fields.map(f => Field.toView(f)); } switch (req.type) { @@ -71,7 +71,7 @@ export class Cipher { domain.favorite = req.favorite; if (req.fields != null) { - domain.fields = req.fields.map((f) => Field.toDomain(f)); + domain.fields = req.fields.map(f => Field.toDomain(f)); } switch (req.type) { @@ -122,9 +122,9 @@ export class Cipher { if (o.fields != null) { if (o instanceof CipherView) { - this.fields = o.fields.map((f) => new Field(f)); + this.fields = o.fields.map(f => new Field(f)); } else { - this.fields = o.fields.map((f) => new Field(f)); + this.fields = o.fields.map(f => new Field(f)); } } diff --git a/src/models/export/login.ts b/src/models/export/login.ts index 4bf487b6f1..8c275746cd 100644 --- a/src/models/export/login.ts +++ b/src/models/export/login.ts @@ -17,7 +17,7 @@ export class Login { static toView(req: Login, view = new LoginView()) { if (req.uris != null) { - view.uris = req.uris.map((u) => LoginUri.toView(u)); + view.uris = req.uris.map(u => LoginUri.toView(u)); } view.username = req.username; view.password = req.password; @@ -27,7 +27,7 @@ export class Login { static toDomain(req: Login, domain = new LoginDomain()) { if (req.uris != null) { - domain.uris = req.uris.map((u) => LoginUri.toDomain(u)); + domain.uris = req.uris.map(u => LoginUri.toDomain(u)); } domain.username = req.username != null ? new CipherString(req.username) : null; domain.password = req.password != null ? new CipherString(req.password) : null; @@ -47,9 +47,9 @@ export class Login { if (o.uris != null) { if (o instanceof LoginView) { - this.uris = o.uris.map((u) => new LoginUri(u)); + this.uris = o.uris.map(u => new LoginUri(u)); } else { - this.uris = o.uris.map((u) => new LoginUri(u)); + this.uris = o.uris.map(u => new LoginUri(u)); } } diff --git a/src/models/request/cipherBulkShareRequest.ts b/src/models/request/cipherBulkShareRequest.ts index 413212e1ae..5d1e6781ef 100644 --- a/src/models/request/cipherBulkShareRequest.ts +++ b/src/models/request/cipherBulkShareRequest.ts @@ -9,7 +9,7 @@ export class CipherBulkShareRequest { constructor(ciphers: Cipher[], collectionIds: string[]) { if (ciphers != null) { this.ciphers = []; - ciphers.forEach((c) => { + ciphers.forEach(c => { this.ciphers.push(new CipherWithIdRequest(c)); }); } diff --git a/src/models/request/cipherRequest.ts b/src/models/request/cipherRequest.ts index 5c1e950311..cdd0087d9d 100644 --- a/src/models/request/cipherRequest.ts +++ b/src/models/request/cipherRequest.ts @@ -50,7 +50,7 @@ export class CipherRequest { this.login.totp = cipher.login.totp ? cipher.login.totp.encryptedString : null; if (cipher.login.uris != null) { - this.login.uris = cipher.login.uris.map((u) => { + this.login.uris = cipher.login.uris.map(u => { const uri = new LoginUriApi(); uri.uri = u.uri != null ? u.uri.encryptedString : null; uri.match = u.match != null ? u.match : null; @@ -110,7 +110,7 @@ export class CipherRequest { } if (cipher.fields != null) { - this.fields = cipher.fields.map((f) => { + this.fields = cipher.fields.map(f => { const field = new FieldApi(); field.type = f.type; field.name = f.name ? f.name.encryptedString : null; @@ -121,7 +121,7 @@ export class CipherRequest { if (cipher.passwordHistory != null) { this.passwordHistory = []; - cipher.passwordHistory.forEach((ph) => { + cipher.passwordHistory.forEach(ph => { this.passwordHistory.push({ lastUsedDate: ph.lastUsedDate, password: ph.password ? ph.password.encryptedString : null, @@ -132,7 +132,7 @@ export class CipherRequest { if (cipher.attachments != null) { this.attachments = {}; this.attachments2 = {}; - cipher.attachments.forEach((attachment) => { + cipher.attachments.forEach(attachment => { const fileName = attachment.fileName ? attachment.fileName.encryptedString : null; this.attachments[attachment.id] = fileName; const attachmentRequest = new AttachmentRequest(); diff --git a/src/models/view/loginView.ts b/src/models/view/loginView.ts index a891f9d231..24c8f0aca4 100644 --- a/src/models/view/loginView.ts +++ b/src/models/view/loginView.ts @@ -32,7 +32,7 @@ export class LoginView implements View { } get canLaunch(): boolean { - return this.hasUris && this.uris.some((u) => u.canLaunch); + return this.hasUris && this.uris.some(u => u.canLaunch); } get hasTotp(): boolean { @@ -41,7 +41,7 @@ export class LoginView implements View { get launchUri(): string { if (this.hasUris) { - const uri = this.uris.find((u) => u.canLaunch); + const uri = this.uris.find(u => u.canLaunch); if (uri != null) { return uri.launchUri; } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 41908283f3..793762889d 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -1312,7 +1312,7 @@ export class ApiService implements ApiServiceAbstraction { } private qsStringify(params: any): string { - return Object.keys(params).map((key) => { + return Object.keys(params).map(key => { return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); }).join('&'); } diff --git a/src/services/audit.service.ts b/src/services/audit.service.ts index 1282ce2bd3..db9a045730 100644 --- a/src/services/audit.service.ts +++ b/src/services/audit.service.ts @@ -22,7 +22,7 @@ export class AuditService implements AuditServiceAbstraction { const response = await this.apiService.nativeFetch(new Request(PwnedPasswordsApi + hashStart)); const leakedHashes = await response.text(); - const match = leakedHashes.split(/\r?\n/).find((v) => { + const match = leakedHashes.split(/\r?\n/).find(v => { return v.split(':')[0] === hashEnding; }); diff --git a/src/services/broadcaster.service.ts b/src/services/broadcaster.service.ts index beda9c1c55..6dfbfa6454 100644 --- a/src/services/broadcaster.service.ts +++ b/src/services/broadcaster.service.ts @@ -11,7 +11,7 @@ export class BroadcasterService implements BroadcasterServiceAbstraction { return; } - this.subscribers.forEach((value) => { + this.subscribers.forEach(value => { value(message); }); } diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 539321f5db..c004f6dca7 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -113,12 +113,12 @@ export class CipherService implements CipherServiceAbstraction { } } if (existingCipher.hasFields) { - const existingHiddenFields = existingCipher.fields.filter((f) => f.type === FieldType.Hidden && + const existingHiddenFields = existingCipher.fields.filter(f => f.type === FieldType.Hidden && f.name != null && f.name !== '' && f.value != null && f.value !== ''); const hiddenFields = model.fields == null ? [] : - model.fields.filter((f) => f.type === FieldType.Hidden && f.name != null && f.name !== ''); - existingHiddenFields.forEach((ef) => { - const matchedField = hiddenFields.find((f) => f.name === ef.name); + model.fields.filter(f => f.type === FieldType.Hidden && f.name != null && f.name !== ''); + existingHiddenFields.forEach(ef => { + const matchedField = hiddenFields.find(f => f.name === ef.name); if (matchedField == null || matchedField.value !== ef.value) { const ph = new PasswordHistoryView(); ph.password = ef.name + ': ' + ef.value; @@ -157,13 +157,13 @@ export class CipherService implements CipherServiceAbstraction { notes: null, }, key), this.encryptCipherData(cipher, model, key), - this.encryptFields(model.fields, key).then((fields) => { + this.encryptFields(model.fields, key).then(fields => { cipher.fields = fields; }), - this.encryptPasswordHistories(model.passwordHistory, key).then((ph) => { + this.encryptPasswordHistories(model.passwordHistory, key).then(ph => { cipher.passwordHistory = ph; }), - this.encryptAttachments(model.attachments, key).then((attachments) => { + this.encryptAttachments(model.attachments, key).then(attachments => { cipher.attachments = attachments; }), ]); @@ -178,7 +178,7 @@ export class CipherService implements CipherServiceAbstraction { const promises: Promise[] = []; const encAttachments: Attachment[] = []; - attachmentsModel.forEach(async (model) => { + attachmentsModel.forEach(async model => { const attachment = new Attachment(); attachment.id = model.id; attachment.size = model.size; @@ -302,8 +302,8 @@ export class CipherService implements CipherServiceAbstraction { const promises: any[] = []; const ciphers = await this.getAll(); - ciphers.forEach((cipher) => { - promises.push(cipher.decrypt().then((c) => decCiphers.push(c))); + ciphers.forEach(cipher => { + promises.push(cipher.decrypt().then(c => decCiphers.push(c))); }); await Promise.all(promises); @@ -315,7 +315,7 @@ export class CipherService implements CipherServiceAbstraction { async getAllDecryptedForGrouping(groupingId: string, folder: boolean = true): Promise { const ciphers = await this.getAllDecrypted(); - return ciphers.filter((cipher) => { + return ciphers.filter(cipher => { if (cipher.isDeleted) { return false; } @@ -339,7 +339,7 @@ export class CipherService implements CipherServiceAbstraction { const eqDomainsPromise = domain == null ? Promise.resolve([]) : this.settingsService.getEquivalentDomains().then((eqDomains: any[][]) => { let matches: any[] = []; - eqDomains.forEach((eqDomain) => { + eqDomains.forEach(eqDomain => { if (eqDomain.length && eqDomain.indexOf(domain) >= 0) { matches = matches.concat(eqDomain); } @@ -363,7 +363,7 @@ export class CipherService implements CipherServiceAbstraction { } } - return ciphers.filter((cipher) => { + return ciphers.filter(cipher => { if (cipher.deletedDate != null) { return false; } @@ -432,10 +432,10 @@ export class CipherService implements CipherServiceAbstraction { if (ciphers != null && ciphers.data != null && ciphers.data.length) { const decCiphers: CipherView[] = []; const promises: any[] = []; - ciphers.data.forEach((r) => { + ciphers.data.forEach(r => { const data = new CipherData(r); const cipher = new Cipher(data); - promises.push(cipher.decrypt().then((c) => decCiphers.push(c))); + promises.push(cipher.decrypt().then(c => decCiphers.push(c))); }); await Promise.all(promises); decCiphers.sort(this.getLocaleSortingFunction()); @@ -556,7 +556,7 @@ export class CipherService implements CipherServiceAbstraction { async shareWithServer(cipher: CipherView, organizationId: string, collectionIds: string[]): Promise { const attachmentPromises: Promise[] = []; if (cipher.attachments != null) { - cipher.attachments.forEach((attachment) => { + cipher.attachments.forEach(attachment => { if (attachment.key == null) { attachmentPromises.push(this.shareAttachmentWithServer(attachment, cipher.id, organizationId)); } @@ -580,7 +580,7 @@ export class CipherService implements CipherServiceAbstraction { for (const cipher of ciphers) { cipher.organizationId = organizationId; cipher.collectionIds = collectionIds; - promises.push(this.encrypt(cipher).then((c) => { + promises.push(this.encrypt(cipher).then(c => { encCiphers.push(c); })); } @@ -588,7 +588,7 @@ export class CipherService implements CipherServiceAbstraction { const request = new CipherBulkShareRequest(encCiphers, collectionIds); await this.apiService.putShareCiphers(request); const userId = await this.userService.getUserId(); - await this.upsert(encCiphers.map((c) => c.toCipherData(userId))); + await this.upsert(encCiphers.map(c => c.toCipherData(userId))); } saveAttachmentWithServer(cipher: Cipher, unencryptedFile: any, admin = false): Promise { @@ -604,7 +604,7 @@ export class CipherService implements CipherServiceAbstraction { reject(e); } }; - reader.onerror = (evt) => { + reader.onerror = evt => { reject('Error reading file.'); }; }); @@ -674,7 +674,7 @@ export class CipherService implements CipherServiceAbstraction { const c = cipher as CipherData; ciphers[c.id] = c; } else { - (cipher as CipherData[]).forEach((c) => { + (cipher as CipherData[]).forEach(c => { ciphers[c.id] = c; }); } @@ -704,7 +704,7 @@ export class CipherService implements CipherServiceAbstraction { ciphers = {}; } - ids.forEach((id) => { + ids.forEach(id => { if (ciphers.hasOwnProperty(id)) { ciphers[id].folderId = folderId; } @@ -728,7 +728,7 @@ export class CipherService implements CipherServiceAbstraction { } delete ciphers[id]; } else { - (id as string[]).forEach((i) => { + (id as string[]).forEach(i => { delete ciphers[i]; }); } diff --git a/src/services/collection.service.ts b/src/services/collection.service.ts index 61e233ff65..3c594080f8 100644 --- a/src/services/collection.service.ts +++ b/src/services/collection.service.ts @@ -52,8 +52,8 @@ export class CollectionService implements CollectionServiceAbstraction { } const decCollections: CollectionView[] = []; const promises: Promise[] = []; - collections.forEach((collection) => { - promises.push(collection.decrypt().then((c) => decCollections.push(c))); + collections.forEach(collection => { + promises.push(collection.decrypt().then(c => decCollections.push(c))); }); await Promise.all(promises); return decCollections.sort(Utils.getSortFunction(this.i18nService, 'name')); @@ -103,7 +103,7 @@ export class CollectionService implements CollectionServiceAbstraction { collections = await this.getAllDecrypted(); } const nodes: TreeNode[] = []; - collections.forEach((c) => { + collections.forEach(c => { const collectionCopy = new CollectionView(); collectionCopy.id = c.id; collectionCopy.organizationId = c.organizationId; @@ -130,7 +130,7 @@ export class CollectionService implements CollectionServiceAbstraction { const c = collection as CollectionData; collections[c.id] = c; } else { - (collection as CollectionData[]).forEach((c) => { + (collection as CollectionData[]).forEach(c => { collections[c.id] = c; }); } @@ -162,7 +162,7 @@ export class CollectionService implements CollectionServiceAbstraction { const i = id as string; delete collections[id]; } else { - (id as string[]).forEach((i) => { + (id as string[]).forEach(i => { delete collections[i]; }); } diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index f2040caf81..6eadd432a7 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -80,7 +80,7 @@ export class CryptoService implements CryptoServiceAbstraction { setOrgKeys(orgs: ProfileOrganizationResponse[]): Promise<{}> { const orgKeys: any = {}; - orgs.forEach((org) => { + orgs.forEach(org => { orgKeys[org.id] = org.key; }); diff --git a/src/services/event.service.ts b/src/services/event.service.ts index 316073d3e5..b34cd4b69a 100644 --- a/src/services/event.service.ts +++ b/src/services/event.service.ts @@ -39,7 +39,7 @@ export class EventService implements EventServiceAbstraction { if (organizations == null) { return; } - const orgIds = new Set(organizations.filter((o) => o.useEvents).map((o) => o.id)); + const orgIds = new Set(organizations.filter(o => o.useEvents).map(o => o.id)); if (orgIds.size === 0) { return; } @@ -73,7 +73,7 @@ export class EventService implements EventServiceAbstraction { if (eventCollection == null || eventCollection.length === 0) { return; } - const request = eventCollection.map((e) => { + const request = eventCollection.map(e => { const req = new EventRequest(); req.type = e.type; req.cipherId = e.cipherId; diff --git a/src/services/export.service.ts b/src/services/export.service.ts index 77a020d864..a53ce3d104 100644 --- a/src/services/export.service.ts +++ b/src/services/export.service.ts @@ -59,11 +59,11 @@ export class ExportService implements ExportServiceAbstraction { let decCiphers: CipherView[] = []; const promises = []; - promises.push(this.folderService.getAllDecrypted().then((folders) => { + promises.push(this.folderService.getAllDecrypted().then(folders => { decFolders = folders; })); - promises.push(this.cipherService.getAllDecrypted().then((ciphers) => { + promises.push(this.cipherService.getAllDecrypted().then(ciphers => { decCiphers = ciphers.filter(f => f.deletedDate == null); })); @@ -71,14 +71,14 @@ export class ExportService implements ExportServiceAbstraction { if (format === 'csv') { const foldersMap = new Map(); - decFolders.forEach((f) => { + decFolders.forEach(f => { if (f.id != null) { foldersMap.set(f.id, f); } }); const exportCiphers: any[] = []; - decCiphers.forEach((c) => { + decCiphers.forEach(c => { // only export logins and secure notes if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) { return; @@ -103,7 +103,7 @@ export class ExportService implements ExportServiceAbstraction { items: [], }; - decFolders.forEach((f) => { + decFolders.forEach(f => { if (f.id == null) { return; } @@ -112,7 +112,7 @@ export class ExportService implements ExportServiceAbstraction { jsonDoc.folders.push(folder); }); - decCiphers.forEach((c) => { + decCiphers.forEach(c => { if (c.organizationId != null) { return; } @@ -131,12 +131,12 @@ export class ExportService implements ExportServiceAbstraction { let ciphers: Cipher[] = []; const promises = []; - promises.push(this.folderService.getAll().then((f) => { + promises.push(this.folderService.getAll().then(f => { folders = f; })); - promises.push(this.cipherService.getAll().then((c) => { - ciphers = c.filter((f) => f.deletedDate == null); + promises.push(this.cipherService.getAll().then(c => { + ciphers = c.filter(f => f.deletedDate == null); })); await Promise.all(promises); @@ -147,7 +147,7 @@ export class ExportService implements ExportServiceAbstraction { items: [], }; - folders.forEach((f) => { + folders.forEach(f => { if (f.id == null) { return; } @@ -156,7 +156,7 @@ export class ExportService implements ExportServiceAbstraction { jsonDoc.folders.push(folder); }); - ciphers.forEach((c) => { + ciphers.forEach(c => { if (c.organizationId != null) { return; } @@ -174,12 +174,12 @@ export class ExportService implements ExportServiceAbstraction { const decCiphers: CipherView[] = []; const promises = []; - promises.push(this.apiService.getCollections(organizationId).then((collections) => { + promises.push(this.apiService.getCollections(organizationId).then(collections => { const collectionPromises: any = []; if (collections != null && collections.data != null && collections.data.length > 0) { - collections.data.forEach((c) => { + collections.data.forEach(c => { const collection = new Collection(new CollectionData(c as CollectionDetailsResponse)); - collectionPromises.push(collection.decrypt().then((decCol) => { + collectionPromises.push(collection.decrypt().then(decCol => { decCollections.push(decCol); })); }); @@ -187,12 +187,12 @@ export class ExportService implements ExportServiceAbstraction { return Promise.all(collectionPromises); })); - promises.push(this.apiService.getCiphersOrganization(organizationId).then((ciphers) => { + promises.push(this.apiService.getCiphersOrganization(organizationId).then(ciphers => { const cipherPromises: any = []; if (ciphers != null && ciphers.data != null && ciphers.data.length > 0) { - ciphers.data.filter((c) => c.deletedDate === null).forEach((c) => { + ciphers.data.filter(c => c.deletedDate === null).forEach(c => { const cipher = new Cipher(new CipherData(c)); - cipherPromises.push(cipher.decrypt().then((decCipher) => { + cipherPromises.push(cipher.decrypt().then(decCipher => { decCiphers.push(decCipher); })); }); @@ -204,12 +204,12 @@ export class ExportService implements ExportServiceAbstraction { if (format === 'csv') { const collectionsMap = new Map(); - decCollections.forEach((c) => { + decCollections.forEach(c => { collectionsMap.set(c.id, c); }); const exportCiphers: any[] = []; - decCiphers.forEach((c) => { + decCiphers.forEach(c => { // only export logins and secure notes if (c.type !== CipherType.Login && c.type !== CipherType.SecureNote) { return; @@ -218,8 +218,8 @@ export class ExportService implements ExportServiceAbstraction { const cipher: any = {}; cipher.collections = []; if (c.collectionIds != null) { - cipher.collections = c.collectionIds.filter((id) => collectionsMap.has(id)) - .map((id) => collectionsMap.get(id).name); + cipher.collections = c.collectionIds.filter(id => collectionsMap.has(id)) + .map(id => collectionsMap.get(id).name); } this.buildCommonCipher(cipher, c); exportCiphers.push(cipher); @@ -233,13 +233,13 @@ export class ExportService implements ExportServiceAbstraction { items: [], }; - decCollections.forEach((c) => { + decCollections.forEach(c => { const collection = new CollectionExport(); collection.build(c); jsonDoc.collections.push(collection); }); - decCiphers.forEach((c) => { + decCiphers.forEach(c => { const cipher = new CipherExport(); cipher.build(c); jsonDoc.items.push(cipher); @@ -253,10 +253,10 @@ export class ExportService implements ExportServiceAbstraction { const ciphers: Cipher[] = []; const promises = []; - promises.push(this.apiService.getCollections(organizationId).then((c) => { + promises.push(this.apiService.getCollections(organizationId).then(c => { const collectionPromises: any = []; if (c != null && c.data != null && c.data.length > 0) { - c.data.forEach((r) => { + c.data.forEach(r => { const collection = new Collection(new CollectionData(r as CollectionDetailsResponse)); collections.push(collection); }); @@ -264,10 +264,10 @@ export class ExportService implements ExportServiceAbstraction { return Promise.all(collectionPromises); })); - promises.push(this.apiService.getCiphersOrganization(organizationId).then((c) => { + promises.push(this.apiService.getCiphersOrganization(organizationId).then(c => { const cipherPromises: any = []; if (c != null && c.data != null && c.data.length > 0) { - c.data.filter((item) => item.deletedDate === null).forEach((item) => { + c.data.filter(item => item.deletedDate === null).forEach(item => { const cipher = new Cipher(new CipherData(item)); ciphers.push(cipher); }); @@ -283,13 +283,13 @@ export class ExportService implements ExportServiceAbstraction { items: [], }; - collections.forEach((c) => { + collections.forEach(c => { const collection = new CollectionExport(); collection.build(c); jsonDoc.collections.push(collection); }); - ciphers.forEach((c) => { + ciphers.forEach(c => { const cipher = new CipherExport(); cipher.build(c); jsonDoc.items.push(cipher); @@ -335,7 +335,7 @@ export class ExportService implements ExportServiceAbstraction { if (c.login.uris) { cipher.login_uri = []; - c.login.uris.forEach((u) => { + c.login.uris.forEach(u => { cipher.login_uri.push(u.uri); }); } diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index dad533973a..0f3ae50e23 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -83,8 +83,8 @@ export class FolderService implements FolderServiceAbstraction { const decFolders: FolderView[] = []; const promises: Promise[] = []; const folders = await this.getAll(); - folders.forEach((folder) => { - promises.push(folder.decrypt().then((f) => decFolders.push(f))); + folders.forEach(folder => { + promises.push(folder.decrypt().then(f => decFolders.push(f))); }); await Promise.all(promises); @@ -101,7 +101,7 @@ export class FolderService implements FolderServiceAbstraction { async getAllNested(): Promise[]> { const folders = await this.getAllDecrypted(); const nodes: TreeNode[] = []; - folders.forEach((f) => { + folders.forEach(f => { const folderCopy = new FolderView(); folderCopy.id = f.id; folderCopy.revisionDate = f.revisionDate; @@ -144,7 +144,7 @@ export class FolderService implements FolderServiceAbstraction { const f = folder as FolderData; folders[f.id] = f; } else { - (folder as FolderData[]).forEach((f) => { + (folder as FolderData[]).forEach(f => { folders[f.id] = f; }); } @@ -178,7 +178,7 @@ export class FolderService implements FolderServiceAbstraction { } delete folders[id]; } else { - (id as string[]).forEach((i) => { + (id as string[]).forEach(i => { delete folders[i]; }); } diff --git a/src/services/import.service.ts b/src/services/import.service.ts index d41cee06bd..26cb1412d6 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -306,7 +306,7 @@ export class ImportService implements ImportServiceAbstraction { } } if (importResult.folderRelationships != null) { - importResult.folderRelationships.forEach((r) => + importResult.folderRelationships.forEach(r => request.folderRelationships.push(new KvpRequest(r[0], r[1]))); } return await this.apiService.postImportCiphers(request); @@ -325,7 +325,7 @@ export class ImportService implements ImportServiceAbstraction { } } if (importResult.collectionRelationships != null) { - importResult.collectionRelationships.forEach((r) => + importResult.collectionRelationships.forEach(r => request.collectionRelationships.push(new KvpRequest(r[0], r[1]))); } return await this.apiService.postImportOrganizationCiphers(organizationId, request); diff --git a/src/services/passwordGeneration.service.ts b/src/services/passwordGeneration.service.ts index fc83dcb0d5..6bce0a1732 100644 --- a/src/services/passwordGeneration.service.ts +++ b/src/services/passwordGeneration.service.ts @@ -269,7 +269,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return enforcedOptions; } - policies.forEach((currentPolicy) => { + policies.forEach(currentPolicy => { if (!currentPolicy.enabled || currentPolicy.data == null) { return; } @@ -471,7 +471,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return Promise.resolve([]); } - const promises = history.map(async (item) => { + const promises = history.map(async item => { const encrypted = await this.cryptoService.encrypt(item.password); return new GeneratedPasswordHistory(encrypted.encryptedString, item.date); }); @@ -484,7 +484,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr return Promise.resolve([]); } - const promises = history.map(async (item) => { + const promises = history.map(async item => { const decrypted = await this.cryptoService.decryptToUtf8(new CipherString(item.password)); return new GeneratedPasswordHistory(decrypted, item.date); }); diff --git a/src/services/policy.service.ts b/src/services/policy.service.ts index 6d3dcb7518..a929aec9d4 100644 --- a/src/services/policy.service.ts +++ b/src/services/policy.service.ts @@ -37,7 +37,7 @@ export class PolicyService implements PolicyServiceAbstraction { this.policyCache = response; } if (type != null) { - return this.policyCache.filter((p) => p.type === type); + return this.policyCache.filter(p => p.type === type); } else { return this.policyCache; } @@ -60,14 +60,14 @@ export class PolicyService implements PolicyServiceAbstraction { if (policies == null) { policies = await this.getAll(PolicyType.MasterPassword); } else { - policies = policies.filter((p) => p.type === PolicyType.MasterPassword); + policies = policies.filter(p => p.type === PolicyType.MasterPassword); } if (policies == null || policies.length === 0) { return enforcedOptions; } - policies.forEach((currentPolicy) => { + policies.forEach(currentPolicy => { if (!currentPolicy.enabled || currentPolicy.data == null) { return; } diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 0fc96ac5b0..fee647dc5c 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -61,7 +61,7 @@ export class SearchService implements SearchServiceAbstraction { { extractor: (c: CipherView) => this.attachmentExtractor(c, true) }); builder.field('organizationid', { extractor: (c: CipherView) => c.organizationId }); const ciphers = await this.cipherService.getAllDecrypted(); - ciphers.forEach((c) => builder.add(c)); + ciphers.forEach(c => builder.add(c)); this.index = builder.build(); this.indexing = false; @@ -85,7 +85,7 @@ export class SearchService implements SearchServiceAbstraction { } if (filter != null && Array.isArray(filter) && filter.length > 0) { - ciphers = ciphers.filter((c) => filter.every((f) => f == null || f(c))); + ciphers = ciphers.filter(c => filter.every(f => f == null || f(c))); } else if (filter != null) { ciphers = ciphers.filter(filter as (cipher: CipherView) => boolean); } @@ -95,9 +95,9 @@ export class SearchService implements SearchServiceAbstraction { } if (this.indexing) { - await new Promise((r) => setTimeout(r, 250)); + await new Promise(r => setTimeout(r, 250)); if (this.indexing) { - await new Promise((r) => setTimeout(r, 500)); + await new Promise(r => setTimeout(r, 500)); } } @@ -108,7 +108,7 @@ export class SearchService implements SearchServiceAbstraction { } const ciphersMap = new Map(); - ciphers.forEach((c) => ciphersMap.set(c.id, c)); + ciphers.forEach(c => ciphersMap.set(c.id, c)); let searchResults: lunr.Index.Result[] = null; const isQueryString = query != null && query.length > 1 && query.indexOf('>') === 0; @@ -119,8 +119,8 @@ export class SearchService implements SearchServiceAbstraction { } else { // tslint:disable-next-line const soWild = lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING; - searchResults = index.query((q) => { - lunr.tokenizer(query).forEach((token) => { + searchResults = index.query(q => { + lunr.tokenizer(query).forEach(token => { const t = token.toString(); q.term(t, { fields: ['name'], wildcard: soWild }); q.term(t, { fields: ['subtitle'], wildcard: soWild }); @@ -131,7 +131,7 @@ export class SearchService implements SearchServiceAbstraction { } if (searchResults != null) { - searchResults.forEach((r) => { + searchResults.forEach(r => { if (ciphersMap.has(r.ref)) { results.push(ciphersMap.get(r.ref)); } @@ -142,7 +142,7 @@ export class SearchService implements SearchServiceAbstraction { searchCiphersBasic(ciphers: CipherView[], query: string, deleted: boolean = false) { query = query.trim().toLowerCase(); - return ciphers.filter((c) => { + return ciphers.filter(c => { if (deleted !== c.isDeleted) { return false; } @@ -193,7 +193,7 @@ export class SearchService implements SearchServiceAbstraction { return null; } let fields: string[] = []; - c.fields.forEach((f) => { + c.fields.forEach(f => { if (f.name != null) { fields.push(f.name); } @@ -201,7 +201,7 @@ export class SearchService implements SearchServiceAbstraction { fields.push(f.value); } }); - fields = fields.filter((f) => f.trim() !== ''); + fields = fields.filter(f => f.trim() !== ''); if (fields.length === 0) { return null; } @@ -213,7 +213,7 @@ export class SearchService implements SearchServiceAbstraction { return null; } let attachments: string[] = []; - c.attachments.forEach((a) => { + c.attachments.forEach(a => { if (a != null && a.fileName != null) { if (joined && a.fileName.indexOf('.') > -1) { attachments.push(a.fileName.substr(0, a.fileName.lastIndexOf('.'))); @@ -222,7 +222,7 @@ export class SearchService implements SearchServiceAbstraction { } } }); - attachments = attachments.filter((f) => f.trim() !== ''); + attachments = attachments.filter(f => f.trim() !== ''); if (attachments.length === 0) { return null; } @@ -234,7 +234,7 @@ export class SearchService implements SearchServiceAbstraction { return null; } const uris: string[] = []; - c.login.uris.forEach((u) => { + c.login.uris.forEach(u => { if (u.uri == null || u.uri === '') { return; } diff --git a/src/services/send.service.ts b/src/services/send.service.ts index 284f60cce3..5a37c242d1 100644 --- a/src/services/send.service.ts +++ b/src/services/send.service.ts @@ -115,8 +115,8 @@ export class SendService implements SendServiceAbstraction { const decSends: SendView[] = []; const promises: Promise[] = []; const sends = await this.getAll(); - sends.forEach((send) => { - promises.push(send.decrypt().then((f) => decSends.push(f))); + sends.forEach(send => { + promises.push(send.decrypt().then(f => decSends.push(f))); }); await Promise.all(promises); @@ -173,7 +173,7 @@ export class SendService implements SendServiceAbstraction { const s = send as SendData; sends[s.id] = s; } else { - (send as SendData[]).forEach((s) => { + (send as SendData[]).forEach(s => { sends[s.id] = s; }); } @@ -207,7 +207,7 @@ export class SendService implements SendServiceAbstraction { } delete sends[id]; } else { - (id as string[]).forEach((i) => { + (id as string[]).forEach(i => { delete sends[i]; }); } @@ -232,7 +232,7 @@ export class SendService implements SendServiceAbstraction { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsArrayBuffer(file); - reader.onload = async (evt) => { + reader.onload = async evt => { try { const [name, data] = await this.encryptFileData(file.name, evt.target.result as ArrayBuffer, key); send.file.fileName = name; @@ -241,7 +241,7 @@ export class SendService implements SendServiceAbstraction { reject(e); } }; - reader.onerror = (evt) => { + reader.onerror = evt => { reject('Error reading file.'); }; }); diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index a609748b58..55cc58fa16 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -290,7 +290,7 @@ export class SyncService implements SyncServiceAbstraction { await this.userService.setSecurityStamp(response.securityStamp); const organizations: { [id: string]: OrganizationData; } = {}; - response.organizations.forEach((o) => { + response.organizations.forEach(o => { organizations[o.id] = new OrganizationData(o); }); return await this.userService.replaceOrganizations(organizations); @@ -298,7 +298,7 @@ export class SyncService implements SyncServiceAbstraction { private async syncFolders(userId: string, response: FolderResponse[]) { const folders: { [id: string]: FolderData; } = {}; - response.forEach((f) => { + response.forEach(f => { folders[f.id] = new FolderData(f, userId); }); return await this.folderService.replace(folders); @@ -306,7 +306,7 @@ export class SyncService implements SyncServiceAbstraction { private async syncCollections(response: CollectionDetailsResponse[]) { const collections: { [id: string]: CollectionData; } = {}; - response.forEach((c) => { + response.forEach(c => { collections[c.id] = new CollectionData(c); }); return await this.collectionService.replace(collections); @@ -314,7 +314,7 @@ export class SyncService implements SyncServiceAbstraction { private async syncCiphers(userId: string, response: CipherResponse[]) { const ciphers: { [id: string]: CipherData; } = {}; - response.forEach((c) => { + response.forEach(c => { ciphers[c.id] = new CipherData(c, userId); }); return await this.cipherService.replace(ciphers); @@ -322,7 +322,7 @@ export class SyncService implements SyncServiceAbstraction { private async syncSends(userId: string, response: SendResponse[]) { const sends: { [id: string]: SendData; } = {}; - response.forEach((s) => { + response.forEach(s => { sends[s.id] = new SendData(s, userId); }); return await this.sendService.replace(sends); @@ -335,7 +335,7 @@ export class SyncService implements SyncServiceAbstraction { } if (response != null && response.globalEquivalentDomains != null) { - response.globalEquivalentDomains.forEach((global) => { + response.globalEquivalentDomains.forEach(global => { if (global.domains.length > 0) { eqDomains.push(global.domains); } @@ -348,7 +348,7 @@ export class SyncService implements SyncServiceAbstraction { private async syncPolicies(response: PolicyResponse[]) { const policies: { [id: string]: PolicyData; } = {}; if (response != null) { - response.forEach((p) => { + response.forEach(p => { policies[p.id] = new PolicyData(p); }); } diff --git a/src/services/system.service.ts b/src/services/system.service.ts index d5d0d6ea01..053dd50de4 100644 --- a/src/services/system.service.ts +++ b/src/services/system.service.ts @@ -59,7 +59,7 @@ export class SystemService implements SystemServiceAbstraction { if (Utils.isNullOrWhitespace(clipboardValue)) { return; } - this.storageService.get(ConstantsService.clearClipboardKey).then((clearSeconds) => { + this.storageService.get(ConstantsService.clearClipboardKey).then(clearSeconds => { if (clearSeconds == null) { return; } diff --git a/tslint.json b/tslint.json index 3fe8b5b5ba..76f1843ee8 100644 --- a/tslint.json +++ b/tslint.json @@ -52,6 +52,10 @@ "check-type" ], "max-classes-per-file": false, + "arrow-parens": [ + true, + "ban-single-arg-parens" + ], "semicolon": [ true, "always" From deabffb7b0aaf9873b212f9962158fc312c4f271 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Thu, 4 Feb 2021 11:22:31 -0600 Subject: [PATCH 1150/1626] Implement disable send policy (#259) * Implement disable send policy * Linter fixes * Add toast on submit if sends are disabled --- .../components/send/add-edit.component.ts | 22 ++++++++++++++++++- src/angular/components/send/send.component.ts | 20 +++++++++++++++-- src/enums/policyType.ts | 1 + 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/angular/components/send/add-edit.component.ts b/src/angular/components/send/add-edit.component.ts index fb90fa9762..c764cce88a 100644 --- a/src/angular/components/send/add-edit.component.ts +++ b/src/angular/components/send/add-edit.component.ts @@ -7,12 +7,15 @@ import { Output, } from '@angular/core'; +import { OrganizationUserStatusType } from '../../../enums/organizationUserStatusType'; +import { PolicyType } from '../../../enums/policyType'; import { SendType } from '../../../enums/sendType'; import { EnvironmentService } from '../../../abstractions/environment.service'; import { I18nService } from '../../../abstractions/i18n.service'; import { MessagingService } from '../../../abstractions/messaging.service'; import { PlatformUtilsService } from '../../../abstractions/platformUtils.service'; +import { PolicyService } from '../../../abstractions/policy.service'; import { SendService } from '../../../abstractions/send.service'; import { UserService } from '../../../abstractions/user.service'; @@ -30,6 +33,7 @@ export class AddEditComponent implements OnInit { @Output() onDeletedSend = new EventEmitter(); @Output() onCancelled = new EventEmitter(); + disableSend = false; editMode: boolean = false; send: SendView; link: string; @@ -52,7 +56,7 @@ export class AddEditComponent implements OnInit { constructor(protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected environmentService: EnvironmentService, protected datePipe: DatePipe, protected sendService: SendService, protected userService: UserService, - protected messagingService: MessagingService) { + protected messagingService: MessagingService, protected policyService: PolicyService) { this.typeOptions = [ { name: i18nService.t('sendTypeFile'), value: SendType.File }, { name: i18nService.t('sendTypeText'), value: SendType.Text }, @@ -84,6 +88,16 @@ export class AddEditComponent implements OnInit { this.title = this.i18nService.t('createSend'); } + const policies = await this.policyService.getAll(PolicyType.DisableSend); + const organizations = await this.userService.getAllOrganizations(); + this.disableSend = organizations.some(o => { + return o.enabled && + o.status === OrganizationUserStatusType.Confirmed && + o.usePolicies && + !o.canManagePolicies && + policies.some(p => p.organizationId === o.id && p.enabled); + }); + this.canAccessPremium = await this.userService.canAccessPremium(); if (!this.canAccessPremium) { this.type = SendType.Text; @@ -119,6 +133,12 @@ export class AddEditComponent implements OnInit { } async submit(): Promise { + if (this.disableSend) { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('sendDisabledWarning')); + return false; + } + if (this.send.name == null || this.send.name === '') { this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), this.i18nService.t('nameRequired')); diff --git a/src/angular/components/send/send.component.ts b/src/angular/components/send/send.component.ts index 674e9622f6..ee521f1120 100644 --- a/src/angular/components/send/send.component.ts +++ b/src/angular/components/send/send.component.ts @@ -3,6 +3,8 @@ import { OnInit, } from '@angular/core'; +import { OrganizationUserStatusType } from '../../../enums/organizationUserStatusType'; +import { PolicyType } from '../../../enums/policyType'; import { SendType } from '../../../enums/sendType'; import { SendView } from '../../../models/view/sendView'; @@ -10,8 +12,10 @@ import { SendView } from '../../../models/view/sendView'; import { EnvironmentService } from '../../../abstractions/environment.service'; import { I18nService } from '../../../abstractions/i18n.service'; import { PlatformUtilsService } from '../../../abstractions/platformUtils.service'; +import { PolicyService } from '../../../abstractions/policy.service'; import { SearchService } from '../../../abstractions/search.service'; import { SendService } from '../../../abstractions/send.service'; +import { UserService } from '../../../abstractions/user.service'; import { BroadcasterService } from '../../../angular/services/broadcaster.service'; @@ -19,6 +23,7 @@ const BroadcasterSubscriptionId = 'SendComponent'; export class SendComponent implements OnInit { + disableSend = false; sendType = SendType; loaded = false; loading = true; @@ -43,9 +48,20 @@ export class SendComponent implements OnInit { constructor(protected sendService: SendService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected environmentService: EnvironmentService, protected broadcasterService: BroadcasterService, protected ngZone: NgZone, - protected searchService: SearchService) { } + protected searchService: SearchService, protected policyService: PolicyService, + protected userService: UserService) { } async ngOnInit() { + const policies = await this.policyService.getAll(PolicyType.DisableSend); + const organizations = await this.userService.getAllOrganizations(); + this.disableSend = organizations.some(o => { + return o.enabled && + o.status === OrganizationUserStatusType.Confirmed && + o.usePolicies && + !o.canManagePolicies && + policies.some(p => p.organizationId === o.id && p.enabled); + }); + this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { this.ngZone.run(async () => { switch (message.command) { @@ -199,7 +215,7 @@ export class SendComponent implements OnInit { private applyTextSearch() { if (this.searchText != null) { - this.filteredSends = this.searchService.searchSends(this.filteredSends, this.searchText); + this.filteredSends = this.searchService.searchSends(this.filteredSends, this.searchText); } } } diff --git a/src/enums/policyType.ts b/src/enums/policyType.ts index a01fa7df47..8ed07c9d77 100644 --- a/src/enums/policyType.ts +++ b/src/enums/policyType.ts @@ -5,4 +5,5 @@ export enum PolicyType { SingleOrg = 3, // Allows users to only be apart of one organization RequireSso = 4, // Requires users to authenticate with SSO PersonalOwnership = 5, // Disables personal vault ownership for adding/cloning items + DisableSend = 6, // Disables the ability to create and edit Bitwarden Sends } From 22f576ea60fdd19cd0eb3c11d3073a45240b9f98 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Fri, 5 Feb 2021 12:36:09 -0600 Subject: [PATCH 1151/1626] Add password toggle to add-edit (#261) --- src/angular/components/send/add-edit.component.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/angular/components/send/add-edit.component.ts b/src/angular/components/send/add-edit.component.ts index c764cce88a..b356816669 100644 --- a/src/angular/components/send/add-edit.component.ts +++ b/src/angular/components/send/add-edit.component.ts @@ -42,6 +42,7 @@ export class AddEditComponent implements OnInit { expirationDate: string; hasPassword: boolean; password: string; + showPassword = false; formPromise: Promise; deletePromise: Promise; sendType = SendType; @@ -248,4 +249,9 @@ export class AddEditComponent implements OnInit { protected dateToString(d: Date) { return d == null ? null : this.datePipe.transform(d, 'yyyy-MM-ddTHH:mm'); } + + protected togglePasswordVisible() { + this.showPassword = !this.showPassword; + document.getElementById('password').focus(); + } } From df59f99ec649c1f4a75456e1e5a3c0f98de0e9c1 Mon Sep 17 00:00:00 2001 From: Lombra Date: Mon, 8 Feb 2021 16:03:43 +0100 Subject: [PATCH 1152/1626] Enable subpixel antialiasing (#256) --- src/electron/window.main.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/electron/window.main.ts b/src/electron/window.main.ts index e34d6f6560..f76010db90 100644 --- a/src/electron/window.main.ts +++ b/src/electron/window.main.ts @@ -114,6 +114,7 @@ export class WindowMain { icon: process.platform === 'linux' ? path.join(__dirname, '/images/icon.png') : undefined, titleBarStyle: this.hideTitleBar && process.platform === 'darwin' ? 'hiddenInset' : undefined, show: false, + backgroundColor: '#fff', alwaysOnTop: this.enableAlwaysOnTop, webPreferences: { nodeIntegration: true, From 6183a30a52b3dfeb24ead0d6b4adf34c4703a5c5 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Mon, 8 Feb 2021 13:21:51 -0600 Subject: [PATCH 1153/1626] Remove unused logout argument (#263) --- src/cli/commands/logout.command.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/commands/logout.command.ts b/src/cli/commands/logout.command.ts index 34413191e5..747cbcefd7 100644 --- a/src/cli/commands/logout.command.ts +++ b/src/cli/commands/logout.command.ts @@ -10,7 +10,7 @@ export class LogoutCommand { constructor(private authService: AuthService, private i18nService: I18nService, private logoutCallback: () => Promise) { } - async run(cmd: program.Command) { + async run() { await this.logoutCallback(); this.authService.logOut(() => { /* Do nothing */ }); const res = new MessageResponse('You have logged out.', null); From 2730e04a0571a7cfa2455309ab641c1180c96df8 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Mon, 8 Feb 2021 14:11:44 -0600 Subject: [PATCH 1154/1626] Match tslint rules (#264) --- .../importers/onepasswordMacCsvImporter.spec.ts | 2 +- .../importers/onepasswordWinCsvImporter.spec.ts | 6 +++--- spec/common/services/consoleLog.service.spec.ts | 2 +- spec/common/services/export.service.spec.ts | 4 ++-- src/angular/components/sso.component.ts | 2 +- tslint.json | 12 ++++++++++++ 6 files changed, 20 insertions(+), 8 deletions(-) diff --git a/spec/common/importers/onepasswordMacCsvImporter.spec.ts b/spec/common/importers/onepasswordMacCsvImporter.spec.ts index 4cc270f492..748a2623d2 100644 --- a/spec/common/importers/onepasswordMacCsvImporter.spec.ts +++ b/spec/common/importers/onepasswordMacCsvImporter.spec.ts @@ -17,7 +17,7 @@ function expectIdentity(cipher: CipherView) { username: 'userNam3', company: 'bitwarden', phone: '8005555555', - email: 'email@bitwarden.com' + email: 'email@bitwarden.com', })); expect(cipher.notes).toContain('address\ncity state zip\nUnited States'); diff --git a/spec/common/importers/onepasswordWinCsvImporter.spec.ts b/spec/common/importers/onepasswordWinCsvImporter.spec.ts index a456d0bbd1..19e6026d2f 100644 --- a/spec/common/importers/onepasswordWinCsvImporter.spec.ts +++ b/spec/common/importers/onepasswordWinCsvImporter.spec.ts @@ -18,15 +18,15 @@ function expectIdentity(cipher: CipherView) { username: 'userNam3', company: 'bitwarden', phone: '8005555555', - email: 'email@bitwarden.com' + email: 'email@bitwarden.com', })); expect(cipher.fields).toEqual(jasmine.arrayContaining([ Object.assign(new FieldView(), { type: FieldType.Text, name: 'address', - value: 'address city state zip us' - }) + value: 'address city state zip us', + }), ])); } diff --git a/spec/common/services/consoleLog.service.spec.ts b/spec/common/services/consoleLog.service.spec.ts index 2aa062418b..57cb8a3e9b 100644 --- a/spec/common/services/consoleLog.service.spec.ts +++ b/spec/common/services/consoleLog.service.spec.ts @@ -18,7 +18,7 @@ export function interceptConsole(interceptions: any): object { // tslint:disable-next-line error: function () { interceptions.error = arguments; - } + }, }; return interceptions; } diff --git a/spec/common/services/export.service.spec.ts b/spec/common/services/export.service.spec.ts index 4cbcf2831a..dd209e8b3d 100644 --- a/spec/common/services/export.service.spec.ts +++ b/spec/common/services/export.service.spec.ts @@ -20,13 +20,13 @@ import { BuildTestObject, GetUniqueString } from '../../utils'; const UserCipherViews = [ generateCipherView(false), generateCipherView(false), - generateCipherView(true) + generateCipherView(true), ]; const UserCipherDomains = [ generateCipherDomain(false), generateCipherDomain(false), - generateCipherDomain(true) + generateCipherDomain(true), ]; function generateCipherView(deleted: boolean) { diff --git a/src/angular/components/sso.component.ts b/src/angular/components/sso.component.ts index 7fbdb37211..94f44040de 100644 --- a/src/angular/components/sso.component.ts +++ b/src/angular/components/sso.component.ts @@ -145,7 +145,7 @@ export class SsoComponent { this.router.navigate([this.twoFactorRoute], { queryParams: { identifier: orgIdFromState, - sso: 'true' + sso: 'true', }, }); } diff --git a/tslint.json b/tslint.json index 76f1843ee8..d0c32a3677 100644 --- a/tslint.json +++ b/tslint.json @@ -59,6 +59,18 @@ "semicolon": [ true, "always" + ], + "trailing-comma": [ + true, + { + "multiline": { + "objects": "always", + "arrays": "always", + "functions": "ignore", + "typeLiterals": "ignore" + }, + "singleline": "never" + } ] } } From 58e6f24d5f3365fcf294763e4a3282f2db374886 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Tue, 9 Feb 2021 06:31:41 +1000 Subject: [PATCH 1155/1626] Add policy property to TakeoverResponse (#260) --- src/models/response/emergencyAccessResponse.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/models/response/emergencyAccessResponse.ts b/src/models/response/emergencyAccessResponse.ts index fbd6084938..400f16f755 100644 --- a/src/models/response/emergencyAccessResponse.ts +++ b/src/models/response/emergencyAccessResponse.ts @@ -1,8 +1,10 @@ import { EmergencyAccessStatusType } from '../../enums/emergencyAccessStatusType'; import { EmergencyAccessType } from '../../enums/emergencyAccessType'; import { KdfType } from '../../enums/kdfType'; + import { BaseResponse } from './baseResponse'; import { CipherResponse } from './cipherResponse'; +import { PolicyResponse } from './policyResponse'; export class EmergencyAccessGranteeDetailsResponse extends BaseResponse { id: string; @@ -54,6 +56,7 @@ export class EmergencyAccessTakeoverResponse extends BaseResponse { keyEncrypted: string; kdf: KdfType; kdfIterations: number; + policy: PolicyResponse[]; constructor(response: any) { super(response); @@ -61,6 +64,7 @@ export class EmergencyAccessTakeoverResponse extends BaseResponse { this.keyEncrypted = this.getResponseProperty('KeyEncrypted'); this.kdf = this.getResponseProperty('Kdf'); this.kdfIterations = this.getResponseProperty('KdfIterations'); + this.policy = this.getResponseProperty('policy'); } } From a4ac842cec95d34d4293acd9e5409ff92caa13dc Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 8 Feb 2021 15:55:32 -0500 Subject: [PATCH 1156/1626] support for copying send link to clipboard on save (#265) --- .../components/send/add-edit.component.ts | 40 ++++++++++++++----- src/services/send.service.ts | 1 + 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/angular/components/send/add-edit.component.ts b/src/angular/components/send/add-edit.component.ts index b356816669..995b2c54ba 100644 --- a/src/angular/components/send/add-edit.component.ts +++ b/src/angular/components/send/add-edit.component.ts @@ -33,10 +33,10 @@ export class AddEditComponent implements OnInit { @Output() onDeletedSend = new EventEmitter(); @Output() onCancelled = new EventEmitter(); + copyLink = false; disableSend = false; editMode: boolean = false; send: SendView; - link: string; title: string; deletionDate: string; expirationDate: string; @@ -54,6 +54,8 @@ export class AddEditComponent implements OnInit { canAccessPremium = true; premiumRequiredAlertShown = false; + private webVaultUrl: string; + constructor(protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected environmentService: EnvironmentService, protected datePipe: DatePipe, protected sendService: SendService, protected userService: UserService, @@ -74,6 +76,18 @@ export class AddEditComponent implements OnInit { this.expirationDateOptions = [ { name: i18nService.t('never'), value: null }, ].concat([...this.deletionDateOptions]); + + this.webVaultUrl = this.environmentService.getWebVaultUrl(); + if (this.webVaultUrl == null) { + this.webVaultUrl = 'https://vault.bitwarden.com'; + } + } + + get link(): string { + if (this.send.id != null && this.send.accessId != null) { + return this.webVaultUrl + '/#/send/' + this.send.accessId + '/' + this.send.urlB64Key; + } + return null; } async ngOnInit() { @@ -123,14 +137,6 @@ export class AddEditComponent implements OnInit { // Parse dates this.deletionDate = this.dateToString(this.send.deletionDate); this.expirationDate = this.dateToString(this.send.expirationDate); - - if (this.editMode) { - let webVaultUrl = this.environmentService.getWebVaultUrl(); - if (webVaultUrl == null) { - webVaultUrl = 'https://vault.bitwarden.com'; - } - this.link = webVaultUrl + '/#/send/' + this.send.accessId + '/' + this.send.urlB64Key; - } } async submit(): Promise { @@ -182,10 +188,18 @@ export class AddEditComponent implements OnInit { try { this.formPromise = this.sendService.saveWithServer(encSend); await this.formPromise; - this.send.id = encSend[0].id; + if (this.send.id == null) { + this.send.id = encSend[0].id; + } + if (this.send.accessId == null) { + this.send.accessId = encSend[0].accessId; + } this.platformUtilsService.showToast('success', null, this.i18nService.t(this.editMode ? 'editedSend' : 'createdSend')); this.onSavedSend.emit(this.send); + if (this.copyLink) { + this.copyLinkToClipboard(this.link); + } return true; } catch { } @@ -196,6 +210,12 @@ export class AddEditComponent implements OnInit { this.expirationDate = null; } + copyLinkToClipboard(link: string) { + if (link != null) { + this.platformUtilsService.copyToClipboard(link); + } + } + async delete(): Promise { if (this.deletePromise != null) { return; diff --git a/src/services/send.service.ts b/src/services/send.service.ts index 5a37c242d1..27d1cb9ff6 100644 --- a/src/services/send.service.ts +++ b/src/services/send.service.ts @@ -152,6 +152,7 @@ export class SendService implements SendServiceAbstraction { response = await this.apiService.postSendFile(fd); } sendData[0].id = response.id; + sendData[0].accessId = response.accessId; } else { response = await this.apiService.putSend(sendData[0].id, request); } From eef8a2a0f73794149d0ea88dbab65f2e8a47fc0a Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Mon, 8 Feb 2021 15:55:58 -0600 Subject: [PATCH 1157/1626] Do not evaluate callback function when testing existence (#266) --- src/angular/components/send/send.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/angular/components/send/send.component.ts b/src/angular/components/send/send.component.ts index ee521f1120..214db86731 100644 --- a/src/angular/components/send/send.component.ts +++ b/src/angular/components/send/send.component.ts @@ -169,12 +169,12 @@ export class SendComponent implements OnInit { this.actionPromise = this.sendService.deleteWithServer(s.id); await this.actionPromise; - if (this.onSuccessfulDelete() != null) { + if (this.onSuccessfulDelete != null) { this.onSuccessfulDelete(); } else { // Default actions this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend')); - await this.load(); + await this.refresh(); } } catch { } this.actionPromise = null; From ee164bebc65aa56e41a122eb4ece8971eb23119b Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Mon, 8 Feb 2021 16:18:44 -0600 Subject: [PATCH 1158/1626] [Send] Add more flexibility to base component (#262) * Updating send component for more flexibility in child class * Updated delegte null check * added comment --- src/angular/components/send/send.component.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/angular/components/send/send.component.ts b/src/angular/components/send/send.component.ts index 214db86731..8cf8acd3d7 100644 --- a/src/angular/components/send/send.component.ts +++ b/src/angular/components/send/send.component.ts @@ -38,10 +38,12 @@ export class SendComponent implements OnInit { searchPlaceholder: string; filter: (cipher: SendView) => boolean; searchPending = false; + hasSearched = false; // search() function called - returns true if text qualifies for search actionPromise: any; onSuccessfulRemovePassword: () => Promise; onSuccessfulDelete: () => Promise; + onSuccessfulLoad: () => Promise; private searchTimeout: any; @@ -73,8 +75,6 @@ export class SendComponent implements OnInit { } }); }); - - await this.load(); } ngOnDestroy() { @@ -85,6 +85,9 @@ export class SendComponent implements OnInit { this.loading = true; const sends = await this.sendService.getAllDecrypted(); this.sends = sends; + if (this.onSuccessfulLoad != null) { + await this.onSuccessfulLoad(); + } this.selectAll(); this.loading = false; this.loaded = true; @@ -116,12 +119,14 @@ export class SendComponent implements OnInit { clearTimeout(this.searchTimeout); } if (timeout == null) { + this.hasSearched = this.searchService.isSearchable(this.searchText); this.filteredSends = this.sends.filter(s => this.filter == null || this.filter(s)); this.applyTextSearch(); return; } this.searchPending = true; this.searchTimeout = setTimeout(async () => { + this.hasSearched = this.searchService.isSearchable(this.searchText); this.filteredSends = this.sends.filter(s => this.filter == null || this.filter(s)); this.applyTextSearch(); this.searchPending = false; @@ -142,7 +147,7 @@ export class SendComponent implements OnInit { try { this.actionPromise = this.sendService.removePasswordWithServer(s.id); await this.actionPromise; - if (this.onSuccessfulRemovePassword() != null) { + if (this.onSuccessfulRemovePassword != null) { this.onSuccessfulRemovePassword(); } else { // Default actions From d376927e5e583d816e78a26168f870ac78b071c0 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Wed, 10 Feb 2021 09:06:18 +1000 Subject: [PATCH 1159/1626] Improved handling of grantor access to organizations after takeover (refactored) (#267) * Revert "Add policy property to TakeoverResponse" This reverts commit 31da5081e6833cf8a9d5bb869c14600f25ca3f39. * Add getEmergencyGrantorPolicies to api service --- src/abstractions/api.service.ts | 1 + src/models/response/emergencyAccessResponse.ts | 4 ---- src/services/api.service.ts | 5 +++++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index beb9fec305..63935f17ea 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -292,6 +292,7 @@ export abstract class ApiService { getEmergencyAccessTrusted: () => Promise>; getEmergencyAccessGranted: () => Promise>; getEmergencyAccess: (id: string) => Promise; + getEmergencyGrantorPolicies: (id: string) => Promise>; putEmergencyAccess: (id: string, request: EmergencyAccessUpdateRequest) => Promise; deleteEmergencyAccess: (id: string) => Promise; postEmergencyAccessInvite: (request: EmergencyAccessInviteRequest) => Promise; diff --git a/src/models/response/emergencyAccessResponse.ts b/src/models/response/emergencyAccessResponse.ts index 400f16f755..fbd6084938 100644 --- a/src/models/response/emergencyAccessResponse.ts +++ b/src/models/response/emergencyAccessResponse.ts @@ -1,10 +1,8 @@ import { EmergencyAccessStatusType } from '../../enums/emergencyAccessStatusType'; import { EmergencyAccessType } from '../../enums/emergencyAccessType'; import { KdfType } from '../../enums/kdfType'; - import { BaseResponse } from './baseResponse'; import { CipherResponse } from './cipherResponse'; -import { PolicyResponse } from './policyResponse'; export class EmergencyAccessGranteeDetailsResponse extends BaseResponse { id: string; @@ -56,7 +54,6 @@ export class EmergencyAccessTakeoverResponse extends BaseResponse { keyEncrypted: string; kdf: KdfType; kdfIterations: number; - policy: PolicyResponse[]; constructor(response: any) { super(response); @@ -64,7 +61,6 @@ export class EmergencyAccessTakeoverResponse extends BaseResponse { this.keyEncrypted = this.getResponseProperty('KeyEncrypted'); this.kdf = this.getResponseProperty('Kdf'); this.kdfIterations = this.getResponseProperty('KdfIterations'); - this.policy = this.getResponseProperty('policy'); } } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 793762889d..29c94b65e8 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -933,6 +933,11 @@ export class ApiService implements ApiServiceAbstraction { return new EmergencyAccessGranteeDetailsResponse(r); } + async getEmergencyGrantorPolicies(id: string): Promise> { + const r = await this.send('GET', '/emergency-access/' + id + '/policies', null, true, true); + return new ListResponse(r, PolicyResponse); + } + putEmergencyAccess(id: string, request: EmergencyAccessUpdateRequest): Promise { return this.send('PUT', '/emergency-access/' + id, request, true, false); } From 380b28d66a79bf41e0e1bd82d887666cd3af7a41 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 10 Feb 2021 10:47:55 -0600 Subject: [PATCH 1160/1626] Chmod exported file permissions (#269) * Save all files as current user read-write only * Save all files as current user read-write only --- src/electron/services/electronPlatformUtils.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index 0a43100b9c..29f84de807 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -120,7 +120,7 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { showsTagField: false, }).then(ret => { if (ret.filePath != null) { - fs.writeFile(ret.filePath, Buffer.from(blobData), err => { + fs.writeFile(ret.filePath, Buffer.from(blobData), { mode: 0o600 }, err => { // error check? }); } From 0951424de77fbb61a38616d13d6c67f74ee19775 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Thu, 11 Feb 2021 16:55:38 -0600 Subject: [PATCH 1161/1626] Update send component to follow existing patterns (#270) --- src/angular/components/send/send.component.ts | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/src/angular/components/send/send.component.ts b/src/angular/components/send/send.component.ts index 8cf8acd3d7..8592695337 100644 --- a/src/angular/components/send/send.component.ts +++ b/src/angular/components/send/send.component.ts @@ -17,10 +17,6 @@ import { SearchService } from '../../../abstractions/search.service'; import { SendService } from '../../../abstractions/send.service'; import { UserService } from '../../../abstractions/user.service'; -import { BroadcasterService } from '../../../angular/services/broadcaster.service'; - -const BroadcasterSubscriptionId = 'SendComponent'; - export class SendComponent implements OnInit { disableSend = false; @@ -49,9 +45,8 @@ export class SendComponent implements OnInit { constructor(protected sendService: SendService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected environmentService: EnvironmentService, - protected broadcasterService: BroadcasterService, protected ngZone: NgZone, - protected searchService: SearchService, protected policyService: PolicyService, - protected userService: UserService) { } + protected ngZone: NgZone, protected searchService: SearchService, + protected policyService: PolicyService, protected userService: UserService) { } async ngOnInit() { const policies = await this.policyService.getAll(PolicyType.DisableSend); @@ -63,22 +58,6 @@ export class SendComponent implements OnInit { !o.canManagePolicies && policies.some(p => p.organizationId === o.id && p.enabled); }); - - this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { - this.ngZone.run(async () => { - switch (message.command) { - case 'syncCompleted': - if (message.successfully) { - await this.load(); - } - break; - } - }); - }); - } - - ngOnDestroy() { - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); } async load(filter: (send: SendView) => boolean = null) { @@ -87,8 +66,10 @@ export class SendComponent implements OnInit { this.sends = sends; if (this.onSuccessfulLoad != null) { await this.onSuccessfulLoad(); + } else { + // Default action + this.selectAll(); } - this.selectAll(); this.loading = false; this.loaded = true; } From 0b79f112b973eb17739c319c1268e580f4bc69e6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 12 Feb 2021 11:04:09 -0500 Subject: [PATCH 1162/1626] detect dark theme based on system theme (#271) --- src/electron/services/electronPlatformUtils.service.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/electron/services/electronPlatformUtils.service.ts b/src/electron/services/electronPlatformUtils.service.ts index 29f84de807..9d97e33854 100644 --- a/src/electron/services/electronPlatformUtils.service.ts +++ b/src/electron/services/electronPlatformUtils.service.ts @@ -3,7 +3,6 @@ import { ipcRenderer, remote, shell, - // nativeTheme, } from 'electron'; import * as fs from 'fs'; @@ -222,14 +221,13 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { } getDefaultSystemTheme() { - return 'light' as 'light' | 'dark'; - // return nativeTheme.shouldUseDarkColors ? 'dark' : 'light'; + return remote.nativeTheme.shouldUseDarkColors ? 'dark' : 'light'; } onDefaultSystemThemeChange(callback: ((theme: 'light' | 'dark') => unknown)) { - // nativeTheme.on('updated', () => { - // callback(this.getDefaultSystemTheme()); - // }); + remote.nativeTheme.on('updated', () => { + callback(this.getDefaultSystemTheme()); + }); } supportsSecureStorage(): boolean { From 1968dbfee2db237a14997bfbd234fcca87315332 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 12 Feb 2021 17:25:47 -0500 Subject: [PATCH 1163/1626] refactored a few properties into getters (#272) --- .../components/send/add-edit.component.ts | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/angular/components/send/add-edit.component.ts b/src/angular/components/send/add-edit.component.ts index 995b2c54ba..f955f07c54 100644 --- a/src/angular/components/send/add-edit.component.ts +++ b/src/angular/components/send/add-edit.component.ts @@ -35,9 +35,7 @@ export class AddEditComponent implements OnInit { copyLink = false; disableSend = false; - editMode: boolean = false; send: SendView; - title: string; deletionDate: string; expirationDate: string; hasPassword: boolean; @@ -94,15 +92,19 @@ export class AddEditComponent implements OnInit { await this.load(); } - async load() { - this.editMode = this.sendId != null; - if (this.editMode) { - this.editMode = true; - this.title = this.i18nService.t('editSend'); - } else { - this.title = this.i18nService.t('createSend'); - } + get editMode(): boolean { + return this.sendId != null; + } + get title(): string { + return this.i18nService.t( + this.editMode ? + 'editSend' : + 'createSend' + ); + } + + async load() { const policies = await this.policyService.getAll(PolicyType.DisableSend); const organizations = await this.userService.getAllOrganizations(); this.disableSend = organizations.some(o => { From 7941664a59f90a1b510b13d37062b90210da3b3c Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Mon, 15 Feb 2021 10:16:12 -0600 Subject: [PATCH 1164/1626] Lock lowdb storage file to avoid dirty data collisions (#273) * Lock lowdb storage file to avoid dirty data collisions * Retry lock acquire rather than immediately fail * Add proper-lockfile types to dev dependencies * remove proper-lockfile from jslib. This package is incompatible with Browser implementations. * await lock on create --- src/services/lowdbStorage.service.ts | 86 +++++++++++++++++----------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/src/services/lowdbStorage.service.ts b/src/services/lowdbStorage.service.ts index aa69717a19..03890b8bd2 100644 --- a/src/services/lowdbStorage.service.ts +++ b/src/services/lowdbStorage.service.ts @@ -10,34 +10,40 @@ import { NodeUtils } from '../misc/nodeUtils'; import { Utils } from '../misc/utils'; export class LowdbStorageService implements StorageService { + protected dataFilePath: string; private db: lowdb.LowdbSync; private defaults: any; - private dataFilePath: string; - constructor(private logService: LogService, defaults?: any, dir?: string, private allowCache = false) { + constructor(protected logService: LogService, defaults?: any, private dir?: string, private allowCache = false) { this.defaults = defaults; + } + async init() { this.logService.info('Initializing lowdb storage service.'); let adapter: lowdb.AdapterSync; - if (Utils.isNode && dir != null) { - if (!fs.existsSync(dir)) { - this.logService.warning(`Could not find dir, "${dir}"; creating it instead.`); - NodeUtils.mkdirpSync(dir, '700'); - this.logService.info(`Created dir "${dir}".`); - } - this.dataFilePath = path.join(dir, 'data.json'); - if (!fs.existsSync(this.dataFilePath)) { - this.logService.warning(`Could not find data file, "${this.dataFilePath}"; creating it instead.`); - fs.writeFileSync(this.dataFilePath, '', { mode: 0o600 }); - fs.chmodSync(this.dataFilePath, 0o600); - this.logService.info(`Created data file "${this.dataFilePath}" with chmod 600.`); + if (Utils.isNode && this.dir != null) { + if (!fs.existsSync(this.dir)) { + this.logService.warning(`Could not find dir, "${this.dir}"; creating it instead.`); + NodeUtils.mkdirpSync(this.dir, '700'); + this.logService.info(`Created dir "${this.dir}".`); } + this.dataFilePath = path.join(this.dir, 'data.json'); + await this.lockDbFile(() => { + if (!fs.existsSync(this.dataFilePath)) { + this.logService.warning(`Could not find data file, "${this.dataFilePath}"; creating it instead.`); + fs.writeFileSync(this.dataFilePath, '', { mode: 0o600 }); + fs.chmodSync(this.dataFilePath, 0o600); + this.logService.info(`Created data file "${this.dataFilePath}" with chmod 600.`); + } else { + this.logService.info(`db file "${this.dataFilePath} already exists"; using existing db`); + } + }); adapter = new FileSync(this.dataFilePath); } try { this.logService.info('Attempting to create lowdb storage adapter.'); this.db = lowdb(adapter); - this.logService.info('Successfuly created lowdb storage adapter.'); + this.logService.info('Successfully created lowdb storage adapter.'); } catch (e) { if (e instanceof SyntaxError) { this.logService.warning(`Error creating lowdb storage adapter, "${e.message}"; emptying data file.`); @@ -48,36 +54,50 @@ export class LowdbStorageService implements StorageService { throw e; } } - } - init() { if (this.defaults != null) { - this.logService.info('Writing defaults.'); - this.readForNoCache(); - this.db.defaults(this.defaults).write(); - this.logService.info('Successfully wrote defaults to db.'); + this.lockDbFile(() => { + this.logService.info('Writing defaults.'); + this.readForNoCache(); + this.db.defaults(this.defaults).write(); + this.logService.info('Successfully wrote defaults to db.'); + }); } } get(key: string): Promise { - this.readForNoCache(); - const val = this.db.get(key).value(); - if (val == null) { - return Promise.resolve(null); - } - return Promise.resolve(val as T); + return this.lockDbFile(() => { + this.readForNoCache(); + const val = this.db.get(key).value(); + this.logService.debug(`Successfully read ${key} from db`); + if (val == null) { + return null; + } + return val as T; + }); } save(key: string, obj: any): Promise { - this.readForNoCache(); - this.db.set(key, obj).write(); - return Promise.resolve(); + return this.lockDbFile(() => { + this.readForNoCache(); + this.db.set(key, obj).write(); + this.logService.debug(`Successfully wrote ${key} to db`); + return; + }); } remove(key: string): Promise { - this.readForNoCache(); - this.db.unset(key).write(); - return Promise.resolve(); + return this.lockDbFile(() => { + this.readForNoCache(); + this.db.unset(key).write(); + this.logService.debug(`Successfully removed ${key} from db`); + return; + }); + } + + protected async lockDbFile(action: () => T): Promise { + // Lock methods implemented in clients + return Promise.resolve(action()); } private readForNoCache() { From fc1275aeb2f893f88c65e2fbd9868952d24d3123 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 16 Feb 2021 22:29:57 -0600 Subject: [PATCH 1165/1626] Do not lock until after the file is created (#274) Proper-lockfile throws if the file it's locking does not exist. Lock around adapter creation rather than file creation. --- src/services/lowdbStorage.service.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/services/lowdbStorage.service.ts b/src/services/lowdbStorage.service.ts index 03890b8bd2..b41289f902 100644 --- a/src/services/lowdbStorage.service.ts +++ b/src/services/lowdbStorage.service.ts @@ -28,17 +28,17 @@ export class LowdbStorageService implements StorageService { this.logService.info(`Created dir "${this.dir}".`); } this.dataFilePath = path.join(this.dir, 'data.json'); + if (!fs.existsSync(this.dataFilePath)) { + this.logService.warning(`Could not find data file, "${this.dataFilePath}"; creating it instead.`); + fs.writeFileSync(this.dataFilePath, '', { mode: 0o600 }); + fs.chmodSync(this.dataFilePath, 0o600); + this.logService.info(`Created data file "${this.dataFilePath}" with chmod 600.`); + } else { + this.logService.info(`db file "${this.dataFilePath} already exists"; using existing db`); + } await this.lockDbFile(() => { - if (!fs.existsSync(this.dataFilePath)) { - this.logService.warning(`Could not find data file, "${this.dataFilePath}"; creating it instead.`); - fs.writeFileSync(this.dataFilePath, '', { mode: 0o600 }); - fs.chmodSync(this.dataFilePath, 0o600); - this.logService.info(`Created data file "${this.dataFilePath}" with chmod 600.`); - } else { - this.logService.info(`db file "${this.dataFilePath} already exists"; using existing db`); - } + adapter = new FileSync(this.dataFilePath); }); - adapter = new FileSync(this.dataFilePath); } try { this.logService.info('Attempting to create lowdb storage adapter.'); From b0ae1bfa4cb3bc2642e1ecb14c6c1f0eceb06cb6 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Thu, 18 Feb 2021 19:08:20 +0100 Subject: [PATCH 1166/1626] Add support for extending VaultTimeoutService (#275) --- src/services/vaultTimeout.service.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/services/vaultTimeout.service.ts b/src/services/vaultTimeout.service.ts index 98e5c02f26..f559d618ea 100644 --- a/src/services/vaultTimeout.service.ts +++ b/src/services/vaultTimeout.service.ts @@ -22,7 +22,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { constructor(private cipherService: CipherService, private folderService: FolderService, private collectionService: CollectionService, private cryptoService: CryptoService, - private platformUtilsService: PlatformUtilsService, private storageService: StorageService, + protected platformUtilsService: PlatformUtilsService, private storageService: StorageService, private messagingService: MessagingService, private searchService: SearchService, private userService: UserService, private tokenService: TokenService, private lockedCallback: () => Promise = null, private loggedOutCallback: () => Promise = null) { @@ -35,11 +35,15 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { this.inited = true; if (checkOnInterval) { - this.checkVaultTimeout(); - setInterval(() => this.checkVaultTimeout(), 10 * 1000); // check every 10 seconds + this.startCheck(); } } + startCheck() { + this.checkVaultTimeout(); + setInterval(() => this.checkVaultTimeout(), 10 * 1000); // check every 10 seconds + } + // Keys aren't stored for a device that is locked or logged out. async isLocked(): Promise { const hasKey = await this.cryptoService.hasKey(); From 5f5580cff9abb09fa1c1e090a3014fdb4b04dc12 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Thu, 18 Feb 2021 16:04:12 -0500 Subject: [PATCH 1167/1626] added toggle options logic (#276) --- src/angular/components/send/add-edit.component.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/angular/components/send/add-edit.component.ts b/src/angular/components/send/add-edit.component.ts index f955f07c54..370aa4b0fb 100644 --- a/src/angular/components/send/add-edit.component.ts +++ b/src/angular/components/send/add-edit.component.ts @@ -51,6 +51,7 @@ export class AddEditComponent implements OnInit { expirationDateSelect: number = null; canAccessPremium = true; premiumRequiredAlertShown = false; + showOptions = false; private webVaultUrl: string; @@ -246,6 +247,10 @@ export class AddEditComponent implements OnInit { } } + toggleOptions() { + this.showOptions = !this.showOptions; + } + protected async loadSend(): Promise { return this.sendService.get(this.sendId); } From 8a3b551c6e4113035dc40a379f43f8778d208ffe Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Fri, 19 Feb 2021 10:23:51 -0600 Subject: [PATCH 1168/1626] [Send] Updated delete function for add/edit component (#277) * [Send] Updated delete function for add/edit component * Saved final return statement --- src/angular/components/send/add-edit.component.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/angular/components/send/add-edit.component.ts b/src/angular/components/send/add-edit.component.ts index 370aa4b0fb..34764b3537 100644 --- a/src/angular/components/send/add-edit.component.ts +++ b/src/angular/components/send/add-edit.component.ts @@ -219,16 +219,16 @@ export class AddEditComponent implements OnInit { } } - async delete(): Promise { + async delete(): Promise { if (this.deletePromise != null) { - return; + return false; } const confirmed = await this.platformUtilsService.showDialog( this.i18nService.t('deleteSendConfirmation'), this.i18nService.t('deleteSend'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); if (!confirmed) { - return; + return false; } try { @@ -237,7 +237,10 @@ export class AddEditComponent implements OnInit { this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend')); await this.load(); this.onDeletedSend.emit(this.send); + return true; } catch { } + + return false; } typeChanged() { From 11b8c5b5e884744aa1706daca59a2e1f8ba94304 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Mon, 22 Feb 2021 20:32:40 -0600 Subject: [PATCH 1169/1626] Added clickable state to callout component (#282) --- src/angular/components/callout.component.html | 2 +- src/angular/components/callout.component.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/angular/components/callout.component.html b/src/angular/components/callout.component.html index 31e1d10e96..11405b4a58 100644 --- a/src/angular/components/callout.component.html +++ b/src/angular/components/callout.component.html @@ -1,4 +1,4 @@ -