1
0
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:
Kyle Spearrin 2018-01-03 21:20:41 -05:00
parent ace8bd5e85
commit a95632294f
77 changed files with 6179 additions and 0 deletions

View File

@ -0,0 +1,8 @@
export enum BrowserType {
Chrome = 2,
Firefox = 3,
Opera = 4,
Edge = 5,
Vivaldi = 19,
Safari = 20,
}

View File

@ -0,0 +1,6 @@
export enum CipherType {
Login = 1,
SecureNote = 2,
Card = 3,
Identity = 4,
}

View 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,
}

View File

@ -0,0 +1,5 @@
export enum FieldType {
Text = 0,
Hidden = 1,
Boolean = 2,
}

View File

@ -0,0 +1,3 @@
export enum SecureNoteType {
Generic = 0,
}

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;
}

View File

@ -0,0 +1,7 @@
export default class AutofillForm {
opid: string;
htmlName: string;
htmlID: string;
htmlAction: string;
htmlMethod: string;
}

View 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;
}

View 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
View 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
View 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;

View 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;

View 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;

View 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;
}
}

View File

@ -0,0 +1,8 @@
import SymmetricCryptoKey from './symmetricCryptoKey';
export default class EncryptedObject {
iv: Uint8Array;
ct: Uint8Array;
mac: Uint8Array;
key: SymmetricCryptoKey;
}

View File

@ -0,0 +1,5 @@
export default class EnvironmentUrls {
base: string;
api: string;
identity: string;
}

View 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;

View 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;

View 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;

View 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;

View File

@ -0,0 +1,9 @@
export default class PasswordHistory {
password: string;
date: number;
constructor(password: string, date: number) {
this.password = password;
this.date = date;
}
}

View 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;

View 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;
}
}

View File

@ -0,0 +1,9 @@
export default class SymmetricCryptoKeyBuffers {
key: ArrayBuffer;
encKey?: ArrayBuffer;
macKey?: ArrayBuffer;
constructor(key: ArrayBuffer) {
this.key = key;
}
}

View 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;

View 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;

View File

@ -0,0 +1,10 @@
class DeviceTokenRequest {
pushToken: string;
constructor() {
this.pushToken = null;
}
}
export { DeviceTokenRequest };
(window as any).DeviceTokenRequest = DeviceTokenRequest;

View 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;

View File

@ -0,0 +1,10 @@
class PasswordHintRequest {
email: string;
constructor(email: string) {
this.email = email;
}
}
export { PasswordHintRequest };
(window as any).PasswordHintRequest = PasswordHintRequest;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@ -0,0 +1,10 @@
class ListResponse {
data: any;
constructor(data: any) {
this.data = data;
}
}
export { ListResponse };
(window as any).ListResponse = ListResponse;

View 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;

View 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;

View 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;

View 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>;
}

View 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
View 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('&');
}
}

View 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();
}
}

View 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;
}
}

View 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.');
}
}
}

View 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;
}
}

View 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,
},
];
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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;
},
});
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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;
}
}

View 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);
}
}

View 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;
}
}

View 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);
});
});
});

View 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);
}
}