mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-28 12:45:45 +01:00
copy common ts code over from browser repo
This commit is contained in:
parent
ace8bd5e85
commit
a95632294f
8
src/enums/browserType.enum.ts
Normal file
8
src/enums/browserType.enum.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export enum BrowserType {
|
||||
Chrome = 2,
|
||||
Firefox = 3,
|
||||
Opera = 4,
|
||||
Edge = 5,
|
||||
Vivaldi = 19,
|
||||
Safari = 20,
|
||||
}
|
6
src/enums/cipherType.enum.ts
Normal file
6
src/enums/cipherType.enum.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export enum CipherType {
|
||||
Login = 1,
|
||||
SecureNote = 2,
|
||||
Card = 3,
|
||||
Identity = 4,
|
||||
}
|
9
src/enums/encryptionType.enum.ts
Normal file
9
src/enums/encryptionType.enum.ts
Normal file
@ -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,
|
||||
}
|
5
src/enums/fieldType.enum.ts
Normal file
5
src/enums/fieldType.enum.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum FieldType {
|
||||
Text = 0,
|
||||
Hidden = 1,
|
||||
Boolean = 2,
|
||||
}
|
3
src/enums/secureNoteType.enum.ts
Normal file
3
src/enums/secureNoteType.enum.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export enum SecureNoteType {
|
||||
Generic = 0,
|
||||
}
|
20
src/models/data/attachmentData.ts
Normal file
20
src/models/data/attachmentData.ts
Normal file
@ -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;
|
20
src/models/data/cardData.ts
Normal file
20
src/models/data/cardData.ts
Normal file
@ -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;
|
87
src/models/data/cipherData.ts
Normal file
87
src/models/data/cipherData.ts
Normal file
@ -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;
|
16
src/models/data/collectionData.ts
Normal file
16
src/models/data/collectionData.ts
Normal file
@ -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;
|
16
src/models/data/fieldData.ts
Normal file
16
src/models/data/fieldData.ts
Normal file
@ -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;
|
18
src/models/data/folderData.ts
Normal file
18
src/models/data/folderData.ts
Normal file
@ -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;
|
44
src/models/data/identityData.ts
Normal file
44
src/models/data/identityData.ts
Normal file
@ -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;
|
16
src/models/data/loginData.ts
Normal file
16
src/models/data/loginData.ts
Normal file
@ -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;
|
12
src/models/data/secureNoteData.ts
Normal file
12
src/models/data/secureNoteData.ts
Normal file
@ -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;
|
43
src/models/domain/attachment.ts
Normal file
43
src/models/domain/attachment.ts
Normal file
@ -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<any> {
|
||||
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;
|
22
src/models/domain/autofillField.ts
Normal file
22
src/models/domain/autofillField.ts
Normal file
@ -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;
|
||||
}
|
7
src/models/domain/autofillForm.ts
Normal file
7
src/models/domain/autofillForm.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export default class AutofillForm {
|
||||
opid: string;
|
||||
htmlName: string;
|
||||
htmlID: string;
|
||||
htmlAction: string;
|
||||
htmlMethod: string;
|
||||
}
|
13
src/models/domain/autofillPageDetails.ts
Normal file
13
src/models/domain/autofillPageDetails.ts
Normal file
@ -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;
|
||||
}
|
12
src/models/domain/autofillScript.ts
Normal file
12
src/models/domain/autofillScript.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
43
src/models/domain/card.ts
Normal file
43
src/models/domain/card.ts
Normal file
@ -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<any> {
|
||||
return this.decryptObj({}, {
|
||||
cardholderName: null,
|
||||
brand: null,
|
||||
number: null,
|
||||
expMonth: null,
|
||||
expYear: null,
|
||||
code: null,
|
||||
}, orgId);
|
||||
}
|
||||
}
|
||||
|
||||
export { Card };
|
||||
(window as any).Card = Card;
|
192
src/models/domain/cipher.ts
Normal file
192
src/models/domain/cipher.ts
Normal file
@ -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<any> {
|
||||
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;
|
115
src/models/domain/cipherString.ts
Normal file
115
src/models/domain/cipherString.ts
Normal file
@ -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;
|
37
src/models/domain/collection.ts
Normal file
37
src/models/domain/collection.ts
Normal file
@ -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<any> {
|
||||
const model = {
|
||||
id: this.id,
|
||||
organizationId: this.organizationId,
|
||||
};
|
||||
|
||||
return this.decryptObj(model, {
|
||||
name: null,
|
||||
}, this.organizationId);
|
||||
}
|
||||
}
|
||||
|
||||
export { Collection };
|
||||
(window as any).Collection = Collection;
|
46
src/models/domain/domain.ts
Normal file
46
src/models/domain/domain.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
8
src/models/domain/encryptedObject.ts
Normal file
8
src/models/domain/encryptedObject.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import SymmetricCryptoKey from './symmetricCryptoKey';
|
||||
|
||||
export default class EncryptedObject {
|
||||
iv: Uint8Array;
|
||||
ct: Uint8Array;
|
||||
mac: Uint8Array;
|
||||
key: SymmetricCryptoKey;
|
||||
}
|
5
src/models/domain/environmentUrls.ts
Normal file
5
src/models/domain/environmentUrls.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default class EnvironmentUrls {
|
||||
base: string;
|
||||
api: string;
|
||||
identity: string;
|
||||
}
|
39
src/models/domain/field.ts
Normal file
39
src/models/domain/field.ts
Normal file
@ -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<any> {
|
||||
const model = {
|
||||
type: this.type,
|
||||
};
|
||||
|
||||
return this.decryptObj(model, {
|
||||
name: null,
|
||||
value: null,
|
||||
}, orgId);
|
||||
}
|
||||
}
|
||||
|
||||
export { Field };
|
||||
(window as any).Field = Field;
|
34
src/models/domain/folder.ts
Normal file
34
src/models/domain/folder.ts
Normal file
@ -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<any> {
|
||||
const model = {
|
||||
id: this.id,
|
||||
};
|
||||
|
||||
return this.decryptObj(model, {
|
||||
name: null,
|
||||
}, null);
|
||||
}
|
||||
}
|
||||
|
||||
export { Folder };
|
||||
(window as any).Folder = Folder;
|
79
src/models/domain/identity.ts
Normal file
79
src/models/domain/identity.ts
Normal file
@ -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<any> {
|
||||
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;
|
37
src/models/domain/login.ts
Normal file
37
src/models/domain/login.ts
Normal file
@ -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<any> {
|
||||
return this.decryptObj({}, {
|
||||
uri: null,
|
||||
username: null,
|
||||
password: null,
|
||||
totp: null,
|
||||
}, orgId);
|
||||
}
|
||||
}
|
||||
|
||||
export { Login };
|
||||
(window as any).Login = Login;
|
9
src/models/domain/passwordHistory.ts
Normal file
9
src/models/domain/passwordHistory.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export default class PasswordHistory {
|
||||
password: string;
|
||||
date: number;
|
||||
|
||||
constructor(password: string, date: number) {
|
||||
this.password = password;
|
||||
this.date = date;
|
||||
}
|
||||
}
|
27
src/models/domain/secureNote.ts
Normal file
27
src/models/domain/secureNote.ts
Normal file
@ -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;
|
80
src/models/domain/symmetricCryptoKey.ts
Normal file
80
src/models/domain/symmetricCryptoKey.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
9
src/models/domain/symmetricCryptoKeyBuffers.ts
Normal file
9
src/models/domain/symmetricCryptoKeyBuffers.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export default class SymmetricCryptoKeyBuffers {
|
||||
key: ArrayBuffer;
|
||||
encKey?: ArrayBuffer;
|
||||
macKey?: ArrayBuffer;
|
||||
|
||||
constructor(key: ArrayBuffer) {
|
||||
this.key = key;
|
||||
}
|
||||
}
|
89
src/models/request/cipherRequest.ts
Normal file
89
src/models/request/cipherRequest.ts
Normal file
@ -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;
|
19
src/models/request/deviceRequest.ts
Normal file
19
src/models/request/deviceRequest.ts
Normal file
@ -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;
|
10
src/models/request/deviceTokenRequest.ts
Normal file
10
src/models/request/deviceTokenRequest.ts
Normal file
@ -0,0 +1,10 @@
|
||||
class DeviceTokenRequest {
|
||||
pushToken: string;
|
||||
|
||||
constructor() {
|
||||
this.pushToken = null;
|
||||
}
|
||||
}
|
||||
|
||||
export { DeviceTokenRequest };
|
||||
(window as any).DeviceTokenRequest = DeviceTokenRequest;
|
12
src/models/request/folderRequest.ts
Normal file
12
src/models/request/folderRequest.ts
Normal file
@ -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;
|
10
src/models/request/passwordHintRequest.ts
Normal file
10
src/models/request/passwordHintRequest.ts
Normal file
@ -0,0 +1,10 @@
|
||||
class PasswordHintRequest {
|
||||
email: string;
|
||||
|
||||
constructor(email: string) {
|
||||
this.email = email;
|
||||
}
|
||||
}
|
||||
|
||||
export { PasswordHintRequest };
|
||||
(window as any).PasswordHintRequest = PasswordHintRequest;
|
18
src/models/request/registerRequest.ts
Normal file
18
src/models/request/registerRequest.ts
Normal file
@ -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;
|
49
src/models/request/tokenRequest.ts
Normal file
49
src/models/request/tokenRequest.ts
Normal file
@ -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;
|
12
src/models/request/twoFactorEmailRequest.ts
Normal file
12
src/models/request/twoFactorEmailRequest.ts
Normal file
@ -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;
|
18
src/models/response/attachmentResponse.ts
Normal file
18
src/models/response/attachmentResponse.ts
Normal file
@ -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;
|
44
src/models/response/cipherResponse.ts
Normal file
44
src/models/response/cipherResponse.ts
Normal file
@ -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;
|
14
src/models/response/collectionResponse.ts
Normal file
14
src/models/response/collectionResponse.ts
Normal file
@ -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;
|
20
src/models/response/deviceResponse.ts
Normal file
20
src/models/response/deviceResponse.ts
Normal file
@ -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;
|
20
src/models/response/domainsResponse.ts
Normal file
20
src/models/response/domainsResponse.ts
Normal file
@ -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;
|
37
src/models/response/errorResponse.ts
Normal file
37
src/models/response/errorResponse.ts
Normal file
@ -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;
|
14
src/models/response/folderResponse.ts
Normal file
14
src/models/response/folderResponse.ts
Normal file
@ -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;
|
14
src/models/response/globalDomainResponse.ts
Normal file
14
src/models/response/globalDomainResponse.ts
Normal file
@ -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;
|
24
src/models/response/identityTokenResponse.ts
Normal file
24
src/models/response/identityTokenResponse.ts
Normal file
@ -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;
|
12
src/models/response/keysResponse.ts
Normal file
12
src/models/response/keysResponse.ts
Normal file
@ -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;
|
10
src/models/response/listResponse.ts
Normal file
10
src/models/response/listResponse.ts
Normal file
@ -0,0 +1,10 @@
|
||||
class ListResponse {
|
||||
data: any;
|
||||
|
||||
constructor(data: any) {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
export { ListResponse };
|
||||
(window as any).ListResponse = ListResponse;
|
30
src/models/response/profileOrganizationResponse.ts
Normal file
30
src/models/response/profileOrganizationResponse.ts
Normal file
@ -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;
|
39
src/models/response/profileResponse.ts
Normal file
39
src/models/response/profileResponse.ts
Normal file
@ -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;
|
44
src/models/response/syncResponse.ts
Normal file
44
src/models/response/syncResponse.ts
Normal file
@ -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;
|
28
src/services/abstractions/crypto.service.ts
Normal file
28
src/services/abstractions/crypto.service.ts
Normal file
@ -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<any>;
|
||||
setKeyHash(keyHash: string): Promise<{}>;
|
||||
setEncKey(encKey: string): Promise<{}>;
|
||||
setEncPrivateKey(encPrivateKey: string): Promise<{}>;
|
||||
setOrgKeys(orgs: ProfileOrganizationResponse[]): Promise<{}>;
|
||||
getKey(): Promise<SymmetricCryptoKey>;
|
||||
getKeyHash(): Promise<string>;
|
||||
getEncKey(): Promise<SymmetricCryptoKey>;
|
||||
getPrivateKey(): Promise<ArrayBuffer>;
|
||||
getOrgKeys(): Promise<Map<string, SymmetricCryptoKey>>;
|
||||
getOrgKey(orgId: string): Promise<SymmetricCryptoKey>;
|
||||
clearKeys(): Promise<any>;
|
||||
toggleKey(): Promise<any>;
|
||||
makeKey(password: string, salt: string): SymmetricCryptoKey;
|
||||
hashPassword(password: string, key: SymmetricCryptoKey): Promise<string>;
|
||||
makeEncKey(key: SymmetricCryptoKey): Promise<CipherString>;
|
||||
encrypt(plainValue: string | Uint8Array, key?: SymmetricCryptoKey, plainValueEncoding?: string): Promise<CipherString>;
|
||||
encryptToBytes(plainValue: ArrayBuffer, key?: SymmetricCryptoKey): Promise<ArrayBuffer>;
|
||||
decrypt(cipherString: CipherString, key?: SymmetricCryptoKey, outputEncoding?: string): Promise<string>;
|
||||
decryptFromBytes(encBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise<ArrayBuffer>;
|
||||
rsaDecrypt(encValue: string): Promise<string>;
|
||||
}
|
22
src/services/abstractions/utils.service.ts
Normal file
22
src/services/abstractions/utils.service.ts
Normal file
@ -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<any>;
|
||||
removeFromStorage(key: string): Promise<any>;
|
||||
getObjFromStorage<T>(key: string): Promise<T>;
|
||||
}
|
454
src/services/api.service.ts
Normal file
454
src/services/api.service.ts
Normal file
@ -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<IdentityTokenResponse | any> {
|
||||
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<any> {
|
||||
try {
|
||||
await this.doRefreshToken();
|
||||
} catch (e) {
|
||||
return Promise.reject(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Two Factor APIs
|
||||
|
||||
async postTwoFactorEmail(request: TwoFactorEmailRequest): Promise<any> {
|
||||
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<number> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
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<FolderResponse> {
|
||||
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<FolderResponse> {
|
||||
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<any> {
|
||||
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<CipherResponse> {
|
||||
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<CipherResponse> {
|
||||
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<any> {
|
||||
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<CipherResponse> {
|
||||
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<any> {
|
||||
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<SyncResponse> {
|
||||
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<ErrorResponse> {
|
||||
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<string> {
|
||||
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<IdentityTokenResponse> {
|
||||
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('&');
|
||||
}
|
||||
}
|
31
src/services/appId.service.ts
Normal file
31
src/services/appId.service.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import UtilsService from './utils.service';
|
||||
|
||||
export default class AppIdService {
|
||||
static getAppId(): Promise<string> {
|
||||
return AppIdService.makeAndGetAppId('appId');
|
||||
}
|
||||
|
||||
static getAnonymousAppId(): Promise<string> {
|
||||
return AppIdService.makeAndGetAppId('anonymousAppId');
|
||||
}
|
||||
|
||||
private static async makeAndGetAppId(key: string) {
|
||||
const existingId = await UtilsService.getObjFromStorage<string>(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<string> {
|
||||
return AppIdService.getAppId();
|
||||
}
|
||||
|
||||
getAnonymousAppId(): Promise<string> {
|
||||
return AppIdService.getAnonymousAppId();
|
||||
}
|
||||
}
|
872
src/services/autofill.service.ts
Normal file
872
src/services/autofill.service.ts
Normal file
@ -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<string> = 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<any> {
|
||||
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;
|
||||
}
|
||||
}
|
514
src/services/cipher.service.ts
Normal file
514
src/services/cipher.service.ts
Normal file
@ -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<Cipher> {
|
||||
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<Field[]> {
|
||||
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<Field> {
|
||||
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<Cipher> {
|
||||
const userId = await this.userService.getUserId();
|
||||
const localData = await UtilsService.getObjFromStorage<any>(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<Cipher[]> {
|
||||
const userId = await this.userService.getUserId();
|
||||
const localData = await UtilsService.getObjFromStorage<any>(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<any[]> {
|
||||
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<any[]> {
|
||||
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<any[]> {
|
||||
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<any> {
|
||||
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<void> {
|
||||
let ciphersLocalData = await UtilsService.getObjFromStorage<any>(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<void> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
const userId = await this.userService.getUserId();
|
||||
await UtilsService.saveObjToStorage(Keys.ciphersPrefix + userId, ciphers);
|
||||
this.decryptedCipherCache = null;
|
||||
}
|
||||
|
||||
async clear(userId: string): Promise<any> {
|
||||
await UtilsService.removeFromStorage(Keys.ciphersPrefix + userId);
|
||||
this.decryptedCipherCache = null;
|
||||
}
|
||||
|
||||
async delete(id: string | string[]): Promise<any> {
|
||||
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<any> {
|
||||
await this.apiService.deleteCipher(id);
|
||||
await this.delete(id);
|
||||
}
|
||||
|
||||
async deleteAttachment(id: string, attachmentId: string): Promise<void> {
|
||||
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<void> {
|
||||
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<void[]> {
|
||||
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<any> {
|
||||
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.');
|
||||
}
|
||||
}
|
||||
}
|
124
src/services/collection.service.ts
Normal file
124
src/services/collection.service.ts
Normal file
@ -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<Collection> {
|
||||
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<Collection[]> {
|
||||
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<any[]> {
|
||||
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<Promise<any>> = [];
|
||||
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<any> {
|
||||
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<any> {
|
||||
const userId = await this.userService.getUserId();
|
||||
await UtilsService.saveObjToStorage(Keys.collectionsPrefix + userId, collections);
|
||||
this.decryptedCollectionCache = null;
|
||||
}
|
||||
|
||||
async clear(userId: string): Promise<any> {
|
||||
await UtilsService.removeFromStorage(Keys.collectionsPrefix + userId);
|
||||
this.decryptedCollectionCache = null;
|
||||
}
|
||||
|
||||
async delete(id: string | string[]): Promise<any> {
|
||||
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;
|
||||
}
|
||||
}
|
116
src/services/constants.service.ts
Normal file
116
src/services/constants.service.ts
Normal file
@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
597
src/services/crypto.service.ts
Normal file
597
src/services/crypto.service.ts
Normal file
@ -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<string, SymmetricCryptoKey>;
|
||||
|
||||
async setKey(key: SymmetricCryptoKey): Promise<any> {
|
||||
this.key = key;
|
||||
|
||||
const option = await UtilsService.getObjFromStorage<number>(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<SymmetricCryptoKey> {
|
||||
if (this.key != null) {
|
||||
return this.key;
|
||||
}
|
||||
|
||||
const option = await UtilsService.getObjFromStorage<number>(ConstantsService.lockOptionKey);
|
||||
if (option != null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const key = await UtilsService.getObjFromStorage<string>(Keys.key);
|
||||
if (key) {
|
||||
this.key = new SymmetricCryptoKey(key, true);
|
||||
}
|
||||
|
||||
return key == null ? null : this.key;
|
||||
}
|
||||
|
||||
getKeyHash(): Promise<string> {
|
||||
if (this.keyHash != null) {
|
||||
return Promise.resolve(this.keyHash);
|
||||
}
|
||||
|
||||
return UtilsService.getObjFromStorage<string>(Keys.keyHash);
|
||||
}
|
||||
|
||||
async getEncKey(): Promise<SymmetricCryptoKey> {
|
||||
if (this.encKey != null) {
|
||||
return this.encKey;
|
||||
}
|
||||
|
||||
const encKey = await UtilsService.getObjFromStorage<string>(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<ArrayBuffer> {
|
||||
if (this.privateKey != null) {
|
||||
return this.privateKey;
|
||||
}
|
||||
|
||||
const encPrivateKey = await UtilsService.getObjFromStorage<string>(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<Map<string, SymmetricCryptoKey>> {
|
||||
if (this.orgKeys != null && this.orgKeys.size > 0) {
|
||||
return this.orgKeys;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
const encOrgKeys = await UtilsService.getObjFromStorage<any>(Keys.encOrgKeys);
|
||||
if (!encOrgKeys) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const orgKeys: Map<string, SymmetricCryptoKey> = new Map<string, SymmetricCryptoKey>();
|
||||
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<SymmetricCryptoKey> {
|
||||
if (orgId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const orgKeys = await this.getOrgKeys();
|
||||
if (orgKeys == null || !orgKeys.has(orgId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return orgKeys.get(orgId);
|
||||
}
|
||||
|
||||
clearKey(): Promise<any> {
|
||||
this.key = this.legacyEtmKey = null;
|
||||
return UtilsService.removeFromStorage(Keys.key);
|
||||
}
|
||||
|
||||
clearKeyHash(): Promise<any> {
|
||||
this.keyHash = null;
|
||||
return UtilsService.removeFromStorage(Keys.keyHash);
|
||||
}
|
||||
|
||||
clearEncKey(memoryOnly?: boolean): Promise<any> {
|
||||
this.encKey = null;
|
||||
if (memoryOnly) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return UtilsService.removeFromStorage(Keys.encKey);
|
||||
}
|
||||
|
||||
clearPrivateKey(memoryOnly?: boolean): Promise<any> {
|
||||
this.privateKey = null;
|
||||
if (memoryOnly) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return UtilsService.removeFromStorage(Keys.encPrivateKey);
|
||||
}
|
||||
|
||||
clearOrgKeys(memoryOnly?: boolean): Promise<any> {
|
||||
this.orgKeys = null;
|
||||
if (memoryOnly) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return UtilsService.removeFromStorage(Keys.encOrgKeys);
|
||||
}
|
||||
|
||||
clearKeys(): Promise<any> {
|
||||
return Promise.all([
|
||||
this.clearKey(),
|
||||
this.clearKeyHash(),
|
||||
this.clearOrgKeys(),
|
||||
this.clearEncKey(),
|
||||
this.clearPrivateKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
async toggleKey(): Promise<any> {
|
||||
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<string> {
|
||||
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<CipherString> {
|
||||
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<CipherString> {
|
||||
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<ArrayBuffer> {
|
||||
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<string> {
|
||||
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<ArrayBuffer> {
|
||||
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<string> {
|
||||
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<EncryptedObject> {
|
||||
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<any> {
|
||||
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<ArrayBuffer> {
|
||||
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<ArrayBuffer> {
|
||||
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<boolean> {
|
||||
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<SymmetricCryptoKey> {
|
||||
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;
|
||||
}
|
||||
}
|
87
src/services/environment.service.ts
Normal file
87
src/services/environment.service.ts
Normal file
@ -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<void> {
|
||||
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<any> {
|
||||
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;
|
||||
}
|
||||
}
|
161
src/services/folder.service.ts
Normal file
161
src/services/folder.service.ts
Normal file
@ -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<Folder> {
|
||||
const folder = new Folder();
|
||||
folder.id = model.id;
|
||||
folder.name = await this.cryptoService.encrypt(model.name);
|
||||
return folder;
|
||||
}
|
||||
|
||||
async get(id: string): Promise<Folder> {
|
||||
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<Folder[]> {
|
||||
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<any[]> {
|
||||
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<Promise<any>> = [];
|
||||
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<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
const userId = await this.userService.getUserId();
|
||||
await UtilsService.saveObjToStorage(Keys.foldersPrefix + userId, folders);
|
||||
this.decryptedFolderCache = null;
|
||||
}
|
||||
|
||||
async clear(userId: string): Promise<any> {
|
||||
await UtilsService.removeFromStorage(Keys.foldersPrefix + userId);
|
||||
this.decryptedFolderCache = null;
|
||||
}
|
||||
|
||||
async delete(id: string | string[]): Promise<any> {
|
||||
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<any> {
|
||||
await this.apiService.deleteFolder(id);
|
||||
await this.delete(id);
|
||||
}
|
||||
}
|
28
src/services/i18n.service.ts
Normal file
28
src/services/i18n.service.ts
Normal file
@ -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;
|
||||
},
|
||||
});
|
||||
}
|
89
src/services/lock.service.ts
Normal file
89
src/services/lock.service.ts
Normal file
@ -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<number>(ConstantsService.lockOptionKey);
|
||||
if (lockOption === -2) {
|
||||
self.lock();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async checkLock(): Promise<void> {
|
||||
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<number>(ConstantsService.lockOptionKey);
|
||||
if (lockOption == null || lockOption < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastActive = await UtilsService.getObjFromStorage<number>(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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
237
src/services/passwordGeneration.service.ts
Normal file
237
src/services/passwordGeneration.service.ts
Normal file
@ -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<PasswordHistory[]>(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<PasswordHistory>();
|
||||
}
|
||||
|
||||
async addHistory(password: string): Promise<any> {
|
||||
// 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<any> {
|
||||
this.history = [];
|
||||
return await UtilsService.removeFromStorage(Keys.history);
|
||||
}
|
||||
|
||||
private async encryptHistory(): Promise<PasswordHistory[]> {
|
||||
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<PasswordHistory[]> {
|
||||
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;
|
||||
}
|
||||
}
|
61
src/services/settings.service.ts
Normal file
61
src/services/settings.service.ts
Normal file
@ -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<any> {
|
||||
return this.getSettingsKey(Keys.equivalentDomains);
|
||||
}
|
||||
|
||||
async setEquivalentDomains(equivalentDomains: string[][]) {
|
||||
await this.setSettingsKey(Keys.equivalentDomains, equivalentDomains);
|
||||
}
|
||||
|
||||
async clear(userId: string): Promise<void> {
|
||||
await UtilsService.removeFromStorage(Keys.settingsPrefix + userId);
|
||||
this.settingsCache = null;
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
private async getSettings(): Promise<any> {
|
||||
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<any> {
|
||||
const settings = await this.getSettings();
|
||||
if (settings != null && settings[key]) {
|
||||
return settings[key];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async setSettingsKey(key: string, value: any): Promise<void> {
|
||||
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;
|
||||
}
|
||||
}
|
180
src/services/sync.service.ts
Normal file
180
src/services/sync.service.ts
Normal file
@ -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<any>(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);
|
||||
}
|
||||
}
|
170
src/services/token.service.ts
Normal file
170
src/services/token.service.ts
Normal file
@ -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<any> {
|
||||
return Promise.all([
|
||||
this.setToken(accessToken),
|
||||
this.setRefreshToken(refreshToken),
|
||||
]);
|
||||
}
|
||||
|
||||
setToken(token: string): Promise<any> {
|
||||
this.token = token;
|
||||
this.decodedToken = null;
|
||||
return UtilsService.saveObjToStorage(Keys.accessToken, token);
|
||||
}
|
||||
|
||||
async getToken(): Promise<string> {
|
||||
if (this.token != null) {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
this.token = await UtilsService.getObjFromStorage<string>(Keys.accessToken);
|
||||
return this.token;
|
||||
}
|
||||
|
||||
setRefreshToken(refreshToken: string): Promise<any> {
|
||||
this.refreshToken = refreshToken;
|
||||
return UtilsService.saveObjToStorage(Keys.refreshToken, refreshToken);
|
||||
}
|
||||
|
||||
async getRefreshToken(): Promise<string> {
|
||||
if (this.refreshToken != null) {
|
||||
return this.refreshToken;
|
||||
}
|
||||
|
||||
this.refreshToken = await UtilsService.getObjFromStorage<string>(Keys.refreshToken);
|
||||
return this.refreshToken;
|
||||
}
|
||||
|
||||
setTwoFactorToken(token: string, email: string): Promise<any> {
|
||||
return UtilsService.saveObjToStorage(Keys.twoFactorTokenPrefix + email, token);
|
||||
}
|
||||
|
||||
getTwoFactorToken(email: string): Promise<string> {
|
||||
return UtilsService.getObjFromStorage<string>(Keys.twoFactorTokenPrefix + email);
|
||||
}
|
||||
|
||||
clearTwoFactorToken(email: string): Promise<any> {
|
||||
return UtilsService.removeFromStorage(Keys.twoFactorTokenPrefix + email);
|
||||
}
|
||||
|
||||
clearToken(): Promise<any> {
|
||||
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;
|
||||
}
|
||||
}
|
113
src/services/totp.service.ts
Normal file
113
src/services/totp.service.ts
Normal file
@ -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<string> {
|
||||
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<boolean> {
|
||||
return !(await UtilsService.getObjFromStorage<boolean>(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);
|
||||
}
|
||||
}
|
79
src/services/user.service.ts
Normal file
79
src/services/user.service.ts
Normal file
@ -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<any> {
|
||||
this.email = email;
|
||||
this.userId = userId;
|
||||
|
||||
return Promise.all([
|
||||
UtilsService.saveObjToStorage(Keys.userEmail, email),
|
||||
UtilsService.saveObjToStorage(Keys.userId, userId),
|
||||
]);
|
||||
}
|
||||
|
||||
setSecurityStamp(stamp: string): Promise<any> {
|
||||
this.stamp = stamp;
|
||||
return UtilsService.saveObjToStorage(Keys.stamp, stamp);
|
||||
}
|
||||
|
||||
async getUserId(): Promise<string> {
|
||||
if (this.userId != null) {
|
||||
return this.userId;
|
||||
}
|
||||
|
||||
this.userId = await UtilsService.getObjFromStorage<string>(Keys.userId);
|
||||
return this.userId;
|
||||
}
|
||||
|
||||
async getEmail(): Promise<string> {
|
||||
if (this.email != null) {
|
||||
return this.email;
|
||||
}
|
||||
|
||||
this.email = await UtilsService.getObjFromStorage<string>(Keys.userEmail);
|
||||
return this.email;
|
||||
}
|
||||
|
||||
async getSecurityStamp(): Promise<string> {
|
||||
if (this.stamp != null) {
|
||||
return this.stamp;
|
||||
}
|
||||
|
||||
this.stamp = await UtilsService.getObjFromStorage<string>(Keys.stamp);
|
||||
return this.stamp;
|
||||
}
|
||||
|
||||
async clear(): Promise<any> {
|
||||
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<boolean> {
|
||||
const token = await this.tokenService.getToken();
|
||||
if (token == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const userId = await this.getUserId();
|
||||
return userId != null;
|
||||
}
|
||||
}
|
113
src/services/utils.service.spec.ts
Normal file
113
src/services/utils.service.spec.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
407
src/services/utils.service.ts
Normal file
407
src/services/utils.service.ts
Normal file
@ -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<T>(key: string): Promise<T> {
|
||||
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<any> {
|
||||
return UtilsService.saveObjToStorage(key, obj);
|
||||
}
|
||||
|
||||
removeFromStorage(key: string): Promise<any> {
|
||||
return UtilsService.removeFromStorage(key);
|
||||
}
|
||||
|
||||
getObjFromStorage<T>(key: string): Promise<T> {
|
||||
return UtilsService.getObjFromStorage<T>(key);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user